// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:Platform; import std; import :Json; // The machine as the driver sees it: whether it runs inside the Flatpak's // sandbox, its environment, the files it shares with other drivers and how // they are locked, /proc, and the identity it is known by on the seat board. // // The sandbox is decided once, by Init(). Everything that reads /proc or the // sandbox's instance files goes through Paths(), which a test points at a // fake tree: flatpak-builder runs the suite inside `flatpak build`, where // /.flatpak-info exists, and a test that read the real files would test the // build sandbox instead of the code. export namespace Crafter::Review::Platform { enum class Mode { Native, // Inside a Flatpak: its own PID namespace, XDG directories under // ~/.var/app, a private /tmp unless the manifest shares the host's. Sandbox, }; struct SystemPaths { std::filesystem::path proc = "/proc"; std::filesystem::path flatpakInfo = "/.flatpak-info"; // $XDG_RUNTIME_DIR, where the instance's bwrapinfo.json is mounted: // /run/user//.flatpak//bwrapinfo.json. std::filesystem::path runtimeDir; }; // Decides the mode from /.flatpak-info and, in the sandbox, restores the // host's XDG directories (RestoreHostXdg) before any thread starts. void Init(); Mode CurrentMode(); bool InSandbox(); // For tests: pin the mode and the paths the identity code reads. void SetMode(Mode mode); const SystemPaths& Paths(); void SetPaths(SystemPaths paths); // ---- environment ----------------------------------------------------------- std::optional GetEnv(std::string_view name); // The process environment. std::map Environment(); // $HOME, else the password database's. std::string Home(); // Flatpak points XDG_{CONFIG,CACHE,DATA,STATE}_HOME at ~/.var/app//…; // they go back to the host's (HOST_XDG_*, else the XDG defaults under // $HOME), so git reads ~/.config/git/config, Playwright finds // ~/.cache/ms-playwright, and claude and npx behave as on the host. void RestoreHostXdg(); // The absolute path of the running binary (/proc/self/exe). std::filesystem::path SelfExe(); // ---- time ---------------------------------------------------------------------- // time.time() double Now(); // time.strftime(format) in local time. std::string LocalTime(std::string_view format); // time.strftime(format, time.localtime(epochSeconds)) std::string LocalTime(std::string_view format, double epochSeconds); // date.today().isoformat() std::string TodayIso(); // ---- files ------------------------------------------------------------------------ // The file's bytes, or nullopt when it cannot be read. std::optional ReadFile(const std::filesystem::path& path); // `content` into `..tmp`, created with `mode` (the umask // applies), then renamed over `path`: a reader never sees half a file, and // two writers never share a temp name. The pid is PublishedPid(), which in // the sandbox is a host pid, where every driver would otherwise be pid 2. // Directories are created as needed. void WriteAtomic(const std::filesystem::path& path, std::string_view content, std::uint32_t mode = 0644); // `path` and its missing parents: the parents made as mkdir makes them, // the last directory with `mode`, both less the umask; a directory that is // there keeps its mode. Throws with the errno text ("[Errno 13] Permission // denied: '/x'"). void MakeDirs(const std::filesystem::path& path, std::uint32_t mode = 0777); // An exclusive flock(2) on `lockPath`, held while the object lives. flock, // not fcntl or OFD locks: every driver on the machine takes flock on the // same side files, and on Linux the two kinds do not exclude each other. A // missing lock file is created 0666 less the umask. class FileLock { public: explicit FileLock(const std::filesystem::path& lockPath); ~FileLock(); FileLock(const FileLock&) = delete; FileLock& operator=(const FileLock&) = delete; // False when the lock file could not be opened or locked; the caller // carries on best-effort. bool Held() const; // Why not, when it is not held. const std::string& Problem() const; private: std::int32_t fd = -1; std::string problem; }; // The one shape every file shared between drivers is written in (the // board, the ledger, memory.json, unit-state.json): lock `.lock`, // re-read the file, let `change` edit it in place — a false return leaves // the file as it is — write it back atomically with `mode`, release. // A file that does not parse as a JSON object hands `change` a copy of // `fallback`. Without the lock the write is still made, best-effort; // `warn` hears about that and about a write that failed. Returns what // `change` saw. Json LockedUpdate(const std::filesystem::path& path, const std::function& change, const Json& fallback, std::uint32_t mode, const std::function& warn); // ---- /proc ---------------------------------------------------------------------------- // The start time of a live process, in clock ticks since boot (field 22 // of /proc//stat, read after the last ')' because the command name // may hold spaces and parentheses); nullopt for no such process or a // zombie. Under Paths().proc. std::optional ProcStart(std::int64_t pid); // The first 8 characters of /proc/sys/kernel/random/boot_id, "" when // unreadable. std::string BootId(); // /proc/meminfo as {field: MB}; a field without a kB unit keeps its value. std::map MemInfo(const std::filesystem::path& path = "/proc/meminfo"); // {ppid: [pids]} of every process now, from one read of each // /proc//stat. Always the real /proc: a process tree is this // driver's own business. std::map> PpidMap(); // The pid and every descendant in `kids`. std::vector Tree(std::int64_t pid, const std::map>& kids); // The summed Pss of the processes, in MB; one that is gone counts nothing. double PssMb(std::span pids); // ---- identity ------------------------------------------------------------------------- // How the other drivers on this machine know a driver: `:`. struct Identity { std::int64_t pid = 0; std::int64_t start = 0; std::string Key() const; }; // This process as every driver on the machine can check it. Natively its // own pid and start time. In the sandbox its pid means nothing outside // (every Flatpak driver is pid 2), so it is the instance's: the host pid // of the sandbox's init (`child-pid` in bwrapinfo.json) and the start time // of /proc/1 inside, which is the same process — a driver outside that // checks the pid then sees it alive exactly as long as the sandbox runs. // nullopt in a sandbox without bwrapinfo.json (`flatpak build`), after // waiting up to 2 s for flatpak to write it. std::optional Self(); // The pid to put in temp names and lock information: Self()'s, else // getpid(). std::int64_t PublishedPid(); // The lifetime lock of a driver: an exclusive flock on // `/-.lock`, held as long as the driver lives. It is // created under a temporary name, locked, then renamed into place, so a // prober never finds the file unlocked while its owner lives. Another // driver tells a live one from a dead one by it across sandboxes, where // pids are not comparable. class LifetimeLock { public: // Throws std::runtime_error when the name is already held by a live // driver: a second full run in one sandbox instance. LifetimeLock(const std::filesystem::path& dir, const Identity& identity); ~LifetimeLock(); LifetimeLock(const LifetimeLock&) = delete; LifetimeLock& operator=(const LifetimeLock&) = delete; const std::filesystem::path& File() const; private: std::int32_t fd = -1; std::filesystem::path file; }; std::filesystem::path LifetimeLockFile(const std::filesystem::path& dir, std::int64_t pid, std::int64_t start); // true: a live driver holds the lock; false: the file is there and nobody // holds it (its driver died); nullopt: there is no lock file (a driver // that takes none). std::optional ProbeLifetimeLock(const std::filesystem::path& dir, std::int64_t pid, std::int64_t start); }