diff --git a/README.md b/README.md index dbb34d2..9ff86da 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ Options: --ipc-path IPC path (default: /tmp/coretrace_ipc). --serve-host HTTP server host when --ipc=serve. --serve-port HTTP server port when --ipc=serve. + --shutdown-token Token required for POST /shutdown (server mode). + --shutdown-timeout-ms Graceful shutdown timeout in ms (0 = wait indefinitely). --async Enables asynchronous execution. Examples: @@ -79,7 +81,7 @@ Description: Start the HTTP server: ```bash -./ctrace --ipc serve --serve-host 127.0.0.1 --serve-port 8080 +./ctrace --ipc serve --serve-host 127.0.0.1 --serve-port 8080 --shutdown-token mytoken ``` Send a request: @@ -114,28 +116,45 @@ Response notes: - `result.outputs` groups tool output by tool name. - Each output entry has `stream` and `message`. If a tool emits JSON, `message` is returned as a JSON object. +Shutdown the server (HTTP request): + +```bash +curl -i -X POST http://127.0.0.1:8080/shutdown \ + -H "Authorization: Bearer mytoken" +``` + +Alternative header: + +```bash +curl -i -X POST http://127.0.0.1:8080/shutdown \ + -H "X-Admin-Token: mytoken" +``` + +The server responds with `202 Accepted` and stops accepting new requests while allowing in-flight requests to finish +(up to `--shutdown-timeout-ms` if configured). + ### Mangle/Demangle API ```c++ -bool hasMangled = ctrace_tools::isMangled(entry_points); +bool hasMangled = ctrace_tools::mangle::isMangled(entry_points); std::cout << "Is mangled : " << hasMangled << std::endl; if (hasMangled) std::cout << abi::__cxa_demangle(entry_points.c_str(), 0, 0, &status) << std::endl; std::vector params1 = {}; -std::string mangled1 = ctrace_tools::mangleFunction("", "single_compute()", params1); +std::string mangled1 = ctrace_tools::mangle::mangleFunction("", "single_compute()", params1); std::cout << "Mangled single_compute(): " << mangled1 << "\n"; std::cout << abi::__cxa_demangle(mangled1.c_str(), 0, 0, &status) << std::endl; // Example 2 : with namespace std::vector params2 = {"std::string", "int"}; -std::string mangled2 = ctrace_tools::mangleFunction("math", "compute", params2); +std::string mangled2 = ctrace_tools::mangle::mangleFunction("math", "compute", params2); std::cout << "Mangled math::compute(std::string, int): " << mangled2 << "\n"; // Example 3 : without parameters with namespace std::vector params3; -std::string mangled3 = ctrace_tools::mangleFunction("utils", "init", params3); +std::string mangled3 = ctrace_tools::mangle::mangleFunction("utils", "init", params3); std::cout << "Mangled utils::init(): " << mangled3 << "\n"; ``` diff --git a/include/Config/config.hpp b/include/Config/config.hpp index 9e5f1ae..fd94090 100644 --- a/include/Config/config.hpp +++ b/include/Config/config.hpp @@ -35,6 +35,8 @@ static void printHelp(void) --ipc Specifies the IPC method to use (e.g., fifo, socket). --ipc-path Specifies the IPC path (default: /tmp/coretrace_ipc). --async Enables asynchronous execution. + --shutdown-token Token required for POST /shutdown (server mode). + --shutdown-timeout-ms Graceful shutdown timeout in ms (0 = wait indefinitely). Examples: ctrace --input main.cpp,util.cpp --static --invoke=cppcheck,flawfinder @@ -82,6 +84,8 @@ namespace ctrace std::string ipcPath = "/tmp/coretrace_ipc"; ///< Path for IPC communication. std::string serverHost = "127.0.0.1"; ///< Host for server IPC (if applicable). int serverPort = 8080; ///< Port for server IPC (if applicable). + std::string shutdownToken; ///< Token required for POST /shutdown. + int shutdownTimeoutMs = 0; ///< Shutdown timeout in milliseconds (0 = wait indefinitely). std::vector specificTools; ///< List of specific tools to invoke. @@ -228,6 +232,18 @@ namespace ctrace config.global.serverPort = std::stoi(value); std::cout << "[DEBUG] Server port set to " << config.global.serverPort << std::endl; }; + commands["--shutdown-token"] = [this](const std::string& value) + { + config.global.shutdownToken = value; + }; + commands["--shutdown-timeout-ms"] = [this](const std::string& value) + { + config.global.shutdownTimeoutMs = std::stoi(value); + if (config.global.shutdownTimeoutMs < 0) + { + config.global.shutdownTimeoutMs = 0; + } + }; // commands["--output"] = [this](const std::string& value) { // if (!config.files.empty()) config.files.back().output_file = value; // }; diff --git a/include/Process/Ipc/HttpServer.hpp b/include/Process/Ipc/HttpServer.hpp index d482ce4..a48be8a 100644 --- a/include/Process/Ipc/HttpServer.hpp +++ b/include/Process/Ipc/HttpServer.hpp @@ -1,8 +1,12 @@ // server.cpp #include +#include +#include +#include #include #include #include +#include #include #include #include @@ -494,8 +498,11 @@ class ApiHandler class HttpServer { public: - HttpServer(ApiHandler& apiHandler, ILogger& logger) - : apiHandler_(apiHandler), logger_(logger) + HttpServer(ApiHandler& apiHandler, ILogger& logger, const ctrace::GlobalConfig& config) + : apiHandler_(apiHandler), + logger_(logger), + shutdown_token_(config.shutdownToken), + shutdown_timeout_(std::chrono::milliseconds(config.shutdownTimeoutMs)) {} void run(const std::string& host, int port) @@ -503,7 +510,14 @@ class HttpServer // CORS server_.Options("/api", [this](const httplib::Request&, httplib::Response& res) { set_cors(res); - res.status = 200; + if (is_shutting_down()) + { + res.status = 503; + } + else + { + res.status = 200; + } }); // Endpoint principal @@ -512,14 +526,61 @@ class HttpServer handle_post_api(req, res); }); + // Shutdown endpoint + server_.Options("/shutdown", [this](const httplib::Request&, httplib::Response& res) { + set_cors(res); + if (is_shutting_down()) + { + res.status = 503; + } + else + { + res.status = 200; + } + }); + + server_.Post("/shutdown", [this](const httplib::Request& req, httplib::Response& res) { + set_cors(res); + handle_post_shutdown(req, res); + }); + logger_.info("[SERVER] Listening on http://" + host + ":" + std::to_string(port)); server_.listen(host.c_str(), port); + finalize_shutdown(); } private: + struct InFlightGuard + { + explicit InFlightGuard(HttpServer& server) : server_(server) + { + server_.begin_request(); + } + + ~InFlightGuard() + { + server_.end_request(); + } + + HttpServer& server_; + }; + httplib::Server server_; ApiHandler& apiHandler_; ILogger& logger_; + std::atomic shutting_down_{false}; + std::atomic shutdown_requested_{false}; + std::atomic in_flight_{0}; + std::mutex shutdown_mutex_; + std::condition_variable shutdown_cv_; + std::thread shutdown_thread_; + std::string shutdown_token_; + std::chrono::milliseconds shutdown_timeout_{0}; + + bool is_shutting_down() const + { + return shutting_down_.load(std::memory_order_acquire); + } static void set_cors(httplib::Response& res) { @@ -528,8 +589,116 @@ class HttpServer res.set_header("Access-Control-Allow-Headers", "Content-Type"); } + void begin_request() + { + in_flight_.fetch_add(1, std::memory_order_relaxed); + } + + void end_request() + { + const int remaining = in_flight_.fetch_sub(1, std::memory_order_acq_rel) - 1; + if (remaining == 0) + { + shutdown_cv_.notify_all(); + } + } + + static void flush_logs() + { + std::cout << std::flush; + std::cerr << std::flush; + } + + bool is_authorized_shutdown(const httplib::Request& req) const + { + if (shutdown_token_.empty()) + { + return false; + } + + const std::string bearer = req.get_header_value("Authorization"); + if (!bearer.empty()) + { + const std::string prefix = "Bearer "; + if (bearer.rfind(prefix, 0) == 0) + { + return bearer.substr(prefix.size()) == shutdown_token_; + } + return bearer == shutdown_token_; + } + + const std::string admin_token = req.get_header_value("X-Admin-Token"); + return !admin_token.empty() && admin_token == shutdown_token_; + } + + void initiate_shutdown() + { + bool expected = false; + if (!shutdown_requested_.compare_exchange_strong(expected, true)) + { + return; + } + + shutting_down_.store(true, std::memory_order_release); + + shutdown_thread_ = std::thread([this]() { + logger_.info("[SERVER] Shutdown requested. Stopping listener..."); + server_.stop(); + wait_for_inflight_or_timeout(); + }); + } + + void wait_for_inflight_or_timeout() + { + std::unique_lock lock(shutdown_mutex_); + const auto done = [this]() { + return in_flight_.load(std::memory_order_acquire) == 0; + }; + + if (shutdown_timeout_.count() > 0) + { + if (!shutdown_cv_.wait_for(lock, shutdown_timeout_, done)) + { + logger_.error("[SERVER] Shutdown timeout exceeded. Forcing exit."); + } + } + else + { + shutdown_cv_.wait(lock, done); + } + } + + void finalize_shutdown() + { + if (shutdown_thread_.joinable()) + { + shutdown_thread_.join(); + } + if (shutdown_requested_.load(std::memory_order_acquire)) + { + logger_.info("[SERVER] Shutdown complete."); + } + flush_logs(); + } + void handle_post_api(const httplib::Request& req, httplib::Response& res) { + if (is_shutting_down()) + { + json err; + err["proto"] = "coretrace-1.0"; + err["type"] = "response"; + err["status"] = "error"; + err["error"] = { + {"code", "ServerShuttingDown"}, + {"message", "Server is shutting down."} + }; + res.status = 503; + res.set_content(err.dump(), "application/json"); + return; + } + + InFlightGuard guard(*this); try { json request = json::parse(req.body); json response = apiHandler_.handle_request(request); @@ -552,4 +721,42 @@ class HttpServer res.set_content(err.dump(), "application/json"); } } + + void handle_post_shutdown(const httplib::Request& req, httplib::Response& res) + { + if (!is_authorized_shutdown(req)) + { + json err; + err["status"] = "error"; + err["error"] = { + {"code", "Unauthorized"}, + {"message", shutdown_token_.empty() + ? "Shutdown token not configured." + : "Invalid shutdown token."} + }; + res.status = 403; + res.set_content(err.dump(), "application/json"); + return; + } + + if (shutdown_requested_.load(std::memory_order_acquire)) + { + json ok; + ok["status"] = "accepted"; + ok["message"] = "Shutdown already in progress."; + ok["timeout_ms"] = shutdown_timeout_.count(); + res.status = 202; + res.set_content(ok.dump(), "application/json"); + return; + } + + json ok; + ok["status"] = "accepted"; + ok["message"] = "Shutdown initiated."; + ok["timeout_ms"] = shutdown_timeout_.count(); + res.status = 202; + res.set_content(ok.dump(), "application/json"); + + initiate_shutdown(); + } }; diff --git a/include/Process/Tools/AnalysisTools.hpp b/include/Process/Tools/AnalysisTools.hpp index 10f6bb2..a0db540 100644 --- a/include/Process/Tools/AnalysisTools.hpp +++ b/include/Process/Tools/AnalysisTools.hpp @@ -24,7 +24,7 @@ class EntryPoint EntryPoint(const std::string& entryPointName, const std::vector& paramTypes) : name(entryPointName), paramTypes(paramTypes) { - m_isMangled = ctrace_tools::isMangled(name); + m_isMangled = ctrace_tools::mangle::isMangled(name); if (m_isMangled) { @@ -32,7 +32,7 @@ class EntryPoint } else { - mangledName = ctrace_tools::mangleFunction("", name, paramTypes); + mangledName = ctrace_tools::mangle::mangleFunction("", name, paramTypes); } } ~EntryPoint() = default; diff --git a/include/ctrace_tools/mangle.hpp b/include/ctrace_tools/mangle.hpp index dcb58c4..1c987e5 100644 --- a/include/ctrace_tools/mangle.hpp +++ b/include/ctrace_tools/mangle.hpp @@ -5,7 +5,7 @@ #include #include -namespace ctrace_tools +namespace ctrace_tools::mangle { /** * @brief Concept to define types that can be converted to `std::string_view`. diff --git a/src/App/Config.cpp b/src/App/Config.cpp index 19f54fc..4935a68 100644 --- a/src/App/Config.cpp +++ b/src/App/Config.cpp @@ -30,6 +30,8 @@ namespace ctrace argManager.addOption("--ipc-path", true, 't'); argManager.addOption("--serve-host", true, 'z'); argManager.addOption("--serve-port", true, 'y'); + argManager.addOption("--shutdown-token", true, 'k'); + argManager.addOption("--shutdown-timeout-ms", true, 'm'); // Parsing des arguments argManager.parse(argc, argv); diff --git a/src/App/Runner.cpp b/src/App/Runner.cpp index 3efd169..93bf0c9 100644 --- a/src/App/Runner.cpp +++ b/src/App/Runner.cpp @@ -18,7 +18,7 @@ namespace ctrace << config.global.serverPort << "...\033[0m\n"; ConsoleLogger logger; ApiHandler apiHandler(logger); - HttpServer server(apiHandler, logger); + HttpServer server(apiHandler, logger, config.global); server.run(config.global.serverHost, config.global.serverPort); return EXIT_SUCCESS; } diff --git a/src/ctrace_tools/mangle.cpp b/src/ctrace_tools/mangle.cpp index 5731998..ac943ea 100644 --- a/src/ctrace_tools/mangle.cpp +++ b/src/ctrace_tools/mangle.cpp @@ -1,6 +1,6 @@ #include "ctrace_tools/mangle.hpp" -namespace ctrace_tools { +namespace ctrace_tools::mangle { std::string mangleFunction(const std::string& namespaceName, const std::string& functionName,