Something went wrong. Try again.
Toolkit for tile-based quality reconstruction of astronomical image stacks
dwarf stacking two-seestar
Something went wrong. Try again.
17 kB · 428 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429#include "app_state.hpp"#include "routes/system_routes.hpp"#include "routes/jobs_routes.hpp"#include "routes/app_state_routes.hpp"#include "routes/scan_routes.hpp"#include "routes/ai_routes.hpp"#include "routes/pi_routes.hpp"#include "routes/config_routes.hpp"#include "routes/runs_routes.hpp"#include "routes/ws_routes.hpp"#include "routes/tools_routes.hpp"#include "routes/preprocessing_routes.hpp"#include "tile_compile/core/build_info.hpp"
#define CROW_MAIN#include "crow_app.hpp"
#include <iostream>#include <fstream>#include <memory>#include <filesystem>#include <system_error>#include <vector>#include <thread>#include <chrono>#include <nlohmann/json.hpp>
#ifdef __linux__#include <signal.h>#include <unistd.h>#endif
namespace fs = std::filesystem;
namespace {
/// @brief Checks whether queue staging job dir./// @details This implementation initializes runtime state, cleans orphaned resources, registers routes, and starts Crow; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.bool is_queue_staging_job_dir(const fs::path& path) { if (path.empty()) return false; const std::string name = path.filename().string(); return name.rfind("job_", 0) == 0;}
#ifdef __linux__/// @brief Reads process argv./// @details This implementation initializes runtime state, cleans orphaned resources, registers routes, and starts Crow; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.std::vector<std::string> read_process_argv(const fs::path& proc_dir) { std::ifstream cmdline(proc_dir / "cmdline", std::ios::binary); if (!cmdline) return {}; std::string raw((std::istreambuf_iterator<char>(cmdline)), std::istreambuf_iterator<char>()); if (raw.empty()) return {};
std::vector<std::string> argv; size_t start = 0; while (start < raw.size()) { size_t end = raw.find('\0', start); if (end == std::string::npos) end = raw.size(); if (end > start) argv.push_back(raw.substr(start, end - start)); start = end + 1; } return argv;}
bool argv_references_path(const std::vector<std::string>& argv, const fs::path& target_path) { const std::string target = target_path.string(); if (target.empty()) return false; return std::any_of(argv.begin(), argv.end(), [&target](const std::string& arg) { return arg.find(target) != std::string::npos; });}
bool process_cmdline_references_path(const fs::path& target_path, const std::string& runner_name, int self_pid) { std::error_code ec; if (!fs::exists("/proc", ec)) return false;
for (const auto& entry : fs::directory_iterator("/proc", ec)) { if (ec || !entry.is_directory()) continue; const std::string pid_text = entry.path().filename().string(); if (pid_text.empty() || !std::all_of(pid_text.begin(), pid_text.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; })) { continue; }
int pid = 0; try { pid = std::stoi(pid_text); } catch (...) { continue; } if (pid == self_pid) continue;
std::vector<std::string> argv = read_process_argv(entry.path()); if (argv.empty()) continue;
const std::string exe_name = fs::path(argv.front()).filename().string(); const bool is_runner = exe_name == runner_name || exe_name.find("tile_compile_runner") != std::string::npos; if (!is_runner) continue;
if (argv_references_path(argv, target_path)) { return true; } }
return false;}
/// @brief Checks process exists./// @details This implementation initializes runtime state, cleans orphaned resources, registers routes, and starts Crow; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.bool pid_exists(pid_t pid) { if (pid <= 0) return false; if (kill(pid, 0) == 0) return true; return errno == EPERM;}
/// @brief Implements terminate pid group./// @details This implementation initializes runtime state, cleans orphaned resources, registers routes, and starts Crow; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.bool terminate_pid_group(pid_t pid) { if (pid <= 0) return false; if (kill(-pid, SIGTERM) != 0) { if (kill(pid, SIGTERM) != 0) return false; } const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (std::chrono::steady_clock::now() < deadline) { if (!pid_exists(pid)) return true; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } if (kill(-pid, SIGKILL) != 0) { if (kill(pid, SIGKILL) != 0) return false; } return true;}
bool is_backend_managed_process(const std::vector<std::string>& argv, const BackendRuntime& runtime) { if (argv.empty()) return false; const std::string exe_name = fs::path(argv.front()).filename().string(); const std::string runner_name = fs::path(runtime.runner_exe).filename().string(); const std::string cli_name = fs::path(runtime.cli_exe).filename().string(); const bool matches_runner = exe_name == runner_name || exe_name.find("tile_compile_runner") != std::string::npos; const bool matches_cli = exe_name == cli_name || exe_name.find("tile_compile_cli") != std::string::npos; if (!matches_runner && !matches_cli) return false;
if (matches_runner) { const bool is_run_like = std::any_of(argv.begin() + 1, argv.end(), [](const std::string& arg) { return arg == "run" || arg == "resume"; }); if (!is_run_like) return false; }
return argv_references_path(argv, runtime.project_root) || argv_references_path(argv, runtime.runs_dir) || argv_references_path(argv, runtime.runtime_dir) || argv_references_path(argv, runtime.default_config_path);}
/// @brief Implements cleanup orphan backend processes./// @details This implementation initializes runtime state, cleans orphaned resources, registers routes, and starts Crow; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.void cleanup_orphan_backend_processes(const BackendRuntime& runtime) { std::error_code ec; if (!fs::exists("/proc", ec)) return;
int killed = 0; int failed = 0; for (const auto& entry : fs::directory_iterator("/proc", ec)) { if (ec || !entry.is_directory()) continue; const std::string pid_text = entry.path().filename().string(); if (pid_text.empty() || !std::all_of(pid_text.begin(), pid_text.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; })) { continue; }
int pid = 0; try { pid = std::stoi(pid_text); } catch (...) { continue; } if (pid == static_cast<int>(::getpid())) continue;
const std::vector<std::string> argv = read_process_argv(entry.path()); if (!is_backend_managed_process(argv, runtime)) continue;
if (terminate_pid_group(static_cast<pid_t>(pid))) { ++killed; } else { ++failed; } }
if (killed > 0 || failed > 0) { std::cout << "[tile_compile_web_backend] Startup process cleanup: killed=" << killed << " failed=" << failed << std::endl; }}#endif
/// @brief Implements cleanup orphan queue staging./// @details This implementation initializes runtime state, cleans orphaned resources, registers routes, and starts Crow; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.void cleanup_orphan_queue_staging(const BackendRuntime& runtime) { const fs::path staging_root = runtime.runs_dir / ".queue_staging"; std::error_code ec; if (!fs::exists(staging_root, ec) || !fs::is_directory(staging_root, ec)) return;
const std::string runner_name = fs::path(runtime.runner_exe).filename().string(); int removed = 0; int kept = 0; int failed = 0;
for (const auto& entry : fs::directory_iterator(staging_root, ec)) { if (ec) break; if (!entry.is_directory()) continue; if (!is_queue_staging_job_dir(entry.path())) continue;
bool is_live = false;#ifdef __linux__ is_live = process_cmdline_references_path(entry.path(), runner_name, static_cast<int>(::getpid()));#endif if (is_live) { ++kept; continue; }
fs::remove_all(entry.path(), ec); if (ec) { ++failed; std::cerr << "[tile_compile_web_backend] Failed to remove stale queue staging dir: " << entry.path() << " (" << ec.message() << ")" << std::endl; ec.clear(); continue; } ++removed; }
bool root_empty = false; if (!ec) { root_empty = fs::is_empty(staging_root, ec); } if (!ec && root_empty) { fs::remove(staging_root, ec); ec.clear(); }
if (removed > 0 || kept > 0 || failed > 0) { std::cout << "[tile_compile_web_backend] Queue staging cleanup: removed=" << removed << " kept_live=" << kept << " failed=" << failed << std::endl; }}
} // namespace
/// @brief Starts the backend process and wires runtime services./// @details This implementation initializes runtime state, cleans orphaned resources, registers routes, and starts Crow; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.int main(int argc, char* argv[]) { bool version_requested = false; bool version_json = false; for (int i = 1; i < argc; ++i) { const std::string arg = argv[i]; version_requested = version_requested || arg == "--version"; version_json = version_json || arg == "--json"; } if (version_requested) { if (version_json) { std::cout << tile_compile::core::build_info_json(true).dump(2) << std::endl; } else { std::cout << tile_compile::core::build_info_text() << std::endl; } return 0; } try { auto state = std::make_shared<AppState>(); state->runtime = BackendRuntime::from_env(); state->job_store.configure_retention(state->runtime.guard_limits.retained_jobs); state->subprocess_manager.configure_limits(state->runtime.guard_limits); state->ui_event_store.configure(state->runtime.ui_events_path);#ifdef __linux__ cleanup_orphan_backend_processes(state->runtime);#endif cleanup_orphan_queue_staging(state->runtime);
CrowApp app;
auto& cors = app.get_middleware<crow::CORSHandler>(); cors.global() .origin("*") .methods("GET"_method, "POST"_method, "PUT"_method, "DELETE"_method, "OPTIONS"_method) .headers("Content-Type", "Authorization", "Accept");
register_system_routes(app, state); register_jobs_routes(app, state); register_app_state_routes(app, state); register_scan_routes(app, state); tile_compile::routes::register_ai_routes(app, state); tile_compile::routes::register_pi_routes(app, state); register_config_routes(app, state, nullptr); register_runs_routes(app, state); register_ws_routes(app, state); register_tools_routes(app, state); register_preprocessing_routes(app, state);
auto read_ui_file = [&state](const fs::path& relative_path) -> std::optional<std::pair<fs::path, std::string>> { fs::path f = state->runtime.ui_dir / relative_path; std::error_code ec; f = f.lexically_normal(); const fs::path ui_root = state->runtime.ui_dir.lexically_normal(); const std::string ui_root_text = ui_root.string(); const std::string file_text = f.string(); if (file_text.compare(0, ui_root_text.size(), ui_root_text) != 0) return std::nullopt; if (!fs::exists(f, ec) || fs::is_directory(f, ec)) return std::nullopt; std::ifstream in(f, std::ios::binary); if (!in) return std::nullopt; std::string body((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>()); return std::make_pair(f, std::move(body)); };
auto serve_ui_file = [&read_ui_file](const fs::path& relative_path, bool spa_fallback = true) { auto loaded = read_ui_file(relative_path); if (!loaded && spa_fallback) loaded = read_ui_file("index.html"); if (!loaded) return crow::response(404); const auto& [f, body] = *loaded; crow::response res(200, body); std::string ext = f.extension().string(); if (ext == ".html") res.set_header("Content-Type", "text/html"); else if (ext == ".js") res.set_header("Content-Type", "application/javascript"); else if (ext == ".css") res.set_header("Content-Type", "text/css"); else if (ext == ".json") res.set_header("Content-Type", "application/json"); else if (ext == ".png") res.set_header("Content-Type", "image/png"); else if (ext == ".svg") res.set_header("Content-Type", "image/svg+xml"); else res.set_header("Content-Type", "application/octet-stream"); return res; };
// Static file serving — frontend SPA if (fs::is_directory(state->runtime.ui_dir)) { CROW_ROUTE(app, "/ui/<path>") ([&serve_ui_file](const crow::request&, std::string path) { return serve_ui_file(path); });
CROW_ROUTE(app, "/ui") ([](const crow::request&) { crow::response res(302); res.set_header("Location", "/ui/"); return res; });
CROW_ROUTE(app, "/<path>") ([&serve_ui_file](const crow::request&, std::string path) { if (path == "ui" || path == "ui/") { return serve_ui_file("index.html"); } return serve_ui_file(path, false); }); }
CROW_ROUTE(app, "/api/<path>") ([](const crow::request&, std::string) { nlohmann::json body = { {"error", { {"code", "NOT_FOUND"}, {"message", "Not Found"} }} }; crow::response res(404, body.dump()); res.set_header("Content-Type", "application/json"); return res; });
// Redirect root to /ui CROW_ROUTE(app, "/") ([]() { crow::response res(302); res.set_header("Location", "/ui/"); return res; });
int port = state->runtime.port; std::cout << "[tile_compile_web_backend] Starting on http://" << state->runtime.host << ":" << port << "/ui" << std::endl;
app.bindaddr(state->runtime.host) .port(port) .multithreaded() .run();
return 0; } catch (const std::system_error& e) { std::cerr << "[tile_compile_web_backend] Fatal system error: " << e.what() << std::endl; if (e.code() == std::errc::address_in_use) { std::cerr << "[tile_compile_web_backend] Port already in use. " << "Stop the existing listener or start with another PORT." << std::endl; } else if (e.code() == std::errc::permission_denied) { std::cerr << "[tile_compile_web_backend] Permission denied while opening " << "the listening socket or runtime files." << std::endl; } else if (e.code() == std::errc::address_not_available) { std::cerr << "[tile_compile_web_backend] Bind address is not available on " << "this host." << std::endl; } return 1; } catch (const std::exception& e) { std::cerr << "[tile_compile_web_backend] Fatal error: " << e.what() << std::endl; return 1; }}