#include <array>
#include <chrono>
#include <csignal>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <utility>
#include <Magick++.h>
#include <spdlog/spdlog.h>
#include <uni_algo/version.h>
#include "app.h"
#include "clock.h"
#include "startup.h"
namespace
{
/// Signal-safe flag requesting shutdown from the main loop.
volatile std::sig_atomic_t STOP_REQUESTED = 0;
void handleSignal([[maybe_unused]] int signal)
{
STOP_REQUESTED = 1;
}
mw::E<void> prepareImageMagick()
{
const std::array<std::string, 4> coders = {
"JPEG", "PNG", "WEBP", "AVIF"};
try
{
for(const std::string& name : coders)
{
const Magick::CoderInfo coder(name);
if(!coder.isReadable())
{
return std::unexpected(mw::runtimeError(
"ImageMagick coder " + name + " is not readable"));
}
if(name == "AVIF" && !coder.isWritable())
{
return std::unexpected(mw::runtimeError(
"ImageMagick coder AVIF is not writable"));
}
}
}
catch(const Magick::Exception& error)
{
return std::unexpected(mw::runtimeError(
"Failed to inspect ImageMagick capabilities: " +
std::string(error.what())));
}
Magick::ResourceLimits::listLength(2);
Magick::ResourceLimits::width(2048);
Magick::ResourceLimits::height(2048);
return {};
}
void printUsage(std::ostream& output, const char* program)
{
output << "Usage: " << program << " <config.toml>\n";
}
} // namespace
/// Start Card Collection from one validated configuration file.
int main(int argc, char** argv)
{
const char* program = argc > 0 ? argv[0] : "card_collection";
if(argc == 2 && std::string(argv[1]) == "--help")
{
printUsage(std::cout, program);
return 0;
}
if(argc != 2)
{
printUsage(std::cerr, program);
return 2;
}
auto config = loadConfig(argv[1]);
if(!config)
{
std::cerr << "Failed to load configuration: "
<< config.error().msg() << '\n';
return 1;
}
Magick::InitializeMagick(argc > 0 ? argv[0] : nullptr);
auto image_magick = prepareImageMagick();
if(!image_magick)
{
spdlog::error(
"Failed to initialize image processing: {}",
image_magick.error().msg());
return 1;
}
spdlog::info(
"Username normalization uses uni-algo {}.{}.{} and Unicode {}.{}.{}",
una::version::library.major(),
una::version::library.minor(),
una::version::library.patch(),
una::version::unicode.major(),
una::version::unicode.minor(),
una::version::unicode.update());
const auto startup_time = std::chrono::system_clock::now();
const std::int64_t startup_seconds =
std::chrono::duration_cast<std::chrono::seconds>(
startup_time.time_since_epoch()).count();
auto data_source = prepareDataSource(
config->database_path,
config->administrator_email,
config->administrator_email_key,
startup_seconds,
utcDay(startup_time));
if(!data_source)
{
spdlog::error(
"Failed to prepare the card database: {}",
data_source.error().msg());
return 1;
}
AssetStore assets(config->card_storage_root);
auto reconciled = assets.reconcile(**data_source);
if(!reconciled)
{
spdlog::error(
"Failed to reconcile card storage: {}",
reconciled.error().msg());
return 1;
}
try
{
App app(
*config,
std::move(*data_source),
std::make_unique<NonSecretRandom>());
std::signal(SIGINT, handleSignal);
std::signal(SIGTERM, handleSignal);
auto start_result = app.start();
if(!start_result)
{
spdlog::error(
"Failed to start the server: {}",
start_result.error().msg());
return 1;
}
spdlog::info("Server listening at {}", config->base_url.str());
while(STOP_REQUESTED == 0)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
app.stop();
app.wait();
}
catch(const std::exception& error)
{
spdlog::error(
"Failed to construct application services: {}", error.what());
return 1;
}
return 0;
}