- platform/layout : racine sequa-root.json, chemins des composants, arguments et chemins en UTF-8.
- Règles install() : bin/windows-x64/{debug,release,asan}/ (core, render, studio + ui, engines).
- Le Core trouve sequa-render et les adaptateurs depuis la racine (--sequa-root, passé par
Studio host à sa copie fantôme) : plus de render-unavailable depuis le panneau Rendu.
- L'adaptateur Godot cherche Godot et son projet à côté de lui (surcharge par --adapter-options) ;
template_release pour le type release. Godot hébergé dans les presets debug et release.
- scripts/build-all.ps1 : Godot, C++, interface, arrêt des processus du dossier visé, install ;
check.ps1 l'appelle (modification de l'ADR 0004).
- docs/installation/ : compiler, lancer, installer et désinstaller par plateforme.
237 lines
9.5 KiB
C++
237 lines
9.5 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
//
|
|
// sequa-core : processus du Core. Serveur WebSocket du protocole Studio - Core, session en
|
|
// direct, et, avec --render, processus de rendu lancé et supervisé (ADR 0009).
|
|
|
|
#include "sequa/core/server/protocol.hpp"
|
|
#include "sequa/core/server/runtime.hpp"
|
|
#include "sequa/core/server/server.hpp"
|
|
#include "sequa/core/version.hpp"
|
|
|
|
#include "sequa/platform/layout.hpp"
|
|
|
|
#include <charconv>
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#define NOMINMAX
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
volatile std::sig_atomic_t stop_requested = 0;
|
|
|
|
extern "C" void on_signal(int /*signal*/)
|
|
{
|
|
stop_requested = 1;
|
|
}
|
|
|
|
// Codes de sortie (spec 0.2 §6).
|
|
constexpr int exit_ok = 0;
|
|
constexpr int exit_start_failed = 1;
|
|
constexpr int exit_bad_arguments = 2;
|
|
constexpr int exit_lease_expired = 3;
|
|
|
|
void print_usage()
|
|
{
|
|
std::printf("Usage : sequa-core [options]\n"
|
|
" --host <adresse> adresse d'écoute (défaut 127.0.0.1 : poste local uniquement)\n"
|
|
" --port <n> port WebSocket (défaut 9100)\n"
|
|
" --token <hex> jeton exigé dans l'URL de connexion (?token=...)\n"
|
|
" --allowed-origin <o> origine de navigateur autorisée (répétable ;\n"
|
|
" défaut : http://localhost:* et http://127.0.0.1:*)\n"
|
|
" --linked s'arrête sans contrôleur (Studio host), voir la spec §5\n"
|
|
" --lease-grace-ms <n> délai de grâce après perte du contrôleur (défaut 3000)\n"
|
|
" --seed <hex> graine de la session de démarrage (défaut 0)\n"
|
|
" --render <godot|debug> lance et supervise le processus de rendu (sequa-render)\n"
|
|
" --render-headless rendu sans fenêtre (adaptateur debug)\n"
|
|
" --render-fps <n> cadence imposée au rendu (debug : 10 conseillé)\n"
|
|
" --render-options <json> réglages propres à l'adaptateur\n"
|
|
" --render-log <fichier> journal du rendu (défaut : sortie du Core)\n"
|
|
" --sequa-root <dossier> racine de l'arborescence (défaut : trouvée depuis l'exécutable)\n"
|
|
" --help\n"
|
|
"Codes de sortie : 0 arrêt normal, 1 démarrage impossible, 2 arguments invalides, 3 bail expiré.\n");
|
|
}
|
|
|
|
template <typename T>
|
|
std::optional<T> parse_number(std::string_view text, int base = 10)
|
|
{
|
|
T value{};
|
|
const auto [next, ec] = std::from_chars(text.data(), text.data() + text.size(), value, base);
|
|
if (ec != std::errc{} || next != text.data() + text.size() || text.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
using namespace sequa::core;
|
|
|
|
#ifdef _WIN32
|
|
SetConsoleOutputCP(CP_UTF8); // journaux en UTF-8 (accents, JSON reçu)
|
|
#endif
|
|
|
|
// Arguments en UTF-8 (Windows : CommandLineToArgvW ; POSIX : argv tel quel).
|
|
const std::vector<std::string> args = sequa::platform::utf8_arguments(argc, argv);
|
|
|
|
server::ServerOptions options;
|
|
server::CoreRuntimeOptions runtime_options;
|
|
std::string render_adapter; // vide : pas de rendu
|
|
server::RenderStartRequest render_request; // paramètres de render/start
|
|
runtime_options.core_version = std::string(version_string());
|
|
|
|
for (std::size_t i = 0; i < args.size(); ++i) {
|
|
const std::string_view arg = args[i];
|
|
const bool has_value = i + 1 < args.size();
|
|
const auto invalid = [&](const char* what) {
|
|
std::fprintf(stderr, "%s invalide : %s\n", what, arg.data());
|
|
return exit_bad_arguments;
|
|
};
|
|
if (arg == "--help" || arg == "-h") {
|
|
print_usage();
|
|
return exit_ok;
|
|
}
|
|
if (arg == "--linked") {
|
|
runtime_options.linked = true;
|
|
} else if (arg == "--host" && has_value) {
|
|
options.host = args[++i];
|
|
} else if (arg == "--port" && has_value) {
|
|
const auto port = parse_number<int>(args[++i]);
|
|
if (!port || *port <= 0 || *port > 65535) {
|
|
return invalid("Port");
|
|
}
|
|
options.port = *port;
|
|
} else if (arg == "--token" && has_value) {
|
|
options.token = args[++i];
|
|
if (options.token.empty()) {
|
|
return invalid("Jeton");
|
|
}
|
|
} else if (arg == "--allowed-origin" && has_value) {
|
|
options.allowed_origins.emplace_back(args[++i]);
|
|
} else if (arg == "--lease-grace-ms" && has_value) {
|
|
const auto grace = parse_number<long long>(args[++i]);
|
|
if (!grace || *grace < 0) {
|
|
return invalid("Délai de grâce");
|
|
}
|
|
runtime_options.lease_grace = std::chrono::milliseconds(*grace);
|
|
} else if (arg == "--seed" && has_value) {
|
|
const std::string_view text = args[++i];
|
|
const auto seed = text.size() <= 16 ? parse_number<std::uint64_t>(text, 16) : std::nullopt;
|
|
if (!seed) {
|
|
return invalid("Graine");
|
|
}
|
|
runtime_options.seed = *seed;
|
|
} else if (arg == "--render" && has_value) {
|
|
render_adapter = args[++i];
|
|
if (render_adapter != "godot" && render_adapter != "debug") {
|
|
return invalid("Adaptateur de rendu");
|
|
}
|
|
} else if (arg == "--render-headless") {
|
|
render_request.headless = true;
|
|
} else if (arg == "--render-fps" && has_value) {
|
|
const auto fps = parse_number<std::uint32_t>(args[++i]);
|
|
if (!fps || *fps > 240) {
|
|
return invalid("Cadence de rendu");
|
|
}
|
|
render_request.fps = *fps;
|
|
} else if (arg == "--render-options" && has_value) {
|
|
render_request.adapter_options = args[++i];
|
|
} else if (arg == "--render-log" && has_value) {
|
|
runtime_options.render_log = sequa::platform::path_from_utf8(args[++i]);
|
|
} else if (arg == "--sequa-root" && has_value) {
|
|
runtime_options.sequa_root = sequa::platform::path_from_utf8(args[++i]);
|
|
} else {
|
|
std::fprintf(stderr, "Argument inconnu : %s\n", arg.data());
|
|
print_usage();
|
|
return exit_bad_arguments;
|
|
}
|
|
}
|
|
|
|
std::mutex log_mutex;
|
|
options.log = [&log_mutex](std::string_view line) {
|
|
const std::lock_guard lock(log_mutex);
|
|
std::printf("%.*s\n", static_cast<int>(line.size()), line.data());
|
|
std::fflush(stdout);
|
|
};
|
|
|
|
// Remplir les options du runtime AVANT de construire le CoreRuntime.
|
|
if (runtime_options.sequa_root.empty()) {
|
|
const auto exe = sequa::platform::current_executable();
|
|
if (!exe.empty()) {
|
|
runtime_options.sequa_root = sequa::platform::find_root(exe.parent_path()).value_or(std::filesystem::path{});
|
|
}
|
|
}
|
|
const std::string host = options.host == "0.0.0.0" ? std::string("127.0.0.1") : options.host;
|
|
std::string url = "ws://" + host + ":" + std::to_string(options.port) + "/";
|
|
if (!options.token.empty()) {
|
|
url += "?token=" + options.token;
|
|
}
|
|
runtime_options.render_core_url = std::move(url);
|
|
runtime_options.log = options.log;
|
|
|
|
server::CoreRuntime runtime(std::move(runtime_options));
|
|
const server::CoreStatus status = runtime.core_status();
|
|
std::printf("SEQUA Core %s - protocole Studio-Core %s (JSON provisoire) - instance %s, pid %lld%s\n",
|
|
status.core_version.c_str(), status.protocol_version.c_str(), status.instance_id.c_str(),
|
|
static_cast<long long>(status.pid), runtime_options.linked ? " - lié (bail)" : "");
|
|
|
|
server::CoreServer core(options, runtime);
|
|
if (const std::string error = core.start(); !error.empty()) {
|
|
std::fprintf(stderr, "Démarrage impossible sur %s:%d : %s\n", options.host.c_str(), options.port,
|
|
error.c_str());
|
|
return exit_start_failed;
|
|
}
|
|
|
|
// Démarrer le rendu si --render a été passé (après core.start() pour que le serveur écoute).
|
|
if (!render_adapter.empty()) {
|
|
render_request.adapter = render_adapter;
|
|
const auto result = runtime.render_start(render_request);
|
|
if (!result.error_code.empty()) {
|
|
std::fprintf(stderr, "Échec du démarrage du rendu (%s) : %s\n",
|
|
result.error_code.c_str(), result.error_message.c_str());
|
|
core.stop();
|
|
return exit_start_failed;
|
|
}
|
|
}
|
|
|
|
std::signal(SIGINT, on_signal);
|
|
std::signal(SIGTERM, on_signal);
|
|
std::printf("Ctrl+C pour arrêter.\n");
|
|
std::fflush(stdout);
|
|
|
|
int exit_code = exit_ok;
|
|
while (stop_requested == 0) {
|
|
const auto reason = runtime.exit_reason();
|
|
if (reason == server::CoreRuntime::ExitReason::shutdown) {
|
|
std::printf("Arrêt demandé par core/shutdown.\n");
|
|
break;
|
|
}
|
|
if (reason == server::CoreRuntime::ExitReason::lease_expired) {
|
|
std::printf("Bail expiré : aucun contrôleur, arrêt.\n");
|
|
exit_code = exit_lease_expired;
|
|
break;
|
|
}
|
|
runtime.tick_render(); // remplace l'ancien render->tick()
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
}
|
|
runtime.stop_render(); // arrête le rendu avant core.stop()
|
|
core.stop();
|
|
return exit_code;
|
|
} |