Refactored namespaces

This commit is contained in:
emeric
2024-03-12 08:32:08 +01:00
parent 487960b413
commit 4b7c4295ec
501 changed files with 12605 additions and 12631 deletions
+298
View File
@@ -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;
}
}
+80
View File
@@ -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{};
};
}
+209
View File
@@ -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;
}
}
+59
View File
@@ -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;
};
}
+130
View File
@@ -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;
}
}
}
+51
View File
@@ -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;
};
}
+137
View File
@@ -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{};
};
}
+85
View File
@@ -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();
}
}
+81
View File
@@ -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();
}
}
+46
View File
@@ -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
+186
View File
@@ -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;
}
}
+38
View File
@@ -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 };
}
}
+134
View File
@@ -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
}
+38
View File
@@ -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;
}
}
+439
View File
@@ -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();
}
}
+194
View File
@@ -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);
}
}
+73
View File
@@ -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;
};
}
+91
View File
@@ -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();
}
}
+69
View File
@@ -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();
}
}
+39
View File
@@ -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)));
}
}
+44
View File
@@ -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;
};
}
+75
View File
@@ -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;
};
}
+240
View File
@@ -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;
}
}
+78
View File
@@ -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;
};
}