The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
8.6 KiB
C++
174 lines
8.6 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);
|
|
|
|
// 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);
|
|
}
|