#include "fgc/Paths.h" #include "fgc/Logger.h" #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace fgc::paths { namespace { std::string envOr(const char* name, const std::string& fallback) { const char* v = std::getenv(name); return (v && *v) ? std::string(v) : fallback; } } // namespace std::string expandUser(const std::string& path) { if (path.empty()) return path; std::string out; out.reserve(path.size()); // Leading ~ -> $HOME size_t i = 0; if (path[0] == '~' && (path.size() == 1 || path[1] == '/')) { out += envOr("HOME", ""); i = 1; } // $VAR and ${VAR} expansion for (; i < path.size(); ++i) { if (path[i] == '$') { size_t start = i + 1; bool braced = (start < path.size() && path[start] == '{'); if (braced) ++start; size_t end = start; while (end < path.size() && (std::isalnum(static_cast(path[end])) || path[end] == '_')) { ++end; } std::string name = path.substr(start, end - start); if (!name.empty()) { out += envOr(name.c_str(), ""); i = braced && end < path.size() && path[end] == '}' ? end : end - 1; continue; } } out += path[i]; } return out; } std::string executableDir() { std::error_code ec; fs::path self = fs::read_symlink("/proc/self/exe", ec); if (ec) return {}; return self.parent_path().string(); } std::vector configSearchPaths(const std::string& cliArg) { std::vector paths; if (!cliArg.empty()) paths.push_back(expandUser(cliArg)); if (const char* env = std::getenv("FGC_CONFIG"); env && *env) paths.push_back(expandUser(env)); paths.push_back("config.ini"); // current working directory if (std::string dir = executableDir(); !dir.empty()) paths.push_back((fs::path(dir) / "config.ini").string()); std::string xdg = envOr("XDG_CONFIG_HOME", expandUser("~/.config")); if (!xdg.empty()) paths.push_back((fs::path(xdg) / "fire_gimbal_control" / "config.ini").string()); return paths; } std::optional resolveConfigPath(const std::string& cliArg) { std::error_code ec; for (const auto& p : configSearchPaths(cliArg)) { if (fs::is_regular_file(p, ec)) return p; } return std::nullopt; } std::vector hardwareSearchPaths(const std::string& cliArg, const std::string& configPath) { std::vector paths; if (!cliArg.empty()) paths.push_back(expandUser(cliArg)); if (const char* env = std::getenv("FGC_HARDWARE"); env && *env) paths.push_back(expandUser(env)); paths.push_back("/etc/fire_gimbal_control/hardware.ini"); // Beside config.ini, so a checkout with a local config.ini picks up a local // hardware.ini without needing root or an env var. if (!configPath.empty()) { const fs::path dir = fs::path(expandUser(configPath)).parent_path(); paths.push_back((dir.empty() ? fs::path(".") : dir) / "hardware.ini"); } return paths; } std::optional resolveHardwarePath(const std::string& cliArg, const std::string& configPath) { std::error_code ec; for (const auto& p : hardwareSearchPaths(cliArg, configPath)) { if (fs::is_regular_file(p, ec)) return p; } return std::nullopt; } std::string defaultOutputDir() { std::string base = envOr("XDG_DATA_HOME", expandUser("~/.local/share")); return (fs::path(base) / "fire_gimbal_control" / "images").string(); } std::string defaultLogDir() { std::string base = envOr("XDG_DATA_HOME", expandUser("~/.local/share")); return (fs::path(base) / "fire_gimbal_control" / "logs").string(); } std::string timestampedLogName(const std::string& prefix) { std::time_t t = std::time(nullptr); std::tm tm{}; localtime_r(&t, &tm); char buf[32]; std::strftime(buf, sizeof(buf), "%Y%m%d-%H%M%S", &tm); return prefix + "_" + buf + ".log"; } // True for "_YYYYMMDD-HHMMSS.log" and nothing else. `want` is the // prefix including its trailing underscore. static bool isGeneratedReportName(const std::string& name, const std::string& want) { const std::string rest = name.substr(want.size()); // "YYYYMMDD-HHMMSS.log" is exactly 19 characters. if (rest.size() != 19) return false; if (rest.compare(15, 4, ".log") != 0) return false; if (rest[8] != '-') return false; for (size_t i = 0; i < 15; ++i) { if (i == 8) continue; if (!std::isdigit(static_cast(rest[i]))) return false; } return true; } void pruneLogFiles(const std::string& prefix, int keep) { if (prefix.empty() || keep <= 0) return; std::error_code ec; const fs::path dir = defaultLogDir(); if (!fs::exists(dir, ec)) return; const std::string want = prefix + "_"; std::vector files; for (const auto& e : fs::directory_iterator(dir, ec)) { if (ec) return; if (!e.is_regular_file(ec)) continue; const std::string n = e.path().filename().string(); if (n.rfind(want, 0) != 0) continue; // Only files this program GENERATED, i.e. exactly // "_YYYYMMDD-HHMMSS.log" as timestampedLogName() builds them. // // Matching the prefix alone is not enough. `[Test] baseline_file` is // configurable and may legitimately live here; the default // `test_baseline.txt` happens to survive a looser match only because // 'b' sorts above the digits of a year, which is luck rather than a // guarantee - point it at `test_mybaseline.log` and a prefix-only match // would delete the very reference every self-test compares against. if (!isGeneratedReportName(n, want)) continue; files.push_back(e.path().string()); } if (static_cast(files.size()) <= keep) return; // Names embed YYYYMMDD-HHMMSS, so lexicographic order is chronological. std::sort(files.begin(), files.end(), std::greater()); for (size_t i = static_cast(keep); i < files.size(); ++i) fs::remove(files[i], ec); // best effort } std::string writeLogFile(const std::string& name, const std::string& text, int keep) { std::error_code ec; fs::path dir = defaultLogDir(); fs::create_directories(dir, ec); fs::path path = dir / name; FILE* f = std::fopen(path.c_str(), "wb"); if (!f) { LOG_WARN << "could not open logfile " << path.string() << " for writing"; return ""; } bool ok = std::fwrite(text.data(), 1, text.size(), f) == text.size(); if (std::fclose(f) != 0) ok = false; if (!ok) { LOG_WARN << "failed to write logfile " << path.string(); return ""; } // Bound this kind of report. The prefix is whatever precedes the first '_', // matching timestampedLogName()'s "_.log". Pruning AFTER the // write means the file just created is the newest and is always retained. const size_t us = name.find('_'); if (us != std::string::npos && us > 0) pruneLogFiles(name.substr(0, us), keep); return path.string(); } } // namespace fgc::paths