Crafter.Review/interfaces/Crafter.Review-Platform.cppm
Jorijn van der Graaf c63c898833
feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a
finished pass, the child's credentials, the OpenRouter seats, and the
board every driver on the machine shares, in one shape and under one
lock. A driver is alive while its lifetime lock is held; with no lock file
natively its pid and start time decide, and in the sandbox it is taken for
alive.

:Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is
HttpError, every other failure carries a fixed text. :Platform gains
MakeDirs, with the last directory's own mode, and errno texts for its
writes and locks.

The tests cover the seat pool and several drivers at once, less the checks
that drive the pool's Scheduler, Admission and Stacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:15:03 +01:00

180 lines
8.9 KiB
C++

// 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/<uid>/.flatpak/<instance-id>/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<std::string> GetEnv(std::string_view name);
// The process environment.
std::map<std::string, std::string> Environment();
// $HOME, else the password database's.
std::string Home();
// Flatpak points XDG_{CONFIG,CACHE,DATA,STATE}_HOME at ~/.var/app/<id>/…;
// 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<std::string> ReadFile(const std::filesystem::path& path);
// `content` into `<path>.<pid>.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 `<path>.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<bool(Json&)>& change, const Json& fallback,
std::uint32_t mode, const std::function<void(const std::string&)>& warn);
// ---- /proc ----------------------------------------------------------------------------
// The start time of a live process, in clock ticks since boot (field 22
// of /proc/<pid>/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<std::int64_t> 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<std::string, std::int64_t> MemInfo(const std::filesystem::path& path = "/proc/meminfo");
// {ppid: [pids]} of every process now, from one read of each
// /proc/<pid>/stat. Always the real /proc: a process tree is this
// driver's own business.
std::map<std::int64_t, std::vector<std::int64_t>> PpidMap();
// The pid and every descendant in `kids`.
std::vector<std::int64_t> Tree(std::int64_t pid, const std::map<std::int64_t, std::vector<std::int64_t>>& kids);
// The summed Pss of the processes, in MB; one that is gone counts nothing.
double PssMb(std::span<const std::int64_t> pids);
// ---- identity -------------------------------------------------------------------------
// How the other drivers on this machine know a driver: `<pid>:<start>`.
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<Identity> 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
// `<dir>/<pid>-<start>.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<bool> ProbeLifetimeLock(const std::filesystem::path& dir, std::int64_t pid, std::int64_t start);
}