Crafter.Review/implementations/Crafter.Review-Platform.cpp
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

429 lines
17 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <fcntl.h>
#include <pwd.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <ctime>
module Crafter.Review:Platform_impl;
import :Platform;
import :Json;
import :Text;
import :Crypto;
import std;
namespace fs = std::filesystem;
namespace Crafter::Review::Platform {
namespace {
Mode CurrentModeValue = Mode::Native;
SystemPaths& PathsValue() {
static SystemPaths paths = [] {
SystemPaths defaults;
if (const char* runtime = std::getenv("XDG_RUNTIME_DIR"); runtime && *runtime) {
defaults.runtimeDir = runtime;
} else {
defaults.runtimeDir = std::format("/run/user/{}", getuid());
}
return defaults;
}();
return paths;
}
// [section] key=value, as GLib's key files are written.
std::optional<std::string> KeyFileValue(std::string_view text, std::string_view section, std::string_view key) {
bool inSection = false;
for (const std::string& line : Dyn::SplitLines(text)) {
const std::string_view view(line);
if (view.starts_with('[')) {
inSection = view == std::format("[{}]", section);
continue;
}
if (!inSection) continue;
const std::size_t equals = view.find('=');
if (equals == std::string_view::npos) continue;
if (Dyn::Strip(view.substr(0, equals)) == key) return Dyn::Strip(view.substr(equals + 1));
}
return std::nullopt;
}
std::optional<std::int64_t> ToInt(std::string_view text) {
std::int64_t value = 0;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
if (error != std::errc() || end != text.data() + text.size()) return std::nullopt;
return value;
}
}
void Init() {
CurrentModeValue = fs::exists(PathsValue().flatpakInfo) ? Mode::Sandbox : Mode::Native;
if (CurrentModeValue == Mode::Sandbox) RestoreHostXdg();
}
Mode CurrentMode() {
return CurrentModeValue;
}
bool InSandbox() {
return CurrentModeValue == Mode::Sandbox;
}
void SetMode(Mode mode) {
CurrentModeValue = mode;
}
const SystemPaths& Paths() {
return PathsValue();
}
void SetPaths(SystemPaths paths) {
PathsValue() = std::move(paths);
}
std::optional<std::string> GetEnv(std::string_view name) {
const std::string key(name);
const char* value = std::getenv(key.c_str());
if (value == nullptr) return std::nullopt;
return std::string(value);
}
std::map<std::string, std::string> Environment() {
std::map<std::string, std::string> out;
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
const std::string_view text(*entry);
const std::size_t equals = text.find('=');
if (equals == std::string_view::npos) continue;
out.emplace(std::string(text.substr(0, equals)), std::string(text.substr(equals + 1)));
}
return out;
}
std::string Home() {
if (const std::optional<std::string> home = GetEnv("HOME"); home && !home->empty()) return *home;
if (const passwd* entry = getpwuid(getuid()); entry != nullptr && entry->pw_dir != nullptr) return entry->pw_dir;
return "/";
}
void RestoreHostXdg() {
const std::string home = Home();
const std::array<std::pair<std::string_view, std::string_view>, 4> defaults = { {
{ "CONFIG", ".config" },
{ "CACHE", ".cache" },
{ "DATA", ".local/share" },
{ "STATE", ".local/state" },
} };
for (const auto& [kind, relative] : defaults) {
const std::string name = std::format("XDG_{}_HOME", kind);
const std::optional<std::string> host = GetEnv(std::format("HOST_XDG_{}_HOME", kind));
const std::string value = host && !host->empty() ? *host : Dyn::JoinPath(home, relative);
setenv(name.c_str(), value.c_str(), 1);
}
}
fs::path SelfExe() {
std::error_code error;
const fs::path exe = fs::read_symlink("/proc/self/exe", error);
return error ? fs::path() : exe;
}
double Now() {
const auto since = std::chrono::system_clock::now().time_since_epoch();
return std::chrono::duration<double>(since).count();
}
std::string LocalTime(std::string_view format, double epochSeconds) {
const std::time_t seconds = static_cast<std::time_t>(std::floor(epochSeconds));
std::tm local {};
localtime_r(&seconds, &local);
std::array<char, 128> buffer {};
const std::string pattern(format);
const std::size_t length = std::strftime(buffer.data(), buffer.size(), pattern.c_str(), &local);
return std::string(buffer.data(), length);
}
std::string LocalTime(std::string_view format) {
return LocalTime(format, Now());
}
std::string TodayIso() {
return LocalTime("%Y-%m-%d");
}
std::optional<std::string> ReadFile(const fs::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) return std::nullopt;
std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (in.bad()) return std::nullopt;
return bytes;
}
void WriteAtomic(const fs::path& path, std::string_view content, std::uint32_t mode) {
if (path.has_parent_path()) fs::create_directories(path.parent_path());
const fs::path temp = std::format("{}.{}.tmp", path.string(), PublishedPid());
const std::int32_t fd = open(temp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, static_cast<mode_t>(mode));
if (fd < 0) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(temp.string())));
std::size_t written = 0;
while (written < content.size()) {
const ssize_t step = write(fd, content.data() + written, content.size() - written);
if (step < 0) {
if (errno == EINTR) continue;
const std::int32_t failure = errno;
close(fd);
unlink(temp.c_str());
throw std::runtime_error(Dyn::ErrnoText(failure));
}
written += static_cast<std::size_t>(step);
}
close(fd);
if (rename(temp.c_str(), path.c_str()) != 0) {
const std::int32_t failure = errno;
unlink(temp.c_str());
throw std::runtime_error(std::format("{}: {} -> {}", Dyn::ErrnoText(failure), Dyn::Repr(temp.string()), Dyn::Repr(path.string())));
}
}
void MakeDirs(const fs::path& path, std::uint32_t mode) {
const fs::path name = path.filename().empty() && path.has_parent_path() ? path.parent_path() : path;
const fs::path head = name.parent_path();
std::error_code error;
if (!head.empty() && head != name && !fs::exists(head, error)) {
MakeDirs(head, 0777);
if (name.filename() == ".") return;
}
if (mkdir(name.c_str(), static_cast<mode_t>(mode)) != 0) {
const std::int32_t failure = errno;
if (!fs::is_directory(name, error)) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(failure), Dyn::Repr(name.string())));
}
}
FileLock::FileLock(const fs::path& lockPath) {
std::error_code error;
if (lockPath.has_parent_path()) fs::create_directories(lockPath.parent_path(), error);
fd = open(lockPath.c_str(), O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0666);
if (fd < 0) {
problem = std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(lockPath.string()));
return;
}
while (flock(fd, LOCK_EX) != 0) {
if (errno == EINTR) continue;
problem = Dyn::ErrnoText(errno);
close(fd);
fd = -1;
return;
}
}
FileLock::~FileLock() {
if (fd >= 0) {
flock(fd, LOCK_UN);
close(fd);
}
}
bool FileLock::Held() const {
return fd >= 0;
}
const std::string& FileLock::Problem() const {
return problem;
}
Json LockedUpdate(const fs::path& path, const std::function<bool(Json&)>& change, const Json& fallback,
std::uint32_t mode, const std::function<void(const std::string&)>& warn) {
const FileLock lock(std::format("{}.lock", path.string()));
if (!lock.Held() && warn) {
warn(std::format("{}: no lock ({}) - best-effort write", path.filename().string(), lock.Problem()));
}
Json data;
try {
if (const std::optional<std::string> text = ReadFile(path)) data = ParseLoose(*text);
} catch (const std::exception&) {
data = Json();
}
if (!data.is_object()) data = fallback.is_object() ? fallback : Json::object();
if (change(data)) {
try {
WriteAtomic(path, DumpFile(data), mode);
} catch (const std::exception& e) {
if (warn) warn(std::format("{}: not saved ({})", path.filename().string(), e.what()));
}
}
return data;
}
std::optional<std::int64_t> ProcStart(std::int64_t pid) {
const std::optional<std::string> stat = ReadFile(Paths().proc / std::to_string(pid) / "stat");
if (!stat) return std::nullopt;
const std::size_t close = stat->rfind(')');
if (close == std::string::npos) return std::nullopt;
const std::vector<std::string> rest = Dyn::Split(std::string_view(*stat).substr(close + 1));
if (rest.size() < 20) return std::nullopt;
if (rest[0] == "Z" || rest[0] == "X") return std::nullopt;
return ToInt(rest[19]);
}
std::string BootId() {
const std::optional<std::string> text = ReadFile(Paths().proc / "sys" / "kernel" / "random" / "boot_id");
if (!text) return "";
return Dyn::Slice(Dyn::Strip(*text), 0, 8);
}
std::map<std::string, std::int64_t> MemInfo(const fs::path& path) {
std::map<std::string, std::int64_t> out;
const std::optional<std::string> text = ReadFile(path);
if (!text) return out;
for (const std::string& line : Dyn::SplitLines(*text)) {
const std::size_t colon = line.find(':');
if (colon == std::string::npos) continue;
const std::vector<std::string> parts = Dyn::Split(std::string_view(line).substr(colon + 1));
if (parts.empty()) continue;
const std::optional<std::int64_t> value = ToInt(parts[0]);
if (!value) continue;
out[line.substr(0, colon)] = parts.size() > 1 && parts[1] == "kB" ? *value / 1024 : *value;
}
return out;
}
std::map<std::int64_t, std::vector<std::int64_t>> PpidMap() {
std::map<std::int64_t, std::vector<std::int64_t>> out;
std::error_code error;
for (const fs::directory_entry& entry : fs::directory_iterator("/proc", error)) {
const std::string name = entry.path().filename().string();
const std::optional<std::int64_t> pid = ToInt(name);
if (!pid) continue;
const std::optional<std::string> stat = ReadFile(entry.path() / "stat");
if (!stat) continue;
const std::size_t close = stat->rfind(')');
if (close == std::string::npos) continue;
const std::vector<std::string> rest = Dyn::Split(std::string_view(*stat).substr(close + 1));
if (rest.size() < 2) continue;
if (const std::optional<std::int64_t> ppid = ToInt(rest[1])) out[*ppid].push_back(*pid);
}
return out;
}
std::vector<std::int64_t> Tree(std::int64_t pid, const std::map<std::int64_t, std::vector<std::int64_t>>& kids) {
std::vector<std::int64_t> out;
std::vector<std::int64_t> todo { pid };
std::set<std::int64_t> seen;
while (!todo.empty()) {
const std::int64_t next = todo.back();
todo.pop_back();
if (!seen.insert(next).second) continue;
out.push_back(next);
if (const auto found = kids.find(next); found != kids.end()) todo.insert(todo.end(), found->second.begin(), found->second.end());
}
return out;
}
double PssMb(std::span<const std::int64_t> pids) {
std::int64_t kb = 0;
for (const std::int64_t pid : pids) {
const std::optional<std::string> text = ReadFile(std::format("/proc/{}/smaps_rollup", pid));
if (!text) continue;
for (const std::string& line : Dyn::SplitLines(*text)) {
if (!line.starts_with("Pss:")) continue;
const std::vector<std::string> parts = Dyn::Split(line);
if (parts.size() > 1) {
if (const std::optional<std::int64_t> value = ToInt(parts[1])) kb += *value;
}
break;
}
}
return static_cast<double>(kb) / 1024;
}
std::string Identity::Key() const {
return std::format("{}:{}", pid, start);
}
std::optional<Identity> Self() {
if (!InSandbox()) {
const std::int64_t pid = getpid();
return Identity { .pid = pid, .start = ProcStart(pid).value_or(0) };
}
const std::optional<std::string> info = ReadFile(Paths().flatpakInfo);
const std::optional<std::string> instance = info ? KeyFileValue(*info, "Instance", "instance-id") : std::nullopt;
if (!instance) return std::nullopt;
const fs::path bwrapinfo = Paths().runtimeDir / ".flatpak" / *instance / "bwrapinfo.json";
// flatpak writes the file once bwrap reports the child's pid, which
// can be a moment after the application is already running.
for (std::int32_t attempt = 0; attempt < 20; ++attempt) {
if (const std::optional<std::string> text = ReadFile(bwrapinfo)) {
try {
const Json parsed = Json::parse(*text);
if (parsed.contains("child-pid") && parsed["child-pid"].is_number_integer()) {
const std::optional<std::int64_t> start = ProcStart(1);
if (start) return Identity { .pid = parsed["child-pid"].get<std::int64_t>(), .start = *start };
}
} catch (const std::exception&) {
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return std::nullopt;
}
std::int64_t PublishedPid() {
static const std::int64_t pid = [] {
if (const std::optional<Identity> self = Self()) return self->pid;
return static_cast<std::int64_t>(getpid());
}();
return pid;
}
fs::path LifetimeLockFile(const fs::path& dir, std::int64_t pid, std::int64_t start) {
return dir / std::format("{}-{}.lock", pid, start);
}
LifetimeLock::LifetimeLock(const fs::path& dir, const Identity& identity) : file(LifetimeLockFile(dir, identity.pid, identity.start)) {
fs::create_directories(dir);
if (ProbeLifetimeLock(dir, identity.pid, identity.start) == true) {
throw std::runtime_error(std::format("another driver already runs as {} (its lock {} is held): start each driver with its own `flatpak run`", identity.Key(), file.string()));
}
const fs::path temp = dir / std::format(".{}-{}.{}.tmp", identity.pid, identity.start, TokenHex(4));
fd = open(temp.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
if (fd < 0) throw std::runtime_error(std::format("{}: {}", temp.string(), Dyn::ErrnoText(errno)));
if (flock(fd, LOCK_EX | LOCK_NB) != 0 || rename(temp.c_str(), file.c_str()) != 0) {
const std::int32_t failure = errno;
close(fd);
fd = -1;
unlink(temp.c_str());
throw std::runtime_error(std::format("{}: {}", file.string(), Dyn::ErrnoText(failure)));
}
}
LifetimeLock::~LifetimeLock() {
if (fd < 0) return;
// Unlinked while still held: a prober that opens the file after this
// finds none, which every rule reads as "this driver is gone or never
// took a lock" and the board's own leave() has already removed it.
unlink(file.c_str());
flock(fd, LOCK_UN);
close(fd);
}
const fs::path& LifetimeLock::File() const {
return file;
}
std::optional<bool> ProbeLifetimeLock(const fs::path& dir, std::int64_t pid, std::int64_t start) {
const fs::path file = LifetimeLockFile(dir, pid, start);
const std::int32_t fd = open(file.c_str(), O_RDONLY | O_CLOEXEC);
if (fd < 0) return std::nullopt;
// Shared: two probers looking at one dead driver both see it dead,
// where an exclusive probe would show each the other's lock.
const bool free = flock(fd, LOCK_SH | LOCK_NB) == 0;
if (free) flock(fd, LOCK_UN);
close(fd);
return !free;
}
}