Something went wrong. Try again.
Toolkit for tile-based quality reconstruction of astronomical image stacks
dwarf stacking two-seestar
Something went wrong. Try again.
26 kB · 567 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568#include "backend_runtime.hpp"#include <algorithm>#include <cstdlib>#include <fstream>#include <sstream>#include <system_error>#include <vector>
#ifdef _WIN32#include <windows.h>#include <winnetwk.h>#pragma comment(lib, "mpr.lib")#endif
namespace {
/// @brief Implements weakly normalize./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.fs::path weakly_normalize(const fs::path& p) { if (p.empty()) return {}; std::error_code ec; fs::path normalized = fs::weakly_canonical(p, ec); if (ec) return p.lexically_normal(); return normalized;}
/// @brief Checks whether like project root./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.bool looks_like_project_root(const fs::path& dir) { if (dir.empty()) return false; std::error_code ec; return fs::is_directory(dir / "web_frontend_v3", ec) && fs::is_directory(dir / "web_backend_cpp", ec) && fs::is_directory(dir / "tile_compile_cpp", ec);}
/// @brief Discovers project root./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.fs::path discover_project_root(fs::path start) { start = weakly_normalize(start); if (start.empty()) return {}; std::error_code ec; if (!fs::is_directory(start, ec)) start = start.parent_path(); for (fs::path current = start; !current.empty(); current = current.parent_path()) { if (looks_like_project_root(current)) return current; if (current == current.parent_path()) break; } return {};}
/// @brief Implements detect default project root./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.fs::path detect_default_project_root() { if (auto from_cwd = discover_project_root(fs::current_path()); !from_cwd.empty()) { return from_cwd; }#ifdef __linux__ std::error_code ec; fs::path exe_path = fs::read_symlink("/proc/self/exe", ec); if (!ec) { if (auto from_exe = discover_project_root(exe_path.parent_path()); !from_exe.empty()) { return from_exe; } }#endif return weakly_normalize(fs::current_path());}
/// @brief Implements env string./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.std::string env_string(const char* name, const char* def = "") { const char* v = std::getenv(name); return v ? v : def;}
/// Splits a path list string into individual path strings./// On Windows, semicolons are used as separators (colons would break drive letters).std::vector<std::string> split_colon_paths(const std::string& s) { std::vector<std::string> result; if (s.empty()) return result;#ifdef _WIN32 std::istringstream iss(s); std::string part; while (std::getline(iss, part, ';')) { if (!part.empty()) result.push_back(std::move(part)); }#else std::istringstream iss(s); std::string part; while (std::getline(iss, part, ':')) { if (!part.empty()) result.push_back(std::move(part)); }#endif return result;}
/// @brief Parses size t env./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.size_t parse_size_t_env(const char* name, size_t fallback, size_t min_value, size_t max_value) { const std::string raw = env_string(name, ""); if (raw.empty()) return fallback; try { const unsigned long long parsed = std::stoull(raw); return std::clamp<size_t>(static_cast<size_t>(parsed), min_value, max_value); } catch (...) { return fallback; }}
/// @brief Parses uintmax env./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.uintmax_t parse_uintmax_env(const char* name, uintmax_t fallback, uintmax_t min_value, uintmax_t max_value) { const std::string raw = env_string(name, ""); if (raw.empty()) return fallback; try { const unsigned long long parsed = std::stoull(raw); return std::clamp<uintmax_t>(static_cast<uintmax_t>(parsed), min_value, max_value); } catch (...) { return fallback; }}
}
/// @brief Resolves an executable name to an absolute path./// @details Tries the value as-is, then relative to the backend executable's directory,/// then relative to the current working directory, and finally on Windows appends `.exe`./// This ensures subprocess launches find the runner/CLI even when the backend is started from a different directory.fs::path resolve_executable_path(const fs::path& candidate) { if (candidate.empty()) return {}; std::error_code ec;
#ifdef _WIN32 const bool has_ext = candidate.has_extension();#else const bool has_ext = true;#endif
auto probe = [&](const fs::path& p) -> fs::path { if (fs::exists(p, ec) && fs::is_regular_file(p, ec)) return p;#ifdef _WIN32 if (!has_ext) { fs::path with_exe = p; with_exe += ".exe"; if (fs::exists(with_exe, ec) && fs::is_regular_file(with_exe, ec)) return with_exe; }#endif return {}; };
if (candidate.is_absolute()) return probe(candidate);
fs::path backend_dir;#ifdef _WIN32 char buf[MAX_PATH] = {}; if (GetModuleFileNameA(nullptr, buf, MAX_PATH) > 0) { backend_dir = fs::path(buf).parent_path(); }#else if (const char* self = std::getenv("_"); self && self[0]) { backend_dir = fs::path(self).parent_path(); }#endif if (!backend_dir.empty()) { if (auto p = probe(backend_dir / candidate); !p.empty()) return p; } if (auto p = probe(fs::current_path() / candidate); !p.empty()) return p;
return candidate;}
/// @brief Implements backend guard limits from env./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.BackendGuardLimits backend_guard_limits_from_env() { BackendGuardLimits limits; limits.subprocess_capture_bytes = parse_size_t_env("TILE_COMPILE_BACKEND_SUBPROCESS_CAPTURE_BYTES", limits.subprocess_capture_bytes, 64 * 1024, 8 * 1024 * 1024); limits.job_stdio_store_bytes = parse_size_t_env("TILE_COMPILE_BACKEND_JOB_STDIO_STORE_BYTES", limits.job_stdio_store_bytes, 8 * 1024, 512 * 1024); limits.scan_frames_preview = parse_size_t_env("TILE_COMPILE_BACKEND_SCAN_FRAMES_PREVIEW", limits.scan_frames_preview, 1, 4096); limits.scan_per_dir_frames_preview = parse_size_t_env("TILE_COMPILE_BACKEND_SCAN_PER_DIR_FRAMES_PREVIEW", limits.scan_per_dir_frames_preview, 1, 512); limits.scan_per_dir_results_preview = parse_size_t_env("TILE_COMPILE_BACKEND_SCAN_PER_DIR_RESULTS_PREVIEW", limits.scan_per_dir_results_preview, 1, 512); limits.scan_messages_preview = parse_size_t_env("TILE_COMPILE_BACKEND_SCAN_MESSAGES_PREVIEW", limits.scan_messages_preview, 1, 1024); limits.scan_color_candidates_preview = parse_size_t_env("TILE_COMPILE_BACKEND_SCAN_COLOR_CANDIDATES_PREVIEW", limits.scan_color_candidates_preview, 1, 128); limits.report_events_max = parse_size_t_env("TILE_COMPILE_BACKEND_REPORT_EVENTS_MAX", limits.report_events_max, 128, 32768); limits.report_log_tail = parse_size_t_env("TILE_COMPILE_BACKEND_REPORT_LOG_TAIL", limits.report_log_tail, 16, 2048); limits.report_text_bytes = parse_size_t_env("TILE_COMPILE_BACKEND_REPORT_TEXT_BYTES", limits.report_text_bytes, 32 * 1024, 2 * 1024 * 1024); limits.report_json_file_bytes = parse_uintmax_env("TILE_COMPILE_BACKEND_REPORT_JSON_FILE_BYTES", limits.report_json_file_bytes, 256 * 1024, 64 * 1024 * 1024); limits.retained_jobs = parse_size_t_env("TILE_COMPILE_BACKEND_RETAINED_JOBS", limits.retained_jobs, 8, 1000); return limits;}
/// @brief Implements from env./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.BackendRuntime BackendRuntime::from_env() { BackendRuntime rt;
rt.guard_limits = backend_guard_limits_from_env();
std::string project_root_str = env_string("TILE_COMPILE_PROJECT_ROOT", ""); if (project_root_str.empty()) { rt.project_root = detect_default_project_root(); } else { rt.project_root = weakly_normalize(fs::path(project_root_str)); }
std::string runs_dir_str = env_string("TILE_COMPILE_RUNS_DIR", ""); if (runs_dir_str.empty()) rt.runs_dir = rt.project_root / "runs"; else rt.runs_dir = weakly_normalize(fs::path(runs_dir_str));
std::string config_str = env_string("TILE_COMPILE_CONFIG", ""); if (config_str.empty()) rt.default_config_path = rt.project_root / "tile_compile_cpp" / "tile_compile.yaml"; else rt.default_config_path = fs::path(config_str);
std::string schema_str = env_string("TILE_COMPILE_SCHEMA", ""); if (schema_str.empty()) rt.schema_path = rt.project_root / "tile_compile_cpp" / "tile_compile.schema.yaml"; else rt.schema_path = fs::path(schema_str);
std::string presets_str = env_string("TILE_COMPILE_PRESETS_DIR", ""); if (presets_str.empty()) rt.presets_dir = rt.project_root / "tile_compile_cpp" / "examples"; else rt.presets_dir = fs::path(presets_str);
std::string ui_str = env_string("TILE_COMPILE_UI_DIR", ""); if (ui_str.empty()) rt.ui_dir = rt.project_root / "web_frontend_v3"; else rt.ui_dir = fs::path(ui_str);
std::string runtime_dir_str = env_string("TILE_COMPILE_RUNTIME_DIR", ""); if (runtime_dir_str.empty()) rt.runtime_dir = rt.project_root / "web_backend_cpp" / "runtime"; else rt.runtime_dir = fs::path(runtime_dir_str); rt.ui_events_path = rt.runtime_dir / "ui_events.jsonl";
rt.host = env_string("TILE_COMPILE_HOST", env_string("HOST", "127.0.0.1").c_str());
// Prefer the repository build artifacts when no explicit executable was // configured. This prevents a stale PATH binary from handling resumes. auto resolve_project_executable = [&](const char* env_name, const char* executable_name) { const std::string configured = env_string(env_name, ""); if (!configured.empty()) { return resolve_executable_path(fs::path(configured)).string(); } for (const auto& candidate : { rt.project_root / "tile_compile_cpp" / "build" / executable_name, rt.project_root / "build" / executable_name, }) { std::error_code ec; if (fs::is_regular_file(candidate, ec)) return candidate.string(); } return resolve_executable_path(fs::path(executable_name)).string(); };
rt.cli_exe = resolve_project_executable("TILE_COMPILE_CLI", "tile_compile_cli"); rt.runner_exe = resolve_project_executable("TILE_COMPILE_RUNNER", "tile_compile_runner");
std::string port_str = env_string("TILE_COMPILE_PORT", "8000"); try { rt.port = std::stoi(port_str); } catch (...) { rt.port = 8000; }
for (const auto& root : { rt.project_root, rt.runs_dir,#ifdef _WIN32 fs::path(std::getenv("USERPROFILE") ? std::getenv("USERPROFILE") : ""), fs::path(std::getenv("HOMEPATH") ? std::getenv("HOMEPATH") : ""),#else fs::path(std::getenv("HOME") ? std::getenv("HOME") : ""), fs::path("/tmp"),#endif }) { if (!root.empty()) { const std::string normalized = rt.normalize_path(root).string(); if (!normalized.empty()) rt._allowed_roots.insert(normalized); } }
std::string input_dir_str = env_string("TILE_COMPILE_INPUT_DIR", ""); if (!input_dir_str.empty()) { const std::string normalized = rt.normalize_path(fs::path(input_dir_str)).string(); if (!normalized.empty()) rt._allowed_roots.insert(normalized); }
std::string astap_data_dir_str = env_string("TILE_COMPILE_ASTAP_DATA_DIR", ""); if (!astap_data_dir_str.empty()) { const std::string normalized = rt.normalize_path(fs::path(astap_data_dir_str)).string(); if (!normalized.empty()) rt._allowed_roots.insert(normalized); }
std::string siril_catalog_dir_str = env_string("TILE_COMPILE_SIRIL_CATALOG_DIR", ""); if (!siril_catalog_dir_str.empty()) { const std::string normalized = rt.normalize_path(fs::path(siril_catalog_dir_str)).string(); if (!normalized.empty()) rt._allowed_roots.insert(normalized); }
#ifdef _WIN32 { const DWORD drives = GetLogicalDrives(); for (int i = 0; i < 26; ++i) { if (drives & (1u << i)) { char drive_letter = static_cast<char>('A' + i); fs::path drive_root = fs::path(std::string(1, drive_letter) + ":\\"); const std::string normalized = rt.normalize_path(drive_root).string(); if (!normalized.empty()) rt._allowed_roots.insert(normalized); } } } // Enumerate UNC network shares (\\server\share) and add as allowed roots { HANDLE hEnum = nullptr; if (WNetOpenEnum(RESOURCE_GLOBALNET, RESOURCETYPE_DISK, 0, nullptr, &hEnum) == NO_ERROR) { DWORD count = 1; DWORD bufSize = 16384; std::vector<char> buf(bufSize); for (;;) { count = 1; DWORD result = WNetEnumResource(hEnum, &count, buf.data(), &bufSize); if (result == NO_ERROR && count > 0) { auto* res = reinterpret_cast<NETRESOURCEA*>(buf.data()); for (DWORD i = 0; i < count; ++i) { if (res[i].lpRemoteName && res[i].dwType == RESOURCETYPE_DISK) { std::string unc_path(res[i].lpRemoteName); // Normalize and add as allowed root const std::string normalized = rt.normalize_path(fs::path(unc_path)).string(); if (!normalized.empty()) rt._allowed_roots.insert(normalized); } } } else if (result == ERROR_MORE_DATA) { buf.resize(bufSize); continue; } else { break; } } if (hEnum) WNetCloseEnum(hEnum); } }#endif
std::string allowed_roots = env_string("TILE_COMPILE_ALLOWED_ROOTS", ""); for (const auto& root : split_colon_paths(allowed_roots)) { rt._allowed_roots.insert(rt.normalize_path(fs::path(root)).string()); }
std::string input_roots = env_string("TILE_COMPILE_INPUT_SEARCH_ROOTS", ""); for (const auto& root : split_colon_paths(input_roots)) { rt._input_search_roots.push_back(rt.normalize_path(fs::path(root))); }
// Load persisted granted roots from file { fs::path roots_file = rt.project_root / ".allowed_roots"; std::error_code ec; if (fs::exists(roots_file, ec)) { std::ifstream in(roots_file); std::string line; while (std::getline(in, line)) { auto trimmed = line; while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\r' || trimmed.back() == '\n')) trimmed.pop_back(); if (trimmed.empty()) continue; const std::string normalized = rt.normalize_path(fs::path(trimmed)).string(); if (!normalized.empty()) rt._allowed_roots.insert(normalized); } } }
return rt;}
/// @brief Normalizes path./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.fs::path BackendRuntime::normalize_path(const fs::path& p) { if (p.empty()) return {}; std::error_code ec; fs::path candidate = p; if (!candidate.is_absolute()) candidate = fs::current_path() / candidate; fs::path normalized = fs::weakly_canonical(candidate, ec); if (ec) normalized = candidate.lexically_normal(); return normalized;}
/// @brief Checks whether within root./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.bool BackendRuntime::is_within_root(const fs::path& candidate, const fs::path& root) { const fs::path normalized_candidate = normalize_path(candidate); const fs::path normalized_root = normalize_path(root); if (normalized_candidate.empty() || normalized_root.empty()) return false; if (normalized_candidate == normalized_root) return true;
auto root_it = normalized_root.begin(); auto candidate_it = normalized_candidate.begin(); for (; root_it != normalized_root.end() && candidate_it != normalized_candidate.end(); ++root_it, ++candidate_it) { if (*root_it != *candidate_it) return false; } return root_it == normalized_root.end();}
/// @brief Resolves run dir./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.fs::path BackendRuntime::resolve_run_dir(const std::string& run_id, const std::string& alt_runs_dir) const { if (run_id.empty()) throw std::invalid_argument("run_id is empty"); // If run_id is already an absolute path to an existing directory, return it directly. // This handles custom runs_dir / network drives where the frontend passes the full path. { fs::path as_path(run_id); if (as_path.is_absolute()) { std::error_code ec; if (fs::is_directory(as_path, ec) && !ec) return as_path; } } // Try alt_runs_dir first if provided (e.g. from job data for custom runs_dir) if (!alt_runs_dir.empty()) { fs::path alt_candidate = fs::path(alt_runs_dir) / run_id; if (fs::is_directory(alt_candidate)) return alt_candidate; std::error_code ec; if (fs::is_directory(alt_runs_dir, ec)) { for (auto& entry : fs::directory_iterator(alt_runs_dir, ec)) { if (ec) break; if (!entry.is_directory()) continue; std::string name = entry.path().filename().string(); if (name == run_id || name.find(run_id) == 0) return entry.path(); } } } fs::path candidate = runs_dir / run_id; if (fs::is_directory(candidate)) return candidate; std::error_code ec2; if (fs::is_directory(runs_dir, ec2)) { for (auto& entry : fs::directory_iterator(runs_dir, ec2)) { if (ec2) break; if (!entry.is_directory()) continue; std::string name = entry.path().filename().string(); if (name == run_id || name.find(run_id) == 0) return entry.path(); } } throw std::runtime_error("run_dir not found for run_id: " + run_id);}
/// @brief Resolves input path./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.PathResolution BackendRuntime::resolve_input_path(const fs::path& p, bool must_exist) const { if (p.empty()) return {PathStatus::not_found, {}};
if (p.is_absolute()) { fs::path normalized = normalize_path(p); if (!is_path_allowed(normalized)) return {PathStatus::not_allowed, normalized}; if (must_exist && !fs::exists(normalized)) return {PathStatus::not_found, normalized}; return {PathStatus::ok, normalized}; }
for (const auto& base : _input_search_roots) { fs::path candidate = normalize_path(base / p); if (!is_path_allowed(candidate)) continue; if (fs::exists(candidate)) return {PathStatus::ok, candidate}; }
fs::path fallback = normalize_path(project_root / p); if (!is_path_allowed(fallback)) return {PathStatus::not_allowed, fallback}; if (must_exist && !fs::exists(fallback)) return {PathStatus::not_found, fallback}; return {PathStatus::ok, fallback};}
/// @brief Checks whether path allowed./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.bool BackendRuntime::is_path_allowed(const fs::path& p) const { std::lock_guard<std::mutex> lk(*_roots_mutex); for (auto& root : _allowed_roots) { if (is_within_root(p, fs::path(root))) return true; } return false;}
/// @brief Implements allowed roots./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.std::vector<fs::path> BackendRuntime::allowed_roots() const { std::lock_guard<std::mutex> lk(*_roots_mutex); std::vector<fs::path> roots; roots.reserve(_allowed_roots.size()); for (const auto& root : _allowed_roots) roots.emplace_back(root); return roots;}
/// @brief Implements input search roots./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.std::vector<fs::path> BackendRuntime::input_search_roots() const { std::lock_guard<std::mutex> lk(*_roots_mutex); return _input_search_roots;}
/// @brief Implements grant root./// @details This implementation resolves backend runtime paths, environment overrides, and filesystem safety roots; it keeps JSON shapes, filesystem/// access, process handling, and error reporting localized to this backend component.void BackendRuntime::grant_root(const fs::path& p) { std::lock_guard<std::mutex> lk(*_roots_mutex); _allowed_roots.insert(normalize_path(p).string());
// Persist to file so granted roots survive backend restarts if (!project_root.empty()) { fs::path roots_file = project_root / ".allowed_roots"; std::ofstream out(roots_file, std::ios::app); if (out) { out << normalize_path(p).string() << "\n"; } }}