@@ -0,0 +1,321 @@
|
||||
#include <vamp_zero/app.hpp>
|
||||
|
||||
#include <csignal>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/use_future.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/serialize.hpp>
|
||||
|
||||
#include <ligo.hpp>
|
||||
#include <modulus.hpp>
|
||||
|
||||
#include <vamp_zero/config.hpp>
|
||||
#include <vamp_zero/database.hpp>
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/library.hpp>
|
||||
#include <vamp_zero/page_control.hpp>
|
||||
#include <vamp_zero/portal.hpp>
|
||||
#include <vamp_zero/ui.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
auto invalid(std::string message) -> modulus::error {
|
||||
return vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::move(message));
|
||||
}
|
||||
|
||||
auto usage() -> std::string {
|
||||
return "usage: vamp-zero [--config PATH] [--check-config] [--health-check] [--version] [--once] [--migrate]";
|
||||
}
|
||||
|
||||
auto bind_port(vamp_zero::config const& config) -> std::uint16_t {
|
||||
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
||||
if (!endpoint) {
|
||||
return 8080;
|
||||
}
|
||||
return endpoint->port == 0 ? 8080 : endpoint->port;
|
||||
}
|
||||
|
||||
auto write_text_atomic(std::filesystem::path const& destination, std::string const& text) -> std::expected<void, modulus::error> {
|
||||
auto error = std::error_code{};
|
||||
if (!destination.parent_path().empty()) {
|
||||
std::filesystem::create_directories(destination.parent_path(), error);
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to create runtime directory: " + error.message()));
|
||||
}
|
||||
}
|
||||
auto temporary = destination;
|
||||
temporary += ".tmp";
|
||||
auto output = std::ofstream{temporary, std::ios::binary | std::ios::trunc};
|
||||
if (!output) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to create heartbeat: " + temporary.string()));
|
||||
}
|
||||
output << text;
|
||||
output.close();
|
||||
if (!output) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to write heartbeat: " + temporary.string()));
|
||||
}
|
||||
std::filesystem::rename(temporary, destination, error);
|
||||
if (error) {
|
||||
std::filesystem::remove(temporary, error);
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to publish heartbeat: " + error.message()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto write_heartbeat(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto heartbeat = boost::json::object{};
|
||||
heartbeat["version"] = VAMP_VERSION;
|
||||
heartbeat["git_sha"] = VAMP_GIT_SHA;
|
||||
heartbeat["status"] = "ok";
|
||||
heartbeat["endpoint"] = "http://" + config.control.zero_ip + ":" + std::to_string(bind_port(config));
|
||||
return write_text_atomic(config.runtime.heartbeat_path, boost::json::serialize(heartbeat));
|
||||
}
|
||||
|
||||
auto migrate(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_migrate_database(io.get_executor(), config.database), boost::asio::use_future);
|
||||
io.run();
|
||||
return future.get();
|
||||
}
|
||||
|
||||
auto make_server_endpoint(vamp_zero::config const& config) -> std::expected<ligo::http::endpoint, modulus::error> {
|
||||
auto endpoint = ligo::http::server_endpoint_from_config(config.control.endpoint);
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
}
|
||||
if (endpoint->port == 0) {
|
||||
endpoint->port = 8080;
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
auto import_library(vamp_zero::config const& config) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_import_catalogs(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
|
||||
io.run();
|
||||
return future.get();
|
||||
}
|
||||
|
||||
auto load_initial_state(vamp_zero::config const& config) -> std::expected<vamp_zero::app_state, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto performance_future = boost::asio::co_spawn(io, vamp_zero::async_load_performance_library(io.get_executor(), config.database), boost::asio::use_future);
|
||||
auto practice_future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config.database), boost::asio::use_future);
|
||||
io.run();
|
||||
auto performance = performance_future.get();
|
||||
if (!performance) {
|
||||
return std::unexpected(performance.error());
|
||||
}
|
||||
auto practice = practice_future.get();
|
||||
if (!practice) {
|
||||
return std::unexpected(practice.error());
|
||||
}
|
||||
auto state = vamp_zero::app_state{
|
||||
.performance = std::move(*performance),
|
||||
.practice = std::move(*practice),
|
||||
};
|
||||
if (auto valid = vamp_zero::validate_app_state(state); !valid) {
|
||||
return std::unexpected(valid.error());
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
auto run_headless(vamp_zero::config const& config, vamp_zero::control_state& state) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto endpoint = make_server_endpoint(config);
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
}
|
||||
auto server = ligo::http::server{io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
}
|
||||
auto heartbeat = write_heartbeat(config);
|
||||
if (!heartbeat) {
|
||||
server.stop();
|
||||
return std::unexpected(heartbeat.error());
|
||||
}
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
signals.async_wait([&](auto const&, int) {
|
||||
server.stop();
|
||||
io.stop();
|
||||
});
|
||||
std::cout << "vamp-zero listening on " << server.selected_port() << '\n';
|
||||
io.run();
|
||||
return {};
|
||||
}
|
||||
|
||||
auto run_display(vamp_zero::config const& config, vamp_zero::control_state& state) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto endpoint = make_server_endpoint(config);
|
||||
if (!endpoint) {
|
||||
return std::unexpected(endpoint.error());
|
||||
}
|
||||
auto ui = vamp_zero::zero_ui{io.get_executor(), config.display};
|
||||
state.set_observer([&ui, &config](vamp_zero::app_state app) {
|
||||
auto status = ui.publish(vamp_zero::make_ui_state(config, std::move(app)));
|
||||
if (!status) {
|
||||
modulus::log_error("failed to publish UI state: " + status.error().describe());
|
||||
}
|
||||
});
|
||||
auto published = ui.publish(vamp_zero::make_ui_state(config, state.snapshot()));
|
||||
if (!published) {
|
||||
return std::unexpected(published.error());
|
||||
}
|
||||
auto server = ligo::http::server{io, *endpoint, vamp_zero::make_router(state), ligo::http::server_options_from_config(config.control.endpoint)};
|
||||
auto started = server.start();
|
||||
if (!started) {
|
||||
return std::unexpected(started.error());
|
||||
}
|
||||
auto heartbeat = write_heartbeat(config);
|
||||
if (!heartbeat) {
|
||||
server.stop();
|
||||
return std::unexpected(heartbeat.error());
|
||||
}
|
||||
auto connection_timer = boost::asio::steady_timer{io};
|
||||
auto schedule_connection_refresh = std::function<void()>{};
|
||||
schedule_connection_refresh = [&] {
|
||||
connection_timer.expires_after(std::chrono::milliseconds{500});
|
||||
connection_timer.async_wait([&](auto const& error) {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
static_cast<void>(state.refresh_connection());
|
||||
schedule_connection_refresh();
|
||||
});
|
||||
};
|
||||
schedule_connection_refresh();
|
||||
auto signals = boost::asio::signal_set{io, SIGINT, SIGTERM};
|
||||
signals.async_wait([&](auto const&, int) {
|
||||
connection_timer.cancel();
|
||||
server.stop();
|
||||
ui.stop();
|
||||
io.stop();
|
||||
});
|
||||
auto io_thread = std::thread{[&io] {
|
||||
io.run();
|
||||
}};
|
||||
auto ui_status = ui.run();
|
||||
server.stop();
|
||||
connection_timer.cancel();
|
||||
io.stop();
|
||||
if (io_thread.joinable()) {
|
||||
io_thread.join();
|
||||
}
|
||||
return ui_status;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto vamp_zero::parse_command_line(std::int32_t argc, std::char_t** argv) -> std::expected<vamp_zero::command_line, modulus::error> {
|
||||
if (argc < 1 || argv == nullptr) {
|
||||
return std::unexpected(invalid("process arguments are invalid"));
|
||||
}
|
||||
auto result = vamp_zero::command_line{};
|
||||
auto require_value = [&](std::int32_t& index, std::string_view option) -> std::expected<std::string_view, modulus::error> {
|
||||
++index;
|
||||
if (index >= argc) {
|
||||
return std::unexpected(invalid(std::string{option} + " requires a value"));
|
||||
}
|
||||
auto value = std::string_view{argv[index]};
|
||||
if (value.empty()) {
|
||||
return std::unexpected(invalid(std::string{option} + " value must not be empty"));
|
||||
}
|
||||
return value;
|
||||
};
|
||||
for (auto index = std::int32_t{1}; index < argc; ++index) {
|
||||
auto argument = std::string_view{argv[index]};
|
||||
if (argument == "--config") {
|
||||
auto value = require_value(index, "--config");
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
result.config_path = *value;
|
||||
} else if (argument == "--check-config") {
|
||||
result.check_config = true;
|
||||
} else if (argument == "--health-check") {
|
||||
result.health_check = true;
|
||||
} else if (argument == "--version") {
|
||||
result.print_version = true;
|
||||
} else if (argument == "--once") {
|
||||
result.exit_after_startup = true;
|
||||
} else if (argument == "--migrate") {
|
||||
result.migrate = true;
|
||||
} else if (argument == "--help" || argument == "-h") {
|
||||
return std::unexpected(invalid(usage()));
|
||||
} else {
|
||||
return std::unexpected(invalid("unknown argument: " + std::string{argument}));
|
||||
}
|
||||
}
|
||||
auto command_count = static_cast<std::uint32_t>(result.check_config ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.health_check ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.print_version ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.exit_after_startup ? 1 : 0)
|
||||
+ static_cast<std::uint32_t>(result.migrate ? 1 : 0);
|
||||
if (command_count > 1U) {
|
||||
return std::unexpected(invalid("only one command flag may be selected"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto vamp_zero::run(vamp_zero::command_line command) -> std::expected<void, modulus::error> {
|
||||
if (command.print_version) {
|
||||
std::cout << VAMP_VERSION << " " << VAMP_GIT_SHA << '\n';
|
||||
return {};
|
||||
}
|
||||
auto config = vamp_zero::load_config(command.config_path);
|
||||
if (!config) {
|
||||
return std::unexpected(config.error());
|
||||
}
|
||||
modulus::configure(config->modulus);
|
||||
if (command.check_config) {
|
||||
std::cout << "ok\n";
|
||||
return {};
|
||||
}
|
||||
if (command.health_check) {
|
||||
std::cout << "ok\n";
|
||||
return {};
|
||||
}
|
||||
if (command.migrate) {
|
||||
return migrate(*config);
|
||||
}
|
||||
if (command.exit_after_startup || config->runtime.exit_after_startup) {
|
||||
std::cout << "ok\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
auto migrated = migrate(*config);
|
||||
if (!migrated) {
|
||||
return migrated;
|
||||
}
|
||||
auto imported = import_library(*config);
|
||||
if (!imported) {
|
||||
return imported;
|
||||
}
|
||||
auto initial = load_initial_state(*config);
|
||||
if (!initial) {
|
||||
return std::unexpected(initial.error());
|
||||
}
|
||||
auto state = vamp_zero::control_state{*config, std::move(*initial)};
|
||||
if (config->runtime.headless) {
|
||||
return run_headless(*config, state);
|
||||
}
|
||||
return run_display(*config, state);
|
||||
}
|
||||
Reference in New Issue
Block a user