Refactored namespaces
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ArchiveZipper.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring> // strerror
|
||||
#include <fstream>
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::zip
|
||||
{
|
||||
std::unique_ptr<IZipper> createArchiveZipper(const EntryContainer& entries)
|
||||
{
|
||||
return std::make_unique<ArchiveZipper>(entries);
|
||||
}
|
||||
|
||||
class FileException : public Exception
|
||||
{
|
||||
public:
|
||||
FileException(const std::filesystem::path& p, std::string_view message)
|
||||
: Exception{ "File '" + p.string() + "': " + std::string {message} }
|
||||
{}
|
||||
|
||||
FileException(const std::filesystem::path& p, std::string_view message, int err)
|
||||
: Exception{ "File '" + p.string() + "': " + std::string {message} + ": " + ::strerror(err) }
|
||||
{}
|
||||
};
|
||||
|
||||
class ArchiveException : public Exception
|
||||
{
|
||||
public:
|
||||
ArchiveException(struct ::archive* arch)
|
||||
: Exception{ getError(arch) }
|
||||
{}
|
||||
|
||||
static std::string_view getError(struct ::archive* arch)
|
||||
{
|
||||
const char* str{ archive_error_string(arch) };
|
||||
if (!str)
|
||||
{
|
||||
static std::string unknownError{ "Unknown archive error" };
|
||||
return unknownError;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
void ArchiveZipper::ArchiveDeleter::operator()(struct ::archive* arch)
|
||||
{
|
||||
const int res{ ::archive_write_free(arch) };
|
||||
if (res != ARCHIVE_OK)
|
||||
LMS_LOG(UTILS, ERROR, "Failure while freeing archive control struct: " << std::string{ ::strerror(res) });
|
||||
}
|
||||
|
||||
void ArchiveZipper::ArchiveEntryDeleter::operator()(struct ::archive_entry* archEntry)
|
||||
{
|
||||
::archive_entry_free(archEntry);
|
||||
}
|
||||
|
||||
ArchiveZipper::ArchiveZipper(const EntryContainer& entries)
|
||||
: _entries{ entries }
|
||||
, _readBuffer(_readBufferSize, {})
|
||||
, _currentEntry{ std::cbegin(_entries) }
|
||||
{
|
||||
_archive = ArchivePtr{ ::archive_write_new() };
|
||||
if (!_archive)
|
||||
throw Exception{ "Cannot create archive control struct" };
|
||||
|
||||
auto archiveOpen{ [](struct ::archive*, void*)
|
||||
{
|
||||
return ARCHIVE_OK;
|
||||
} };
|
||||
|
||||
auto archiveWrite{ [](struct ::archive*, void* clientData, const void* buff, ::size_t n) -> la_ssize_t
|
||||
{
|
||||
ArchiveZipper* zipper {static_cast<ArchiveZipper*>(clientData)};
|
||||
return zipper->onWriteCallback(static_cast<const std::byte*>(buff), n);
|
||||
} };
|
||||
|
||||
auto archiveClose{ [](struct ::archive*, void*)
|
||||
{
|
||||
return ARCHIVE_OK;
|
||||
} };
|
||||
|
||||
if (::archive_write_set_bytes_per_block(_archive.get(), _writeBlockSize) != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
|
||||
// 1 => no padding for last block
|
||||
if (::archive_write_set_bytes_in_last_block(_archive.get(), 1) != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
|
||||
if (::archive_write_set_format_zip(_archive.get()) != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
|
||||
if (::archive_write_set_option(_archive.get(), "zip", "compression", "deflate") != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
|
||||
int res{ ::archive_write_open(_archive.get(), this, archiveOpen, archiveWrite, archiveClose) };
|
||||
if (res != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
}
|
||||
|
||||
std::uint64_t ArchiveZipper::writeSome(std::ostream& output)
|
||||
{
|
||||
assert(!_currentOutputStream);
|
||||
|
||||
_currentOutputStream = &output;
|
||||
_bytesWrittenInCurrentOutputStream = 0;
|
||||
|
||||
while (_bytesWrittenInCurrentOutputStream == 0)
|
||||
{
|
||||
if (!_currentArchiveEntry)
|
||||
{
|
||||
if (_currentEntry == std::cend(_entries))
|
||||
{
|
||||
if (::archive_write_close(_archive.get()) != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
|
||||
_archive.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
_currentArchiveEntry = createArchiveEntry(*_currentEntry);
|
||||
_currentEntryOffset = 0;
|
||||
if (::archive_write_header(_archive.get(), _currentArchiveEntry.get()) != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
}
|
||||
|
||||
if (writeSomeCurrentFileData())
|
||||
{
|
||||
// entry complete
|
||||
if (::archive_write_finish_entry(_archive.get()) != ARCHIVE_OK)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
|
||||
_currentArchiveEntry.reset();
|
||||
_currentEntry++;
|
||||
}
|
||||
}
|
||||
|
||||
_currentOutputStream = nullptr;
|
||||
return _bytesWrittenInCurrentOutputStream;
|
||||
}
|
||||
|
||||
bool ArchiveZipper::isComplete() const
|
||||
{
|
||||
return !_archive;
|
||||
}
|
||||
|
||||
void ArchiveZipper::abort()
|
||||
{
|
||||
LMS_LOG(UTILS, DEBUG, "Aborting zip creation");
|
||||
if (_archive)
|
||||
{
|
||||
::archive_write_fail(_archive.get());
|
||||
_archive.reset();
|
||||
}
|
||||
}
|
||||
|
||||
static ::mode_t permsToMode(const std::filesystem::perms p)
|
||||
{
|
||||
using std::filesystem::perms;
|
||||
::mode_t mode{};
|
||||
|
||||
auto testPerm{ [](perms p, perms permToTest)
|
||||
{
|
||||
return (p & permToTest) == permToTest;
|
||||
} };
|
||||
|
||||
if (testPerm(p, perms::owner_read))
|
||||
mode |= S_IRUSR;
|
||||
if (testPerm(p, perms::owner_write))
|
||||
mode |= S_IWUSR;
|
||||
if (testPerm(p, perms::owner_exec))
|
||||
mode |= S_IXUSR;
|
||||
if (testPerm(p, perms::group_read))
|
||||
mode |= S_IRGRP;
|
||||
if (testPerm(p, perms::group_write))
|
||||
mode |= S_IWGRP;
|
||||
if (testPerm(p, perms::group_exec))
|
||||
mode |= S_IXGRP;
|
||||
if (testPerm(p, perms::others_read))
|
||||
mode |= S_IROTH;
|
||||
if (testPerm(p, perms::others_write))
|
||||
mode |= S_IWOTH;
|
||||
if (testPerm(p, perms::others_exec))
|
||||
mode |= S_IXOTH;
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
ArchiveZipper::ArchiveEntryPtr ArchiveZipper::createArchiveEntry(const Entry& entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::is_regular_file(entry.filePath))
|
||||
throw FileException{ entry.filePath, "not a regular file" };
|
||||
|
||||
ArchiveEntryPtr archiveEntry{ archive_entry_new() };
|
||||
if (!archiveEntry)
|
||||
throw Exception{ "Cannot create archive entry control struct" };
|
||||
|
||||
archive_entry_set_pathname(archiveEntry.get(), entry.fileName.c_str());
|
||||
archive_entry_set_size(archiveEntry.get(), std::filesystem::file_size(entry.filePath));
|
||||
archive_entry_set_mode(archiveEntry.get(), permsToMode(std::filesystem::status(entry.filePath).permissions()));
|
||||
archive_entry_set_filetype(archiveEntry.get(), AE_IFREG);
|
||||
|
||||
return archiveEntry;
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& error)
|
||||
{
|
||||
throw FileException{ entry.filePath, error.what() };
|
||||
}
|
||||
}
|
||||
|
||||
bool ArchiveZipper::writeSomeCurrentFileData()
|
||||
{
|
||||
assert(_currentEntry != std::cend(_entries));
|
||||
|
||||
std::ifstream ifs{ _currentEntry->filePath.c_str(), std::ios_base::binary };
|
||||
if (!ifs)
|
||||
throw FileException{ _currentEntry->filePath, "cannot open file", errno };
|
||||
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const std::uint64_t fileSize{ static_cast<std::uint64_t>(ifs.tellg()) };
|
||||
ifs.seekg(0, std::ios::beg);
|
||||
|
||||
// TODO: store file size?
|
||||
if (fileSize < _currentEntryOffset)
|
||||
throw FileException{ _currentEntry->filePath, "size changed?" };
|
||||
|
||||
const std::uint64_t bytesToRead{ std::min(fileSize - _currentEntryOffset, static_cast<std::uint64_t>(_readBufferSize)) };
|
||||
|
||||
// read from file
|
||||
if (!ifs.seekg(_currentEntryOffset, std::ios::beg))
|
||||
throw FileException{ _currentEntry->filePath, "seek failed", errno };
|
||||
|
||||
if (!ifs.read(reinterpret_cast<char*>(&_readBuffer[0]), bytesToRead))
|
||||
throw FileException{ _currentEntry->filePath, "read failed", errno };
|
||||
|
||||
const std::uint64_t actualBytesRead{ static_cast<std::uint64_t>(ifs.gcount()) };
|
||||
|
||||
// write to archive
|
||||
{
|
||||
std::uint64_t remainingBytesToWrite{ actualBytesRead };
|
||||
while (remainingBytesToWrite > 0)
|
||||
{
|
||||
const auto writtenBytes{ archive_write_data(_archive.get(), &_readBuffer[actualBytesRead - remainingBytesToWrite], remainingBytesToWrite) };
|
||||
if (writtenBytes < 0)
|
||||
throw ArchiveException{ _archive.get() };
|
||||
|
||||
assert(static_cast<std::uint64_t>(writtenBytes) <= remainingBytesToWrite);
|
||||
remainingBytesToWrite -= writtenBytes;
|
||||
}
|
||||
}
|
||||
|
||||
_currentEntryOffset += actualBytesRead;
|
||||
return (_currentEntryOffset >= fileSize);
|
||||
}
|
||||
|
||||
std::int64_t ArchiveZipper::onWriteCallback(const std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
if (!_currentOutputStream)
|
||||
{
|
||||
archive_set_error(_archive.get(), EIO, "IO error: operation cancelled");
|
||||
return -1;
|
||||
}
|
||||
|
||||
_currentOutputStream->write(reinterpret_cast<const char*>(buffer), bufferSize);
|
||||
if (!*_currentOutputStream)
|
||||
throw Exception{ "Failed to write " + std::to_string(bufferSize) + " bytes in final archive output!" };
|
||||
|
||||
_bytesWrittenInCurrentOutputStream += bufferSize;
|
||||
|
||||
return bufferSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include "core/IZipper.hpp"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
struct archive;
|
||||
struct archive_entry;
|
||||
}
|
||||
|
||||
namespace lms::zip
|
||||
{
|
||||
class ArchiveZipper : public IZipper
|
||||
{
|
||||
public:
|
||||
ArchiveZipper(const EntryContainer& files);
|
||||
ArchiveZipper(const ArchiveZipper&) = delete;
|
||||
ArchiveZipper& operator=(const ArchiveZipper&) = delete;
|
||||
|
||||
private:
|
||||
std::uint64_t writeSome(std::ostream& output) override;
|
||||
bool isComplete() const override;
|
||||
void abort() override;
|
||||
|
||||
class ArchiveDeleter
|
||||
{
|
||||
public:
|
||||
void operator()(struct ::archive* arch);
|
||||
};
|
||||
using ArchivePtr = std::unique_ptr<struct ::archive, ArchiveDeleter>;
|
||||
|
||||
class ArchiveEntryDeleter
|
||||
{
|
||||
public:
|
||||
void operator()(struct ::archive_entry* archEntry);
|
||||
};
|
||||
using ArchiveEntryPtr = std::unique_ptr<struct ::archive_entry, ArchiveEntryDeleter>;
|
||||
|
||||
void prepareCurrentEntry();
|
||||
static ArchiveEntryPtr createArchiveEntry(const Entry& entry);
|
||||
bool writeSomeCurrentFileData();
|
||||
std::int64_t onWriteCallback(const std::byte* buff, std::size_t size);
|
||||
|
||||
const EntryContainer _entries;
|
||||
ArchivePtr _archive;
|
||||
|
||||
static inline constexpr std::size_t _writeBlockSize{ 65536 };
|
||||
static inline constexpr std::size_t _readBufferSize{ 65536 };
|
||||
std::vector<std::byte> _readBuffer;
|
||||
|
||||
EntryContainer::const_iterator _currentEntry;
|
||||
ArchiveEntryPtr _currentArchiveEntry;
|
||||
|
||||
std::uint64_t _currentEntryOffset{};
|
||||
std::ostream* _currentOutputStream{};
|
||||
std::uint64_t _bytesWrittenInCurrentOutputStream{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChildProcess.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <stdexcept>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
|
||||
#include <boost/asio/read.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class SystemException : public ChildProcessException
|
||||
{
|
||||
public:
|
||||
SystemException(int err, const std::string& errMsg)
|
||||
: ChildProcessException{ errMsg + ": " + ::strerror(err) }
|
||||
{}
|
||||
|
||||
SystemException(boost::system::error_code ec, const std::string& errMsg)
|
||||
: ChildProcessException{ errMsg + ": " + ec.message() }
|
||||
{}
|
||||
};
|
||||
}
|
||||
|
||||
ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args)
|
||||
: _ioContext{ ioContext }
|
||||
, _childStdout{ _ioContext }
|
||||
|
||||
{
|
||||
// make sure only one thread is executing this part of code
|
||||
static std::mutex mutex;
|
||||
std::unique_lock<std::mutex> lock{ mutex };
|
||||
|
||||
int pipe[2];
|
||||
|
||||
int res{ pipe2(pipe, O_NONBLOCK | O_CLOEXEC) };
|
||||
if (res < 0)
|
||||
throw SystemException{ errno, "pipe2 failed!" };
|
||||
|
||||
{
|
||||
#if defined(__linux__) && defined(F_SETPIPE_SZ)
|
||||
// Just a hint here to prevent the writer from writing too many bytes ahead of the reader
|
||||
constexpr std::size_t pipeSize{ 65536 * 4 };
|
||||
|
||||
if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1)
|
||||
throw SystemException{ errno, "fcntl failed!" };
|
||||
if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1)
|
||||
throw SystemException{ errno, "fcntl failed!" };
|
||||
#endif
|
||||
}
|
||||
|
||||
res = fork();
|
||||
if (res == -1)
|
||||
throw SystemException{ errno, "fork failed!" };
|
||||
|
||||
if (res == 0) // CHILD
|
||||
{
|
||||
close(pipe[0]);
|
||||
close(STDIN_FILENO);
|
||||
close(STDERR_FILENO);
|
||||
|
||||
// Replace stdout with pipe write
|
||||
if (dup2(pipe[1], STDOUT_FILENO) == -1)
|
||||
exit(-1);
|
||||
|
||||
std::vector<const char*> execArgs;
|
||||
std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); });
|
||||
execArgs.push_back(nullptr);
|
||||
|
||||
res = execv(path.string().c_str(), (char* const*)&execArgs[0]);
|
||||
if (res == -1)
|
||||
exit(-1);
|
||||
}
|
||||
else // PARENT
|
||||
{
|
||||
close(pipe[1]);
|
||||
{
|
||||
boost::system::error_code assignError;
|
||||
_childStdout.assign(pipe[0], assignError);
|
||||
if (assignError)
|
||||
throw SystemException{ assignError, "fork failed!" };
|
||||
}
|
||||
_childPID = res;
|
||||
}
|
||||
}
|
||||
|
||||
ChildProcess::~ChildProcess()
|
||||
{
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Closing child process...");
|
||||
{
|
||||
boost::system::error_code closeError;
|
||||
_childStdout.close(closeError);
|
||||
if (closeError)
|
||||
LMS_LOG(CHILDPROCESS, ERROR, "Closed failed: " << closeError.message());
|
||||
}
|
||||
|
||||
if (!_finished)
|
||||
kill();
|
||||
|
||||
wait(true);
|
||||
}
|
||||
|
||||
void ChildProcess::kill()
|
||||
{
|
||||
// process may already have finished
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process...");
|
||||
if (::kill(_childPID, SIGKILL) == -1)
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << ::strerror(errno));
|
||||
}
|
||||
|
||||
bool ChildProcess::wait(bool block)
|
||||
{
|
||||
assert(!_waited);
|
||||
|
||||
int wstatus{};
|
||||
const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) };
|
||||
|
||||
if (pid == -1)
|
||||
throw SystemException{ errno, "waitpid failed!" };
|
||||
else if (pid == 0)
|
||||
return false;
|
||||
|
||||
if (WIFEXITED(wstatus))
|
||||
{
|
||||
_exitCode = WEXITSTATUS(wstatus);
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Exit code = " << *_exitCode);
|
||||
}
|
||||
|
||||
_waited = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback)
|
||||
{
|
||||
assert(!finished());
|
||||
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Async read, bufferSize = " << bufferSize);
|
||||
|
||||
boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize),
|
||||
[this, callback{ std::move(callback) }](const boost::system::error_code& error, std::size_t bytesTransferred)
|
||||
{
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred);
|
||||
|
||||
ReadResult readResult{ ReadResult::Success };
|
||||
if (error)
|
||||
{
|
||||
if (error != boost::asio::error::eof)
|
||||
{
|
||||
// forbidden to read any captured param here as the ChildProcess instance may already have been killed
|
||||
return;
|
||||
}
|
||||
|
||||
readResult = ReadResult::EndOfFile;
|
||||
_finished = true;
|
||||
}
|
||||
|
||||
callback(readResult, bytesTransferred);
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t ChildProcess::readSome(std::byte* data, std::size_t bufferSize)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
const std::size_t res{ _childStdout.read_some(boost::asio::buffer(data, bufferSize), ec) };
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "read some " << res << " bytes, ec = " << ec.message());
|
||||
if (ec)
|
||||
_childStdout.close(ec);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool ChildProcess::finished() const
|
||||
{
|
||||
return _finished;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/posix/stream_descriptor.hpp>
|
||||
|
||||
#include "core/IChildProcess.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
class ChildProcess : public IChildProcess
|
||||
{
|
||||
public:
|
||||
~ChildProcess();
|
||||
ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args);
|
||||
|
||||
private:
|
||||
void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) override;
|
||||
std::size_t readSome(std::byte* data, std::size_t bufferSize) override;
|
||||
bool finished() const override;
|
||||
|
||||
void kill();
|
||||
bool wait(bool block); // return true if waited
|
||||
|
||||
using FileDescriptor = boost::asio::posix::stream_descriptor;
|
||||
|
||||
boost::asio::io_context& _ioContext;
|
||||
FileDescriptor _childStdout;
|
||||
::pid_t _childPID{};
|
||||
bool _waited{};
|
||||
bool _finished{};
|
||||
std::optional<int> _exitCode;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChildProcessManager.hpp"
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "ChildProcess.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
std::unique_ptr<IChildProcessManager>
|
||||
createChildProcessManager(boost::asio::io_context& ioContext)
|
||||
{
|
||||
return std::make_unique<ChildProcessManager>(ioContext);
|
||||
}
|
||||
|
||||
ChildProcessManager::ChildProcessManager(boost::asio::io_context& ioContext)
|
||||
: _ioContext{ ioContext }
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<IChildProcess>
|
||||
ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args)
|
||||
{
|
||||
return std::make_unique<ChildProcess>(_ioContext, path, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include "core/IChildProcessManager.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
class ChildProcessManager : public IChildProcessManager
|
||||
{
|
||||
public:
|
||||
ChildProcessManager(boost::asio::io_context& ioContext);
|
||||
~ChildProcessManager() = default;
|
||||
|
||||
ChildProcessManager(const ChildProcessManager&) = delete;
|
||||
ChildProcessManager(ChildProcessManager&&) = delete;
|
||||
ChildProcessManager& operator=(const ChildProcessManager&) = delete;
|
||||
ChildProcessManager& operator=(ChildProcessManager&&) = delete;
|
||||
|
||||
private:
|
||||
std::unique_ptr<IChildProcess> spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) override;
|
||||
|
||||
boost::asio::io_context& _ioContext;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Config.hpp"
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
std::unique_ptr<IConfig> createConfig(const std::filesystem::path& p)
|
||||
{
|
||||
return std::make_unique<Config>(p);
|
||||
}
|
||||
|
||||
Config::Config(const std::filesystem::path& p)
|
||||
{
|
||||
try
|
||||
{
|
||||
_config.readFile(p.string().c_str());
|
||||
}
|
||||
catch (libconfig::FileIOException& e)
|
||||
{
|
||||
throw LmsException{ "Cannot open config file '" + p.string() + "'" };
|
||||
}
|
||||
catch (libconfig::ParseException& e)
|
||||
{
|
||||
throw LmsException{ "Cannot parse config file '" + p.string() + "', line = " + std::to_string(e.getLine()) + ", error = '" + e.getError() + "'" };
|
||||
}
|
||||
catch (libconfig::ConfigException& e)
|
||||
{
|
||||
throw LmsException{ "Cannot open config file '" + p.string() + "': " + e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view Config::getString(std::string_view setting, std::string_view def)
|
||||
{
|
||||
try
|
||||
{
|
||||
return static_cast<const char*>(_config.lookup(std::string{ setting }));
|
||||
}
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
void Config::visitStrings(std::string_view setting, std::function<void(std::string_view)> _func, std::initializer_list<std::string_view> defs)
|
||||
{
|
||||
try
|
||||
{
|
||||
const libconfig::Setting& values{ _config.lookup(std::string {setting}) };
|
||||
for (int i{}; i < values.getLength(); ++i)
|
||||
_func(static_cast<const char*>(values[i]));
|
||||
}
|
||||
catch (const libconfig::SettingNotFoundException&)
|
||||
{
|
||||
for (std::string_view def : defs)
|
||||
_func(def);
|
||||
}
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path Config::getPath(std::string_view setting, const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
const char* res{ _config.lookup(std::string {setting}) };
|
||||
return std::filesystem::path{ std::string(res) };
|
||||
}
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long Config::getULong(std::string_view setting, unsigned long def)
|
||||
{
|
||||
try
|
||||
{
|
||||
return static_cast<unsigned int>(_config.lookup(std::string{ setting }));
|
||||
}
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
long Config::getLong(std::string_view setting, long def)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _config.lookup(std::string{ setting });
|
||||
}
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
bool Config::getBool(std::string_view setting, bool def)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _config.lookup(std::string{ setting });
|
||||
}
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/IConfig.hpp"
|
||||
|
||||
#include <libconfig.h++>
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
// Used to get config values from configuration files
|
||||
class Config final : public IConfig
|
||||
{
|
||||
public:
|
||||
Config(const std::filesystem::path& p);
|
||||
~Config() = default;
|
||||
|
||||
Config(const Config&) = delete;
|
||||
Config& operator=(const Config&) = delete;
|
||||
Config(Config&&) = delete;
|
||||
Config& operator=(Config&&) = delete;
|
||||
|
||||
// Default values are returned in case of setting not found
|
||||
std::string_view getString(std::string_view setting, std::string_view def = "") override;
|
||||
void visitStrings(std::string_view setting, std::function<void(std::string_view)> _func, std::initializer_list<std::string_view> defs) override;
|
||||
std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override;
|
||||
unsigned long getULong(std::string_view setting, unsigned long def = 0) override;
|
||||
long getLong(std::string_view setting, long def = 0) override;
|
||||
bool getBool(std::string_view setting, bool def = false) override;
|
||||
|
||||
private:
|
||||
|
||||
libconfig::Config _config;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FileResourceHandler.hpp"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms
|
||||
{
|
||||
std::unique_ptr<IResourceHandler> createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
|
||||
{
|
||||
return std::make_unique<FileResourceHandler>(path, mimeType);
|
||||
}
|
||||
|
||||
FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
|
||||
: _path{ path }
|
||||
, _mimeType{ mimeType }
|
||||
{
|
||||
}
|
||||
|
||||
Wt::Http::ResponseContinuation* FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
::uint64_t startByte{ _offset };
|
||||
std::ifstream ifs{ _path.string().c_str(), std::ios::in | std::ios::binary };
|
||||
|
||||
if (startByte == 0)
|
||||
{
|
||||
if (!ifs)
|
||||
{
|
||||
LMS_LOG(UTILS, ERROR, "Cannot open file stream for '" << _path.string() << "'");
|
||||
response.setStatus(404);
|
||||
return {};
|
||||
}
|
||||
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) };
|
||||
ifs.seekg(0, std::ios::beg);
|
||||
|
||||
LMS_LOG(UTILS, DEBUG, "File '" << _path.string() << "', fileSize = " << fileSize);
|
||||
|
||||
response.addHeader("Accept-Ranges", "bytes");
|
||||
|
||||
const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(fileSize) };
|
||||
if (!ranges.isSatisfiable())
|
||||
{
|
||||
std::ostringstream contentRange;
|
||||
contentRange << "bytes */" << fileSize;
|
||||
response.setStatus(416); // Requested range not satisfiable
|
||||
response.addHeader("Content-Range", contentRange.str());
|
||||
|
||||
LMS_LOG(UTILS, DEBUG, "Range not satisfiable");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (ranges.size() == 1)
|
||||
{
|
||||
LMS_LOG(UTILS, DEBUG, "Range requested = " << ranges[0].firstByte() << "-" << ranges[0].lastByte());
|
||||
|
||||
response.setStatus(206);
|
||||
startByte = ranges[0].firstByte();
|
||||
_beyondLastByte = ranges[0].lastByte() + 1;
|
||||
|
||||
std::ostringstream contentRange;
|
||||
contentRange << "bytes " << startByte << "-"
|
||||
<< _beyondLastByte - 1 << "/" << fileSize;
|
||||
|
||||
response.addHeader("Content-Range", contentRange.str());
|
||||
response.setContentLength(_beyondLastByte - startByte);
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(UTILS, DEBUG, "No range requested");
|
||||
|
||||
response.setStatus(200);
|
||||
_beyondLastByte = fileSize;
|
||||
response.setContentLength(_beyondLastByte);
|
||||
}
|
||||
|
||||
LMS_LOG(UTILS, DEBUG, "Mimetype set to '" << _mimeType << "'");
|
||||
response.setMimeType(_mimeType);
|
||||
}
|
||||
else if (!ifs)
|
||||
{
|
||||
LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for '" << _path.string() << "'");
|
||||
return {};
|
||||
}
|
||||
|
||||
ifs.seekg(static_cast<std::istream::pos_type>(startByte));
|
||||
|
||||
std::vector<char> buf;
|
||||
buf.resize(_chunkSize);
|
||||
|
||||
::uint64_t restSize = _beyondLastByte - startByte;
|
||||
::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size();
|
||||
|
||||
ifs.read(&buf[0], pieceSize);
|
||||
const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) };
|
||||
if (actualPieceSize > 0)
|
||||
{
|
||||
response.out().write(&buf[0], actualPieceSize);
|
||||
LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << startByte << "-" << startByte + actualPieceSize - 1 << "");
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(UTILS, DEBUG, "Written 0 byte");
|
||||
}
|
||||
|
||||
if (ifs.good() && actualPieceSize < restSize)
|
||||
{
|
||||
_offset = startByte + actualPieceSize;
|
||||
LMS_LOG(UTILS, DEBUG, "Job not complete! Remaining range: " << _offset << "-" << _beyondLastByte - 1);
|
||||
|
||||
return response.createContinuation();
|
||||
}
|
||||
|
||||
LMS_LOG(UTILS, DEBUG, "Job complete!");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "core/IResourceHandler.hpp"
|
||||
|
||||
namespace lms
|
||||
{
|
||||
class FileResourceHandler final : public IResourceHandler
|
||||
{
|
||||
public:
|
||||
FileResourceHandler(const std::filesystem::path& filePath, std::string_view mimeType);
|
||||
|
||||
private:
|
||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||
void abort() override {};
|
||||
|
||||
static constexpr std::size_t _chunkSize{ 262'144 };
|
||||
|
||||
std::filesystem::path _path;
|
||||
std::string _mimeType;
|
||||
::uint64_t _beyondLastByte{};
|
||||
::uint64_t _offset{};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/IOContextRunner.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
IOContextRunner::IOContextRunner(boost::asio::io_service& ioService, std::size_t threadCount, std::string_view name)
|
||||
: _ioService{ ioService }
|
||||
, _work{ ioService }
|
||||
{
|
||||
LMS_LOG(UTILS, INFO, "Starting IO context with " << threadCount << " threads...");
|
||||
|
||||
for (std::size_t i{}; i < threadCount; ++i)
|
||||
{
|
||||
std::string threadName{ name };
|
||||
if (!threadName.empty())
|
||||
{
|
||||
threadName += "Thread_";
|
||||
threadName += std::to_string(i);
|
||||
}
|
||||
|
||||
_threads.emplace_back([this, threadName]
|
||||
{
|
||||
if (!threadName.empty())
|
||||
{
|
||||
if (auto * traceLogger{ Service<tracing::ITraceLogger>::get() })
|
||||
traceLogger->setThreadName(std::this_thread::get_id(), threadName);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ioService.run();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
LMS_LOG(UTILS, FATAL, "Exception caught in IO context: " << e.what());
|
||||
std::abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void IOContextRunner::stop()
|
||||
{
|
||||
LMS_LOG(UTILS, DEBUG, "Stopping IO context...");
|
||||
_work.reset();
|
||||
_ioService.stop();
|
||||
LMS_LOG(UTILS, DEBUG, "IO context stopped!");
|
||||
}
|
||||
|
||||
std::size_t IOContextRunner::getThreadCount() const
|
||||
{
|
||||
return _threads.size();
|
||||
}
|
||||
|
||||
IOContextRunner::~IOContextRunner()
|
||||
{
|
||||
stop();
|
||||
|
||||
for (std::thread& t : _threads)
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
const char* getModuleName(Module mod)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case Module::API_SUBSONIC: return "API_SUBSONIC";
|
||||
case Module::AUTH: return "AUTH";
|
||||
case Module::AV: return "AV";
|
||||
case Module::CHILDPROCESS: return "CHILDPROC";
|
||||
case Module::COVER: return "COVER";
|
||||
case Module::DB: return "DB";
|
||||
case Module::DBUPDATER: return "DB UPDATER";
|
||||
case Module::FEATURE: return "FEATURE";
|
||||
case Module::FEEDBACK: return "FEEDBACK";
|
||||
case Module::HTTP: return "HTTP";
|
||||
case Module::MAIN: return "MAIN";
|
||||
case Module::METADATA: return "METADATA";
|
||||
case Module::REMOTE: return "REMOTE";
|
||||
case Module::SCROBBLING: return "SCROBBLING";
|
||||
case Module::SERVICE: return "SERVICE";
|
||||
case Module::RECOMMENDATION: return "RECOMMENDATION";
|
||||
case Module::TRANSCODING: return "TRANSCODING";
|
||||
case Module::UI: return "UI";
|
||||
case Module::UTILS: return "UTILS";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* getSeverityName(Severity sev)
|
||||
{
|
||||
switch (sev)
|
||||
{
|
||||
case Severity::FATAL: return "fatal";
|
||||
case Severity::ERROR: return "error";
|
||||
case Severity::WARNING: return "warning";
|
||||
case Severity::INFO: return "info";
|
||||
case Severity::DEBUG: return "debug";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Log::Log(ILogger& logger, Module module, Severity severity)
|
||||
: _logger{ logger }
|
||||
, _module{ module }
|
||||
, _severity{ severity }
|
||||
|
||||
{}
|
||||
|
||||
Log::~Log()
|
||||
{
|
||||
assert(_logger.isSeverityActive(_severity));
|
||||
_logger.processLog(*this);
|
||||
}
|
||||
|
||||
std::string Log::getMessage() const
|
||||
{
|
||||
return _oss.str();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/NetAddress.hpp"
|
||||
|
||||
#ifndef BOOST_ASIO_HAS_STD_HASH
|
||||
|
||||
namespace std
|
||||
{
|
||||
std::size_t hash<boost::asio::ip::address>::operator()(const boost::asio::ip::address& ipAddr) const
|
||||
{
|
||||
if (ipAddr.is_v4())
|
||||
return ipAddr.to_v4().to_ulong();
|
||||
|
||||
if (ipAddr.is_v6())
|
||||
{
|
||||
const auto& range {ipAddr.to_v6().to_bytes()};
|
||||
std::size_t res {};
|
||||
|
||||
for (auto b : range)
|
||||
res ^= std::hash<char>{}(static_cast<char>(b));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return std::hash<std::string>{}(ipAddr.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#endif // BOOST_ASIO_HAS_STD_HASH
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/Path.hpp"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
|
||||
#include <boost/tokenizer.hpp>
|
||||
|
||||
#include "core/Crc32Calculator.hpp"
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::core::pathUtils
|
||||
{
|
||||
std::uint32_t computeCrc32(const std::filesystem::path& p)
|
||||
{
|
||||
core::Crc32Calculator crc32;
|
||||
|
||||
std::ifstream ifs{ p.string().c_str(), std::ios_base::binary };
|
||||
if (ifs)
|
||||
{
|
||||
do
|
||||
{
|
||||
std::array<char, 1024> buffer;
|
||||
|
||||
ifs.read(buffer.data(), buffer.size());
|
||||
crc32.processBytes(reinterpret_cast<const std::byte*>(buffer.data()), ifs.gcount());
|
||||
} while (ifs);
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to open file '" << p.string() << "'");
|
||||
throw LmsException("Failed to open file '" + p.string() + "'");
|
||||
}
|
||||
|
||||
return crc32.getResult();
|
||||
}
|
||||
|
||||
bool ensureDirectory(const std::filesystem::path& dir)
|
||||
{
|
||||
if (std::filesystem::exists(dir))
|
||||
return std::filesystem::is_directory(dir);
|
||||
else
|
||||
return std::filesystem::create_directory(dir);
|
||||
}
|
||||
|
||||
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file)
|
||||
{
|
||||
struct stat sb {};
|
||||
|
||||
if (stat(file.string().c_str(), &sb) == -1)
|
||||
throw LmsException("Failed to get stats on file '" + file.string() + "'");
|
||||
|
||||
return Wt::WDateTime::fromTime_t(sb.st_mtime);
|
||||
}
|
||||
|
||||
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
|
||||
{
|
||||
std::error_code ec;
|
||||
std::filesystem::directory_iterator itPath{ directory, std::filesystem::directory_options::follow_directory_symlink, ec };
|
||||
|
||||
if (ec)
|
||||
{
|
||||
cb(ec, directory);
|
||||
return true; // try to continue exploring anyway
|
||||
}
|
||||
|
||||
if (excludeDirFileName && !excludeDirFileName->empty())
|
||||
{
|
||||
const std::filesystem::path excludePath{ directory / *excludeDirFileName };
|
||||
|
||||
if (std::filesystem::exists(excludePath, ec))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Found '" << excludePath.string() << "': skipping directory");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::directory_iterator itEnd;
|
||||
while (itPath != itEnd)
|
||||
{
|
||||
bool continueExploring{ true };
|
||||
|
||||
if (ec)
|
||||
{
|
||||
continueExploring = cb(ec, *itPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (std::filesystem::is_regular_file(*itPath, ec))
|
||||
{
|
||||
continueExploring = cb(ec, *itPath);
|
||||
}
|
||||
else if (std::filesystem::is_directory(*itPath, ec))
|
||||
{
|
||||
if (!ec)
|
||||
continueExploring = exploreFilesRecursive(*itPath, cb, excludeDirFileName);
|
||||
else
|
||||
continueExploring = cb(ec, *itPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!continueExploring)
|
||||
return false;
|
||||
|
||||
itPath.increment(ec);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hasFileAnyExtension(const std::filesystem::path& file, const std::vector<std::filesystem::path>& supportedExtensions)
|
||||
{
|
||||
const std::filesystem::path extension{ stringUtils::stringToLower(file.extension().string()) };
|
||||
|
||||
return (std::find(std::cbegin(supportedExtensions), std::cend(supportedExtensions), extension) != std::cend(supportedExtensions));
|
||||
}
|
||||
|
||||
bool isPathInRootPath(const std::filesystem::path& path, const std::filesystem::path& rootPathArg, const std::filesystem::path* excludeDirFileName)
|
||||
{
|
||||
std::filesystem::path curPath{ path };
|
||||
std::filesystem::path rootPath{ rootPathArg.has_filename() ? rootPathArg : rootPathArg.parent_path() };
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (excludeDirFileName && !excludeDirFileName->empty())
|
||||
{
|
||||
assert(!excludeDirFileName->has_parent_path());
|
||||
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(curPath / *excludeDirFileName, ec))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (curPath == rootPath)
|
||||
return true;
|
||||
|
||||
if (curPath == curPath.root_path())
|
||||
break;
|
||||
|
||||
curPath = curPath.parent_path();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path getLongestCommonPath(const std::filesystem::path& path1, const std::filesystem::path& path2)
|
||||
{
|
||||
std::filesystem::path longestCommonPath;
|
||||
|
||||
auto it1{ path1.begin() };
|
||||
auto it2{ path2.begin() };
|
||||
|
||||
while (it1 != std::cend(path1) && it2 != std::cend(path2) && *it1 == *it2)
|
||||
{
|
||||
longestCommonPath /= *it1;
|
||||
++it1;
|
||||
++it2;
|
||||
}
|
||||
|
||||
return longestCommonPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/Random.hpp"
|
||||
|
||||
namespace lms::core::random
|
||||
{
|
||||
|
||||
RandGenerator& getRandGenerator()
|
||||
{
|
||||
static thread_local std::random_device rd;
|
||||
static thread_local RandGenerator randGenerator(rd());
|
||||
|
||||
return randGenerator;
|
||||
}
|
||||
|
||||
RandGenerator createSeededGenerator(uint_fast32_t seed)
|
||||
{
|
||||
return RandGenerator{ seed };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/RecursiveSharedMutex.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
void RecursiveSharedMutex::lock()
|
||||
{
|
||||
const auto thisThreadId{ std::this_thread::get_id() };
|
||||
|
||||
if (_uniqueOwner == thisThreadId)
|
||||
{
|
||||
// already locked
|
||||
_uniqueCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mutex.lock();
|
||||
_uniqueOwner = thisThreadId;
|
||||
assert(_uniqueCount == 0);
|
||||
_uniqueCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void RecursiveSharedMutex::unlock()
|
||||
{
|
||||
assert(_uniqueCount > 0);
|
||||
|
||||
if (--_uniqueCount == 0)
|
||||
{
|
||||
_uniqueOwner = {};
|
||||
_mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void RecursiveSharedMutex::lock_shared()
|
||||
{
|
||||
const auto thisThreadId{ std::this_thread::get_id() };
|
||||
|
||||
if (_uniqueOwner == thisThreadId)
|
||||
{
|
||||
// alone here, no need to lock
|
||||
_sharedCounts[thisThreadId]++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool needLock{};
|
||||
|
||||
{
|
||||
std::scoped_lock lock{ _sharedCountMutex };
|
||||
|
||||
auto& sharedCount{ _sharedCounts[thisThreadId] };
|
||||
if (sharedCount == 0)
|
||||
needLock = true;
|
||||
else
|
||||
++sharedCount;
|
||||
}
|
||||
|
||||
if (needLock)
|
||||
{
|
||||
_mutex.lock_shared();
|
||||
|
||||
assert(_uniqueOwner == std::thread::id{});
|
||||
|
||||
std::scoped_lock lock{ _sharedCountMutex };
|
||||
_sharedCounts[thisThreadId]++;
|
||||
}
|
||||
}
|
||||
|
||||
void RecursiveSharedMutex::unlock_shared()
|
||||
{
|
||||
const auto thisThreadId{ std::this_thread::get_id() };
|
||||
|
||||
if (_uniqueOwner == thisThreadId)
|
||||
{
|
||||
// alone here, no need to lock
|
||||
auto& sharedCount{ _sharedCounts[thisThreadId] };
|
||||
assert(sharedCount > 0);
|
||||
--sharedCount;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool needUnlock{};
|
||||
|
||||
{
|
||||
std::scoped_lock lock{ _sharedCountMutex };
|
||||
|
||||
auto& sharedCount{ _sharedCounts[thisThreadId] };
|
||||
assert(sharedCount > 0);
|
||||
needUnlock = (--sharedCount == 0);
|
||||
}
|
||||
|
||||
if (needUnlock)
|
||||
_mutex.unlock_shared();
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
bool RecursiveSharedMutex::isUniqueLocked()
|
||||
{
|
||||
return _uniqueOwner == std::this_thread::get_id();
|
||||
}
|
||||
|
||||
bool RecursiveSharedMutex::isSharedLocked()
|
||||
{
|
||||
const auto thisThreadId{ std::this_thread::get_id() };
|
||||
if (_uniqueOwner == thisThreadId)
|
||||
return true;
|
||||
|
||||
std::scoped_lock lock{ _sharedCountMutex };
|
||||
return _sharedCounts[thisThreadId] > 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <cassert>
|
||||
#include <thread>
|
||||
|
||||
#include "core/StreamLogger.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities)
|
||||
: _os{ os }
|
||||
, _severities{ severities }
|
||||
{
|
||||
}
|
||||
|
||||
void StreamLogger::processLog(const Log& log)
|
||||
{
|
||||
assert(isSeverityActive(log.getSeverity()));
|
||||
_os << std::this_thread::get_id() << " [" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <utility>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
namespace lms::core::stringUtils
|
||||
{
|
||||
namespace details
|
||||
{
|
||||
constexpr std::pair<char, std::string_view> jsEscapeChars[]
|
||||
{
|
||||
{ '\\', "\\\\" },
|
||||
{ '\n', "\\n" },
|
||||
{ '\r', "\\r" },
|
||||
{ '\t', "\\t" },
|
||||
{ '"', "\\\"" },
|
||||
{ '\'', "\\\'" },
|
||||
};
|
||||
|
||||
constexpr std::pair<char, std::string_view> jsonEscapeChars[]
|
||||
{
|
||||
{ '\\', "\\\\" },
|
||||
{ '\n', "\\n" },
|
||||
{ '\r', "\\r" },
|
||||
{ '\t', "\\t" },
|
||||
{ '"', "\\\"" },
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
std::string escape(std::string_view str, const std::pair<char, std::string_view>(&charsToEscape)[N])
|
||||
{
|
||||
std::string escaped;
|
||||
escaped.reserve(str.length());
|
||||
|
||||
for (const char c : str)
|
||||
{
|
||||
auto it{ std::find_if(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](const auto& entry) { return entry.first == c; }) };
|
||||
if (it == std::cend(charsToEscape))
|
||||
{
|
||||
escaped += c;
|
||||
continue;
|
||||
}
|
||||
|
||||
escaped += it->second;
|
||||
}
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
template <std::size_t N>
|
||||
void writeEscapedString(std::ostream& os, std::string_view str, const std::pair<char, std::string_view>(&charsToEscape)[N])
|
||||
{
|
||||
for (const char c : str)
|
||||
{
|
||||
auto itEntry{ std::find_if(std::cbegin(charsToEscape), std::cend(charsToEscape), [=](const auto& entry) { return entry.first == c;}) };
|
||||
if (itEntry != std::cend(charsToEscape))
|
||||
os << itEntry->second;
|
||||
else
|
||||
os << c;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename StringType>
|
||||
std::string joinStrings(std::span<const StringType> strings, std::string_view delimiter)
|
||||
{
|
||||
std::string res;
|
||||
bool first{ true };
|
||||
|
||||
for (const StringType& str : strings)
|
||||
{
|
||||
if (!first)
|
||||
res += delimiter;
|
||||
res += str;
|
||||
first = false;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<std::string> readAs(std::string_view str)
|
||||
{
|
||||
return std::string{ str };
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<std::string_view> readAs(std::string_view str)
|
||||
{
|
||||
return str;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<bool> readAs(std::string_view str)
|
||||
{
|
||||
if (str == "1" || str == "true")
|
||||
return true;
|
||||
else if (str == "0" || str == "false")
|
||||
return false;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> splitString(std::string_view str, char separator)
|
||||
{
|
||||
return splitString(str, std::string_view{ &separator, 1 });
|
||||
}
|
||||
|
||||
std::vector<std::string_view> splitString(std::string_view str, std::string_view separator)
|
||||
{
|
||||
std::vector<std::string_view> res;
|
||||
|
||||
if (separator.empty())
|
||||
return { str };
|
||||
|
||||
size_t pos{};
|
||||
size_t found{ str.find(separator) };
|
||||
|
||||
while (found != std::string_view::npos)
|
||||
{
|
||||
res.push_back(str.substr(pos, found - pos));
|
||||
pos = found + separator.size();
|
||||
found = str.find(separator, pos);
|
||||
}
|
||||
|
||||
res.push_back(str.substr(pos));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string_view> strings, std::string_view delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, delimiter);
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string> strings, std::string_view delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, delimiter);
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string> strings, char delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
|
||||
}
|
||||
|
||||
std::string joinStrings(std::span<const std::string_view> strings, char delimiter)
|
||||
{
|
||||
return details::joinStrings(strings, std::string_view{ &delimiter, 1 });
|
||||
}
|
||||
|
||||
std::string escapeAndJoinStrings(std::span<const std::string_view> strings, char delimiter, char escapeChar)
|
||||
{
|
||||
std::string result;
|
||||
for (const std::string_view str : strings)
|
||||
{
|
||||
if (!result.empty())
|
||||
result.push_back(delimiter);
|
||||
|
||||
for (char c : str)
|
||||
{
|
||||
if (c == delimiter || c == escapeChar)
|
||||
result.push_back(escapeChar);
|
||||
|
||||
result.push_back(c);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> splitEscapedStrings(std::string_view str, char delimiter, char escapeChar)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
std::string current;
|
||||
bool escaped{};
|
||||
|
||||
for (char c : str)
|
||||
{
|
||||
if (escaped) {
|
||||
current.push_back(c);
|
||||
escaped = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c == delimiter)
|
||||
{
|
||||
result.push_back(std::move(current));
|
||||
current.clear();
|
||||
}
|
||||
else if (c == escapeChar)
|
||||
escaped = true;
|
||||
else
|
||||
current.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (!current.empty())
|
||||
result.push_back(std::move(current));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
std::string_view res;
|
||||
|
||||
const auto strBegin = str.find_first_not_of(whitespaces);
|
||||
if (strBegin != std::string_view::npos)
|
||||
{
|
||||
const auto strEnd{ str.find_last_not_of(whitespaces) };
|
||||
const auto strRange{ strEnd - strBegin + 1 };
|
||||
|
||||
res = str.substr(strBegin, strRange);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||
}
|
||||
|
||||
std::string stringToLower(std::string_view str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
|
||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void stringToLower(std::string& str)
|
||||
{
|
||||
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
||||
}
|
||||
|
||||
std::string stringToUpper(const std::string& str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
|
||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string bufferToString(std::span<const unsigned char> data)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
for (unsigned char c : data)
|
||||
{
|
||||
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB)
|
||||
{
|
||||
if (strA.size() != strB.size())
|
||||
return false;
|
||||
|
||||
for (std::size_t i{}; i < strA.size(); ++i)
|
||||
{
|
||||
if (std::tolower(strA[i]) != std::tolower(strB[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void capitalize(std::string& str)
|
||||
{
|
||||
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
||||
{
|
||||
if (std::isspace(*it))
|
||||
continue;
|
||||
|
||||
if (std::isalpha(*it))
|
||||
*it = std::toupper(*it);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||
{
|
||||
std::string res{ str };
|
||||
size_t pos = 0;
|
||||
|
||||
while ((pos = res.find(from, pos)) != std::string::npos)
|
||||
{
|
||||
res.replace(pos, from.length(), to);
|
||||
pos += to.length();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string jsEscape(std::string_view str)
|
||||
{
|
||||
return details::escape(str, details::jsEscapeChars);
|
||||
}
|
||||
|
||||
void writeJSEscapedString(std::ostream& os, std::string_view str)
|
||||
{
|
||||
details::writeEscapedString(os, str, details::jsEscapeChars);
|
||||
}
|
||||
|
||||
std::string jsonEscape(std::string_view str)
|
||||
{
|
||||
return details::escape(str, details::jsonEscapeChars);
|
||||
}
|
||||
|
||||
void writeJsonEscapedString(std::ostream& os, std::string_view str)
|
||||
{
|
||||
details::writeEscapedString(os, str, details::jsonEscapeChars);
|
||||
}
|
||||
|
||||
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
|
||||
for (const char c : str)
|
||||
{
|
||||
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
|
||||
res += escapeChar;
|
||||
|
||||
res += c;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string unescapeString(std::string_view str, char escapeChar)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
|
||||
bool escaped{};
|
||||
|
||||
for (char c : str)
|
||||
{
|
||||
if (escaped)
|
||||
{
|
||||
res += c;
|
||||
escaped = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c == escapeChar)
|
||||
escaped = true;
|
||||
else
|
||||
res += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (escaped)
|
||||
res += escapeChar;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool stringEndsWith(std::string_view str, std::string_view ending)
|
||||
{
|
||||
if (str.length() < ending.length())
|
||||
return false;
|
||||
|
||||
return str.substr(str.length() - ending.length()) == ending;
|
||||
}
|
||||
|
||||
std::optional<std::string> stringFromHex(const std::string& str)
|
||||
{
|
||||
static const char lut[]{ "0123456789ABCDEF" };
|
||||
|
||||
if (str.length() % 2 != 0)
|
||||
return std::nullopt;
|
||||
|
||||
std::string res;
|
||||
res.reserve(str.length() / 2);
|
||||
|
||||
auto it{ std::cbegin(str) };
|
||||
while (it != std::cend(str))
|
||||
{
|
||||
unsigned val{};
|
||||
|
||||
auto itHigh{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||
auto itLow{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||
|
||||
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
|
||||
return {};
|
||||
|
||||
val = std::distance(std::cbegin(lut), itHigh) << 4;
|
||||
val += std::distance(std::cbegin(lut), itLow);
|
||||
|
||||
res.push_back(static_cast<char>(val));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string toISO8601String(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
// assume UTC
|
||||
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
|
||||
}
|
||||
|
||||
std::string toISO8601String(const Wt::WDate& date)
|
||||
{
|
||||
// assume UTC
|
||||
return date.toString("yyyy-MM-dd").toUTF8();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TraceLogger.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
namespace lms::core::tracing
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class CurrentThreadUnregisterer
|
||||
{
|
||||
public:
|
||||
CurrentThreadUnregisterer(TraceLogger* logger) : _logger{ logger } {}
|
||||
~CurrentThreadUnregisterer()
|
||||
{
|
||||
if (_logger)
|
||||
_logger->onThreadPreDestroy();
|
||||
}
|
||||
|
||||
private:
|
||||
CurrentThreadUnregisterer(const CurrentThreadUnregisterer&) = delete;
|
||||
CurrentThreadUnregisterer& operator=(const CurrentThreadUnregisterer&) = delete;
|
||||
|
||||
TraceLogger* _logger;
|
||||
};
|
||||
}
|
||||
|
||||
thread_local TraceLogger::Buffer* TraceLogger::_currentBuffer{};
|
||||
|
||||
std::unique_ptr<ITraceLogger> createTraceLogger(Level minLevel, std::size_t bufferSizeInMbytes)
|
||||
{
|
||||
return std::make_unique<TraceLogger>(minLevel, bufferSizeInMbytes);
|
||||
}
|
||||
|
||||
TraceLogger::TraceLogger(Level minLevel, std::size_t bufferSizeinMBytes)
|
||||
: _minLevel{ minLevel }
|
||||
, _start{ clock::now() }
|
||||
, _creatorThreadId{ std::this_thread::get_id() }
|
||||
, _buffers((bufferSizeinMBytes * 1024 * 1024) / BufferSize)
|
||||
{
|
||||
if (bufferSizeinMBytes < MinBufferSizeInMBytes)
|
||||
throw LmsException{ "TraceLogger must be configured with at least " + std::to_string(MinBufferSizeInMBytes) + " MBytes" };
|
||||
|
||||
setThreadName(_creatorThreadId, "MainThread");
|
||||
|
||||
for (Buffer& buffer : _buffers)
|
||||
_freeBuffers.push_back(&buffer);
|
||||
|
||||
LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize));
|
||||
}
|
||||
|
||||
bool TraceLogger::isLevelActive(Level level) const
|
||||
{
|
||||
return static_cast<std::underlying_type_t<Level>>(level) <= static_cast<std::underlying_type_t<Level>>(_minLevel);
|
||||
}
|
||||
|
||||
void TraceLogger::write(const CompleteEvent& event)
|
||||
{
|
||||
if (!_currentBuffer)
|
||||
_currentBuffer = acquireBuffer();
|
||||
|
||||
_currentBuffer->durationEvents[_currentBuffer->currentDurationIndex] = event;
|
||||
|
||||
// update the index after writing the event, in case another thread wants to dump
|
||||
if (++_currentBuffer->currentDurationIndex == _currentBuffer->durationEvents.size())
|
||||
{
|
||||
releaseBuffer(_currentBuffer);
|
||||
_currentBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TraceLogger::onThreadPreDestroy()
|
||||
{
|
||||
if (_currentBuffer)
|
||||
releaseBuffer(_currentBuffer);
|
||||
}
|
||||
|
||||
TraceLogger::Buffer* TraceLogger::acquireBuffer()
|
||||
{
|
||||
// We consider the creator thread will survive the trace logger (thus we don't want to release anything on thread destruction)
|
||||
static thread_local CurrentThreadUnregisterer currentThreadUnregister{ _creatorThreadId == std::this_thread::get_id() ? nullptr : this };
|
||||
|
||||
std::scoped_lock lock{ _mutex };
|
||||
assert(!_freeBuffers.empty());
|
||||
|
||||
TraceLogger::Buffer* buffer{ _freeBuffers.front() };
|
||||
_freeBuffers.pop_front();
|
||||
|
||||
// Empty new buffer only now (we want to keep history on released buffers since we dump them)
|
||||
buffer->currentDurationIndex = 0;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void TraceLogger::releaseBuffer(Buffer* buffer)
|
||||
{
|
||||
assert(buffer);
|
||||
|
||||
std::scoped_lock lock{ _mutex };
|
||||
_freeBuffers.push_back(buffer);
|
||||
}
|
||||
|
||||
void TraceLogger::dumpCurrentBuffer(std::ostream& os)
|
||||
{
|
||||
os << "{" << std::endl;
|
||||
os << "\t\"traceEvents\": [" << std::endl;
|
||||
|
||||
bool first{ true };
|
||||
|
||||
{
|
||||
std::scoped_lock lock{ _threadNameMutex };
|
||||
|
||||
for (const auto& [threadId, threadName] : _threadNames)
|
||||
{
|
||||
if (first)
|
||||
first = false;
|
||||
else
|
||||
os << ", " << std::endl;
|
||||
|
||||
os << "\t\t{ ";
|
||||
os << "\"name\" : \"thread_name\", ";
|
||||
os << "\"pid\" : 1, ";
|
||||
os << "\"tid\" : " << threadId << ", ";
|
||||
os << "\"ph\" : \"M\", ";
|
||||
os << "\"args\" : { \"name\" : \"" + threadName + "\" }";
|
||||
os << " }";
|
||||
}
|
||||
}
|
||||
|
||||
// we allow threads to fill in their current block while dumping
|
||||
{
|
||||
std::scoped_lock lock{ _mutex };
|
||||
|
||||
for (Buffer& buffer : _buffers)
|
||||
{
|
||||
for (std::size_t i{}; i < buffer.currentDurationIndex; ++i)
|
||||
{
|
||||
// Looks like tracing viewer is not pleased when nested event start at the same timestamp
|
||||
// Hence the double representation as the microsecond unit is not precise enough
|
||||
using clockMicro = std::chrono::duration<double, std::micro>;
|
||||
|
||||
const CompleteEvent& event{ buffer.durationEvents[i] };
|
||||
|
||||
if (first)
|
||||
first = false;
|
||||
else
|
||||
os << ", " << std::endl;;
|
||||
|
||||
os << "\t\t{ ";
|
||||
os << "\"name\" : \"" << event.name.c_str() << "\", ";
|
||||
os << "\"cat\" : \"" << event.category.c_str() << "\", ";
|
||||
os << "\"pid\": 1, ";
|
||||
os << "\"tid\" : " << event.threadId << ", ";
|
||||
os << "\"ts\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast<clockMicro>(event.start - _start).count() << ", ";
|
||||
os << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast<clockMicro>(event.duration).count() << ", ";
|
||||
os << "\"ph\" : \"X\"";
|
||||
os << " }";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os << std::endl;
|
||||
os << "\t]," << std::endl;
|
||||
os << "\t\"meta_cpu_count\" : " << std::thread::hardware_concurrency() << std::endl;
|
||||
os << "}" << std::endl;
|
||||
}
|
||||
|
||||
void TraceLogger::setThreadName(std::thread::id id, std::string_view threadName)
|
||||
{
|
||||
std::scoped_lock lock{ _threadNameMutex };
|
||||
_threadNames.emplace(id, threadName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
namespace lms::core::tracing
|
||||
{
|
||||
class TraceLogger : public ITraceLogger
|
||||
{
|
||||
public:
|
||||
TraceLogger(Level minLevel, std::size_t bufferSizeinMBytes);
|
||||
|
||||
void onThreadPreDestroy();
|
||||
|
||||
private:
|
||||
bool isLevelActive(Level level) const override;
|
||||
void write(const CompleteEvent& event) override;
|
||||
void dumpCurrentBuffer(std::ostream& os) override;
|
||||
void setThreadName(std::thread::id id, std::string_view threadName) override;
|
||||
|
||||
static constexpr std::size_t BufferSize{ 32 * 1024 };
|
||||
|
||||
struct alignas(64) Buffer
|
||||
{
|
||||
static constexpr std::size_t CompleteEventCount{ BufferSize / sizeof(CompleteEvent) };
|
||||
|
||||
std::array<CompleteEvent, CompleteEventCount> durationEvents;
|
||||
std::atomic<std::size_t> currentDurationIndex{};
|
||||
};
|
||||
|
||||
Buffer* acquireBuffer();
|
||||
void releaseBuffer(Buffer* buffer);
|
||||
|
||||
const Level _minLevel;
|
||||
const clock::time_point _start;
|
||||
const std::thread::id _creatorThreadId;
|
||||
|
||||
std::vector<Buffer> _buffers; // allocated once during construction
|
||||
|
||||
std::mutex _threadNameMutex;
|
||||
std::unordered_map<std::thread::id, std::string> _threadNames;
|
||||
|
||||
std::mutex _mutex;
|
||||
std::deque<Buffer*> _freeBuffers;
|
||||
|
||||
static thread_local Buffer* _currentBuffer;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/UUID.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
#include "core/Random.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
namespace stringUtils
|
||||
{
|
||||
template <>
|
||||
std::optional<UUID>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
return UUID::fromString(str);
|
||||
}
|
||||
}
|
||||
namespace
|
||||
{
|
||||
bool stringIsUUID(std::string_view str)
|
||||
{
|
||||
static const std::regex re{ R"([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})" };
|
||||
|
||||
return std::regex_match(std::cbegin(str), std::cend(str), re);
|
||||
}
|
||||
}
|
||||
|
||||
UUID::UUID(std::string_view str)
|
||||
: _value{ stringUtils::stringToLower(str) }
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<UUID> UUID::fromString(std::string_view str)
|
||||
{
|
||||
if (!stringIsUUID(str))
|
||||
return std::nullopt;
|
||||
|
||||
return UUID{ str };
|
||||
}
|
||||
|
||||
UUID UUID::generate()
|
||||
{
|
||||
// Form is "123e4567-e89b-12d3-a456-426614174000"
|
||||
// TODO: store 128 bits and only convert to string when necessary
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
auto concatRandomBytes{ [](std::ostream& os, std::size_t byteCount)
|
||||
{
|
||||
for (std::size_t i {}; i < byteCount; ++i)
|
||||
os << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(random::getRandom<std::uint8_t>(0, 255));
|
||||
} };
|
||||
|
||||
concatRandomBytes(oss, 4);
|
||||
oss << "-";
|
||||
concatRandomBytes(oss, 2);
|
||||
oss << "-";
|
||||
concatRandomBytes(oss, 2);
|
||||
oss << "-";
|
||||
concatRandomBytes(oss, 2);
|
||||
oss << "-";
|
||||
concatRandomBytes(oss, 6);
|
||||
|
||||
const auto uuid{ fromString(oss.str()) };
|
||||
assert(uuid);
|
||||
return uuid.value();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/WtLogger.hpp"
|
||||
|
||||
#include <thread>
|
||||
#include <sstream>
|
||||
#include <Wt/WServer.h>
|
||||
#include <Wt/WLogger.h>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::core::logging
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string to_string(std::thread::id id)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << id;
|
||||
return oss.str();
|
||||
}
|
||||
}
|
||||
|
||||
WtLogger::WtLogger(Severity minSeverity)
|
||||
: _minSeverity{ minSeverity }
|
||||
{
|
||||
}
|
||||
|
||||
std::string WtLogger::computeLogConfig(Severity minSeverity)
|
||||
{
|
||||
switch (minSeverity)
|
||||
{
|
||||
case Severity::DEBUG: return "*";
|
||||
case Severity::INFO: return "* -debug";
|
||||
case Severity::WARNING: return "* -debug -info";
|
||||
case Severity::ERROR: return "* -debug -info -warning";
|
||||
case Severity::FATAL: return "* -debug -info -warning -error";
|
||||
}
|
||||
|
||||
throw LmsException{ "Unhandled severity" };
|
||||
}
|
||||
|
||||
bool WtLogger::isSeverityActive(Severity severity) const
|
||||
{
|
||||
return static_cast<int>(severity) <= static_cast<int>(_minSeverity);
|
||||
}
|
||||
|
||||
void WtLogger::processLog(const Log& log)
|
||||
{
|
||||
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << to_string(std::this_thread::get_id()) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Client.hpp"
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::core::http
|
||||
{
|
||||
std::unique_ptr<IClient> createClient(boost::asio::io_context& ioContext, std::string_view baseUrl)
|
||||
{
|
||||
return std::make_unique<Client>(ioContext, baseUrl);
|
||||
}
|
||||
|
||||
void Client::sendGETRequest(ClientGETRequestParameters&& GETParams)
|
||||
{
|
||||
_sendQueue.sendRequest(std::make_unique<ClientRequest>(std::move(GETParams)));
|
||||
}
|
||||
|
||||
void Client::sendPOSTRequest(ClientPOSTRequestParameters&& POSTParams)
|
||||
{
|
||||
_sendQueue.sendRequest(std::make_unique<ClientRequest>(std::move(POSTParams)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <shared_mutex>
|
||||
|
||||
#include "core/http/IClient.hpp"
|
||||
#include "SendQueue.hpp"
|
||||
|
||||
namespace lms::core::http
|
||||
{
|
||||
class Client final : public IClient
|
||||
{
|
||||
public:
|
||||
Client(boost::asio::io_context& ioContext, std::string_view baseUrl)
|
||||
: _sendQueue{ ioContext, baseUrl }
|
||||
{}
|
||||
|
||||
private:
|
||||
void sendGETRequest(ClientGETRequestParameters&& request) override;
|
||||
void sendPOSTRequest(ClientPOSTRequestParameters&& request) override;
|
||||
|
||||
SendQueue _sendQueue;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include "core/http/ClientRequestParameters.hpp"
|
||||
|
||||
namespace lms::core::http
|
||||
{
|
||||
class ClientRequest
|
||||
{
|
||||
public:
|
||||
ClientRequest(ClientGETRequestParameters&& GETParams) : _parameters{ std::move(GETParams) } {}
|
||||
ClientRequest(ClientPOSTRequestParameters&& POSTParams) : _parameters{ std::move(POSTParams) } {}
|
||||
|
||||
std::size_t retryCount{};
|
||||
|
||||
const ClientRequestParameters& getParameters() const
|
||||
{
|
||||
const ClientRequestParameters* res;
|
||||
|
||||
std::visit([&](const auto& parameters)
|
||||
{
|
||||
res = &static_cast<const ClientRequestParameters&>(parameters);
|
||||
}, _parameters);
|
||||
|
||||
return *res;
|
||||
}
|
||||
|
||||
|
||||
enum class Type
|
||||
{
|
||||
GET,
|
||||
POST
|
||||
};
|
||||
Type getType() const
|
||||
{
|
||||
if (std::holds_alternative<ClientGETRequestParameters>(_parameters))
|
||||
return Type::GET;
|
||||
else
|
||||
return Type::POST;
|
||||
}
|
||||
|
||||
const ClientGETRequestParameters& getGETParameters() const
|
||||
{
|
||||
return std::get<ClientGETRequestParameters>(_parameters);
|
||||
}
|
||||
|
||||
const ClientPOSTRequestParameters& getPOSTParameters() const
|
||||
{
|
||||
return std::get<ClientPOSTRequestParameters>(_parameters);
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<ClientGETRequestParameters, ClientPOSTRequestParameters> _parameters;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "SendQueue.hpp"
|
||||
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[Http SendQueue] - " << message)
|
||||
|
||||
namespace lms::core::stringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<std::chrono::seconds> readAs(std::string_view str)
|
||||
{
|
||||
std::optional<std::chrono::seconds> res;
|
||||
|
||||
if (const std::optional<std::size_t> value{ readAs<std::size_t>(str) })
|
||||
res = std::chrono::seconds{ *value };
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
namespace lms::core::http
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
std::optional<T> headerReadAs(const Wt::Http::Message& msg, std::string_view headerName)
|
||||
{
|
||||
std::optional<T> res;
|
||||
|
||||
if (const std::string * headerValue{ msg.getHeader(std::string {headerName}) })
|
||||
res = stringUtils::readAs<T>(*headerValue);
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
SendQueue::SendQueue(boost::asio::io_context& ioContext, std::string_view baseUrl)
|
||||
: _ioContext{ ioContext }
|
||||
, _baseUrl{ baseUrl }
|
||||
{
|
||||
_client.done().connect([this](Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg)
|
||||
{
|
||||
_strand.dispatch([this, ec, msg = std::move(msg)]
|
||||
{
|
||||
onClientDone(ec, msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
SendQueue::~SendQueue()
|
||||
{
|
||||
_client.abort();
|
||||
}
|
||||
|
||||
void SendQueue::sendRequest(std::unique_ptr<ClientRequest> request)
|
||||
{
|
||||
boost::asio::dispatch(_strand, [this, request = std::move(request)]() mutable
|
||||
{
|
||||
_sendQueue[request->getParameters().priority].emplace_back(std::move(request));
|
||||
|
||||
if (_state == State::Idle)
|
||||
sendNextQueuedRequest();
|
||||
});
|
||||
}
|
||||
|
||||
void SendQueue::sendNextQueuedRequest()
|
||||
{
|
||||
assert(_state == State::Idle);
|
||||
assert(!_currentRequest);
|
||||
|
||||
for (auto& [prio, requests] : _sendQueue)
|
||||
{
|
||||
LOG(DEBUG, "Processing prio " << static_cast<int>(prio) << ", request count = " << requests.size());
|
||||
while (!requests.empty())
|
||||
{
|
||||
std::unique_ptr<ClientRequest> request{ std::move(requests.front()) };
|
||||
requests.pop_front();
|
||||
|
||||
if (!sendRequest(*request))
|
||||
continue;
|
||||
|
||||
_state = State::Sending;
|
||||
_currentRequest = std::move(request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SendQueue::sendRequest(const ClientRequest& request)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("SendQueue", "SendRequest");
|
||||
|
||||
std::string url{ _baseUrl + request.getParameters().relativeUrl };
|
||||
LOG(DEBUG, "Sending request to url '" << url << "'");
|
||||
|
||||
bool res{};
|
||||
switch (request.getType())
|
||||
{
|
||||
case ClientRequest::Type::GET:
|
||||
res = _client.get(url, request.getGETParameters().headers);
|
||||
break;
|
||||
|
||||
case ClientRequest::Type::POST:
|
||||
res = _client.post(url, request.getPOSTParameters().message);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!res)
|
||||
LOG(ERROR, "Send failed, bad url or unsupported scheme?");
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void SendQueue::onClientDone(Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("SendQueue", "OnClientDone");
|
||||
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
{
|
||||
LOG(DEBUG, "Client aborted");
|
||||
return;
|
||||
}
|
||||
|
||||
assert(_currentRequest);
|
||||
_state = State::Idle;
|
||||
|
||||
LOG(DEBUG, "Client done. status = " << msg.status());
|
||||
if (ec)
|
||||
onClientDoneError(std::move(_currentRequest), ec);
|
||||
else
|
||||
onClientDoneSuccess(std::move(_currentRequest), msg);
|
||||
}
|
||||
|
||||
void SendQueue::onClientDoneError(std::unique_ptr<ClientRequest> request, Wt::AsioWrapper::error_code ec)
|
||||
{
|
||||
LOG(ERROR, "Retry " << request->retryCount << ", client error: '" << ec.message() << "'");
|
||||
|
||||
// may be a network error, try again later
|
||||
throttle(_defaultRetryWaitDuration);
|
||||
|
||||
if (request->retryCount++ < _maxRetryCount)
|
||||
{
|
||||
_sendQueue[request->getParameters().priority].emplace_front(std::move(request));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(ERROR, "Too many retries, giving up operation and throttle");
|
||||
if (request->getParameters().onFailureFunc)
|
||||
request->getParameters().onFailureFunc();
|
||||
}
|
||||
}
|
||||
|
||||
void SendQueue::onClientDoneSuccess(std::unique_ptr<ClientRequest> request, const Wt::Http::Message& msg)
|
||||
{
|
||||
const ClientRequestParameters& requestParameters{ request->getParameters() };
|
||||
bool mustThrottle{};
|
||||
if (msg.status() == 429)
|
||||
{
|
||||
_sendQueue[requestParameters.priority].emplace_front(std::move(request));
|
||||
mustThrottle = true;
|
||||
}
|
||||
|
||||
const auto remainingCount{ headerReadAs<std::size_t>(msg, "X-RateLimit-Remaining") };
|
||||
LOG(DEBUG, "Remaining messages = " << (remainingCount ? *remainingCount : 0));
|
||||
if (mustThrottle || (remainingCount && *remainingCount == 0))
|
||||
{
|
||||
const auto waitDuration{ headerReadAs<std::chrono::seconds>(msg, "X-RateLimit-Reset-In") };
|
||||
throttle(waitDuration.value_or(_defaultRetryWaitDuration));
|
||||
}
|
||||
|
||||
if (!mustThrottle)
|
||||
{
|
||||
if (msg.status() == 200)
|
||||
{
|
||||
if (requestParameters.onSuccessFunc)
|
||||
requestParameters.onSuccessFunc(msg.body());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(ERROR, "Send error: '" << msg.body() << "'");
|
||||
if (requestParameters.onFailureFunc)
|
||||
requestParameters.onFailureFunc();
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == State::Idle)
|
||||
sendNextQueuedRequest();
|
||||
}
|
||||
|
||||
void SendQueue::throttle(std::chrono::seconds requestedDuration)
|
||||
{
|
||||
assert(_state == State::Idle);
|
||||
|
||||
const std::chrono::seconds duration{ clamp(requestedDuration, _minRetryWaitDuration, _maxRetryWaitDuration) };
|
||||
LOG(DEBUG, "Throttling for " << duration.count() << " seconds");
|
||||
|
||||
_throttleTimer.expires_after(duration);
|
||||
_throttleTimer.async_wait([this](const boost::system::error_code& ec)
|
||||
{
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
{
|
||||
LOG(DEBUG, "Throttle aborted");
|
||||
return;
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
throw LmsException{ "Throttle timer failure: " + std::string {ec.message()} };
|
||||
}
|
||||
|
||||
_state = State::Idle;
|
||||
sendNextQueuedRequest();
|
||||
});
|
||||
_state = State::Throttled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/io_context_strand.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <Wt/Http/Client.h>
|
||||
#include "ClientRequest.hpp"
|
||||
|
||||
namespace lms::core::http
|
||||
{
|
||||
class SendQueue
|
||||
{
|
||||
public:
|
||||
SendQueue(boost::asio::io_context& ioContext, std::string_view baseUrl);
|
||||
~SendQueue();
|
||||
|
||||
SendQueue(const SendQueue&) = delete;
|
||||
SendQueue(const SendQueue&&) = delete;
|
||||
SendQueue& operator=(const SendQueue&) = delete;
|
||||
SendQueue& operator=(const SendQueue&&) = delete;
|
||||
|
||||
void sendRequest(std::unique_ptr<ClientRequest> request);
|
||||
|
||||
private:
|
||||
void sendNextQueuedRequest();
|
||||
bool sendRequest(const ClientRequest& request);
|
||||
void onClientDone(Wt::AsioWrapper::error_code ec, const Wt::Http::Message& msg);
|
||||
void onClientDoneError(std::unique_ptr<ClientRequest> request, Wt::AsioWrapper::error_code ec);
|
||||
void onClientDoneSuccess(std::unique_ptr<ClientRequest> request, const Wt::Http::Message& msg);
|
||||
void throttle(std::chrono::seconds duration);
|
||||
|
||||
const std::size_t _maxRetryCount{ 2 };
|
||||
const std::chrono::seconds _defaultRetryWaitDuration{ 30 };
|
||||
const std::chrono::seconds _minRetryWaitDuration{ 1 };
|
||||
const std::chrono::seconds _maxRetryWaitDuration{ 300 };
|
||||
|
||||
boost::asio::io_context& _ioContext;
|
||||
boost::asio::io_context::strand _strand{ _ioContext };
|
||||
boost::asio::steady_timer _throttleTimer{ _ioContext };
|
||||
std::string _baseUrl;
|
||||
|
||||
enum class State
|
||||
{
|
||||
Idle,
|
||||
Throttled,
|
||||
Sending,
|
||||
};
|
||||
State _state{ State::Idle };
|
||||
Wt::Http::Client _client{ _ioContext };
|
||||
std::map<ClientRequestParameters::Priority, std::deque<std::unique_ptr<ClientRequest>>> _sendQueue;
|
||||
std::unique_ptr<ClientRequest> _currentRequest;
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user