// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #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 d'écoute (défaut 127.0.0.1 : poste local uniquement)\n" " --port port WebSocket (défaut 9100)\n" " --token jeton exigé dans l'URL de connexion (?token=...)\n" " --allowed-origin 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 délai de grâce après perte du contrôleur (défaut 3000)\n" " --seed graine de la session de démarrage (défaut 0)\n" " --render lance et supervise le processus de rendu (sequa-render)\n" " --render-headless rendu sans fenêtre (adaptateur debug)\n" " --render-fps cadence imposée au rendu (debug : 10 conseillé)\n" " --render-options réglages propres à l'adaptateur\n" " --render-log journal du rendu (défaut : sortie du Core)\n" " --sequa-root 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 std::optional 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 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(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(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(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(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(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(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; }