feat(platform): processes that never outlive their pass, shared files, the board identity

:Process makes sure no child outlives what it was started for. SIGKILL to
the direct child alone would leave the Playwright server and Chromium
running after a timeout, and every pass running after a SIGKILLed driver:

- every child is forked by one long-lived spawner thread, the reaper's
  registry locked across fork, so PR_SET_PDEATHSIG holds and a fast exit
  is never reaped unregistered; one reaper thread owns waitpid(-1);
- the child gets an empty signal mask, SIGPIPE and SIGXFSZ at their
  defaults, and nothing but its three streams (close_range); the driver
  ignores SIGPIPE and takes SIGINT/SIGTERM/SIGHUP on a thread of its own;
- `crafter-review __supervise <argv>` is a pass's subreaper. It kills the
  pass's whole tree, Chromium's detached process group included, when the
  target exits, on SIGTERM, and when the driver dies, by its death signal.
- Run() captures both outputs without the deadlock.

The shared state the detached threads wait on is never destroyed: a
condition variable destroyed under a waiter makes glibc's
pthread_cond_destroy, and with it exit(), wait forever.

:Platform: the Flatpak sandbox decided once, the host's XDG directories
restored there; WriteAtomic and LockedUpdate, the read-modify-write of
the board, ledger, memory.json and unit-state.json every driver on the
machine shares, under flock(2) on the same side files; /proc readers; and
the identity other drivers can check: natively pid and start time, in the
sandbox the instance's host pid from bwrapinfo.json with /proc/1's start
time. The lifetime lock is created under a temporary name and renamed into
place, so no prober ever finds it unlocked while its driver lives.

Tests: Process (the three ways a pass's tree must die, return codes, a
missing program, capture, timeouts, fds, the child's signal state),
Platform (atomic writes, LockedUpdate against a damaged file, two writers,
and another process holding flock on the side file; /proc on a fake
tree; both identities; the lifetime lock; the XDG restore).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 05:49:46 +01:00
commit f94d895701
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
8 changed files with 1611 additions and 3 deletions

View file

@ -0,0 +1,415 @@
// 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("{}: {}", temp.string(), Dyn::ErrnoText(errno)));
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(std::format("{}: {}", temp.string(), 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("{}: {}", path.string(), Dyn::ErrnoText(failure)));
}
}
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, 0644);
if (fd < 0) {
problem = Dyn::ErrnoText(errno);
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 = Json::parse(*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;
}
}

View file

@ -0,0 +1,473 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <fcntl.h>
#include <linux/close_range.h>
#include <poll.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
module Crafter.Review:Process_impl;
import :Process;
import :Platform;
import :Text;
import std;
namespace fs = std::filesystem;
namespace Crafter::Review::Process {
namespace {
// ---- the reaper's registry and the spawner's queue ------------------------
struct Entry {
std::optional<std::int32_t> status;
};
struct Request;
struct Shared {
std::mutex registryMutex;
std::condition_variable registryChanged;
std::map<pid_t, Entry> registry;
std::int32_t unreaped = 0;
std::mutex spawnMutex;
std::condition_variable spawnReady;
std::deque<Request*> spawnQueue;
};
// Never destroyed. The reaper and the spawner are detached threads
// that wait on these until the process ends, and a condition variable
// destroyed under a waiter blocks exit(): glibc's pthread_cond_destroy
// waits for its waiters, which here never come back.
Shared& State() {
static Shared* state = new Shared;
return *state;
}
std::int32_t ReturnCode(std::int32_t status) {
if (WIFEXITED(status)) return WEXITSTATUS(status);
if (WIFSIGNALED(status)) return -WTERMSIG(status);
return -1;
}
void ReaperLoop() {
while (true) {
std::int32_t status = 0;
const pid_t pid = waitpid(-1, &status, 0);
if (pid > 0) {
const std::lock_guard lock(State().registryMutex);
// A pid nobody registered is an orphan of one of our
// children, reparented to us as the subreaper: reaped,
// and that is all.
if (const auto found = State().registry.find(pid); found != State().registry.end() && !found->second.status) {
found->second.status = ReturnCode(status);
--State().unreaped;
State().registryChanged.notify_all();
}
continue;
}
if (errno == EINTR) continue;
// ECHILD: no child at all. Wait for Start() to register one;
// wake now and then for orphans reparented in the meantime.
std::unique_lock lock(State().registryMutex);
State().registryChanged.wait_for(lock, std::chrono::seconds(1), [] { return State().unreaped > 0; });
}
}
// ---- the spawner -------------------------------------------------------------
struct Request {
const Spec* spec;
std::promise<Child> result;
};
std::once_flag ThreadsStarted;
std::optional<std::string> FindOnPath(const std::string& name, const std::string& path) {
if (name.contains('/')) return name;
for (const std::string& dir : Dyn::Split(path, ":")) {
const std::string candidate = Dyn::JoinPath(dir.empty() ? std::string(".") : dir, name);
if (access(candidate.c_str(), X_OK) == 0) return candidate;
}
return std::nullopt;
}
std::int32_t OpenStream(const Stream& stream, bool reading, std::int32_t& ours) {
switch (stream.kind) {
case Stream::Kind::Inherit:
return -1;
case Stream::Kind::Null:
return open("/dev/null", (reading ? O_RDONLY : O_WRONLY) | O_CLOEXEC);
case Stream::Kind::File:
return reading ? open(stream.file.c_str(), O_RDONLY | O_CLOEXEC)
: open(stream.file.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
case Stream::Kind::Pipe: {
std::array<std::int32_t, 2> ends {};
if (pipe2(ends.data(), O_CLOEXEC) != 0) return -2;
ours = reading ? ends[1] : ends[0];
return reading ? ends[0] : ends[1];
}
}
return -1;
}
Child DoSpawn(const Spec& spec) {
if (spec.argv.empty()) throw std::runtime_error("Start: no argv");
// Everything the child needs is made here: between fork and exec
// it may only make async-signal-safe calls.
std::vector<std::string> envStrings;
std::string pathVariable = "/usr/local/bin:/usr/bin:/bin";
if (spec.environment) {
for (const auto& [name, value] : *spec.environment) envStrings.push_back(std::format("{}={}", name, value));
if (const auto found = spec.environment->find("PATH"); found != spec.environment->end()) pathVariable = found->second;
} else {
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) envStrings.emplace_back(*entry);
if (const std::optional<std::string> path = Platform::GetEnv("PATH")) pathVariable = *path;
}
const std::optional<std::string> program = FindOnPath(spec.argv.front(), pathVariable);
if (!program) {
throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(ENOENT), Dyn::Repr(spec.argv.front())));
}
std::vector<char*> argv;
for (const std::string& arg : spec.argv) argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(nullptr);
std::vector<char*> envp;
for (const std::string& entry : envStrings) envp.push_back(const_cast<char*>(entry.c_str()));
envp.push_back(nullptr);
const std::string cwd = spec.cwd ? spec.cwd->string() : std::string();
Child child;
const std::int32_t in = OpenStream(spec.input, true, child.input);
const std::int32_t out = OpenStream(spec.output, false, child.output);
const std::int32_t err = OpenStream(spec.error, false, child.error);
auto closeAll = [&] {
for (const std::int32_t fd : { in, out, err, child.input, child.output, child.error }) {
if (fd >= 0) close(fd);
}
};
if (in == -2 || out == -2 || err == -2 || (spec.input.kind != Stream::Kind::Inherit && in < 0)
|| (spec.output.kind != Stream::Kind::Inherit && out < 0) || (spec.error.kind != Stream::Kind::Inherit && err < 0)) {
const std::int32_t failure = errno;
closeAll();
throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(failure), Dyn::Repr(spec.argv.front())));
}
std::array<std::int32_t, 2> failurePipe {};
if (pipe2(failurePipe.data(), O_CLOEXEC) != 0) {
closeAll();
throw std::runtime_error(Dyn::ErrnoText(errno));
}
sigset_t empty;
sigemptyset(&empty);
const pid_t parent = getpid();
const std::int32_t deathSignal = spec.deathSignal;
std::unique_lock registry(State().registryMutex);
const pid_t pid = fork();
if (pid == 0) {
struct sigaction defaults {};
defaults.sa_handler = SIG_DFL;
sigaction(SIGPIPE, &defaults, nullptr);
sigaction(SIGXFSZ, &defaults, nullptr);
sigprocmask(SIG_SETMASK, &empty, nullptr);
prctl(PR_SET_PDEATHSIG, deathSignal);
if (getppid() != parent) _exit(127);
if (in >= 0) dup2(in, 0);
if (out >= 0) dup2(out, 1);
if (err >= 0) dup2(err, 2);
std::int32_t failure = 0;
if (!cwd.empty() && chdir(cwd.c_str()) != 0) {
failure = errno;
} else {
// Nothing but the three streams reaches the program: a lock
// or a socket of the driver's held open by a child would
// outlive the driver.
syscall(SYS_close_range, 3U, ~0U, CLOSE_RANGE_CLOEXEC);
execve(program->c_str(), argv.data(), envp.data());
failure = errno;
}
(void)!write(failurePipe[1], &failure, sizeof(failure));
_exit(127);
}
if (pid < 0) {
const std::int32_t failure = errno;
registry.unlock();
closeAll();
close(failurePipe[0]);
close(failurePipe[1]);
throw std::runtime_error(std::format("fork: {}", Dyn::ErrnoText(failure)));
}
State().registry[pid] = {};
++State().unreaped;
registry.unlock();
State().registryChanged.notify_all();
for (const std::int32_t fd : { in, out, err }) {
if (fd >= 0) close(fd);
}
close(failurePipe[1]);
std::int32_t failure = 0;
ssize_t got = 0;
do {
got = read(failurePipe[0], &failure, sizeof(failure));
} while (got < 0 && errno == EINTR);
close(failurePipe[0]);
if (got == static_cast<ssize_t>(sizeof(failure))) {
Wait(pid);
for (const std::int32_t fd : { child.input, child.output, child.error }) {
if (fd >= 0) close(fd);
}
throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(failure), Dyn::Repr(spec.argv.front())));
}
child.pid = pid;
return child;
}
void SpawnerLoop() {
while (true) {
Request* request = nullptr;
{
std::unique_lock lock(State().spawnMutex);
State().spawnReady.wait(lock, [] { return !State().spawnQueue.empty(); });
request = State().spawnQueue.front();
State().spawnQueue.pop_front();
}
try {
request->result.set_value(DoSpawn(*request->spec));
} catch (...) {
request->result.set_exception(std::current_exception());
}
}
}
void EnsureThreads() {
std::call_once(ThreadsStarted, [] {
std::thread(ReaperLoop).detach();
std::thread(SpawnerLoop).detach();
});
}
}
Child Start(const Spec& spec) {
EnsureThreads();
Request request { .spec = &spec };
std::future<Child> result = request.result.get_future();
{
const std::lock_guard lock(State().spawnMutex);
State().spawnQueue.push_back(&request);
}
State().spawnReady.notify_one();
return result.get();
}
std::optional<std::int32_t> Wait(std::int64_t pid, std::optional<std::chrono::milliseconds> timeout) {
std::unique_lock lock(State().registryMutex);
auto exited = [pid] {
const auto found = State().registry.find(static_cast<pid_t>(pid));
return found == State().registry.end() || found->second.status.has_value();
};
if (timeout) {
if (!State().registryChanged.wait_for(lock, *timeout, exited)) return std::nullopt;
} else {
State().registryChanged.wait(lock, exited);
}
const auto found = State().registry.find(static_cast<pid_t>(pid));
if (found == State().registry.end()) return -1;
const std::int32_t code = *found->second.status;
State().registry.erase(found);
return code;
}
void Signal(std::int64_t pid, std::int32_t signal) {
if (pid > 0) kill(static_cast<pid_t>(pid), signal);
}
void KillTree(std::int64_t root, bool includeRoot) {
std::set<std::int64_t> stopped;
for (std::int32_t round = 0; round < 50; ++round) {
const auto kids = Platform::PpidMap();
bool added = false;
for (const std::int64_t pid : Platform::Tree(root, kids)) {
if (pid == root && !includeRoot) continue;
if (pid <= 1) continue;
if (stopped.insert(pid).second) {
kill(static_cast<pid_t>(pid), SIGSTOP);
added = true;
}
}
if (!added) break;
}
for (const std::int64_t pid : stopped) kill(static_cast<pid_t>(pid), SIGKILL);
}
void KillDescendants() {
KillTree(getpid(), false);
}
Captured Run(const std::vector<std::string>& argv, const std::optional<fs::path>& cwd,
std::optional<std::chrono::milliseconds> timeout, std::string_view input,
const std::optional<std::map<std::string, std::string>>& environment) {
Spec spec { .argv = argv, .environment = environment, .cwd = cwd };
spec.input.kind = input.empty() ? Stream::Kind::Null : Stream::Kind::Pipe;
spec.output.kind = Stream::Kind::Pipe;
spec.error.kind = Stream::Kind::Pipe;
Child child = Start(spec);
Captured captured;
const auto deadline = timeout ? std::optional(std::chrono::steady_clock::now() + *timeout) : std::nullopt;
std::size_t sent = 0;
if (child.input >= 0) fcntl(child.input, F_SETFL, fcntl(child.input, F_GETFL) | O_NONBLOCK);
std::array<char, 65536> buffer {};
while (child.output >= 0 || child.error >= 0 || child.input >= 0) {
std::vector<pollfd> watched;
if (child.input >= 0) watched.push_back({ .fd = child.input, .events = POLLOUT, .revents = 0 });
if (child.output >= 0) watched.push_back({ .fd = child.output, .events = POLLIN, .revents = 0 });
if (child.error >= 0) watched.push_back({ .fd = child.error, .events = POLLIN, .revents = 0 });
std::int32_t waitMs = -1;
if (deadline) {
const auto left = std::chrono::duration_cast<std::chrono::milliseconds>(*deadline - std::chrono::steady_clock::now()).count();
if (left <= 0) {
Signal(child.pid, SIGKILL);
captured.timedOut = true;
break;
}
waitMs = static_cast<std::int32_t>(std::min<std::int64_t>(left, 1000));
}
const std::int32_t ready = poll(watched.data(), watched.size(), waitMs);
if (ready < 0 && errno != EINTR) break;
for (const pollfd& entry : watched) {
if (entry.revents == 0) continue;
if (entry.fd == child.input) {
if (entry.revents & (POLLERR | POLLHUP)) {
close(child.input);
child.input = -1;
continue;
}
const ssize_t wrote = write(child.input, input.data() + sent, input.size() - sent);
if (wrote > 0) sent += static_cast<std::size_t>(wrote);
if (sent >= input.size() || (wrote < 0 && errno != EAGAIN && errno != EINTR)) {
close(child.input);
child.input = -1;
}
continue;
}
const ssize_t got = read(entry.fd, buffer.data(), buffer.size());
if (got > 0) {
(entry.fd == child.output ? captured.out : captured.err).append(buffer.data(), static_cast<std::size_t>(got));
} else if (got == 0 || (errno != EINTR && errno != EAGAIN)) {
close(entry.fd);
(entry.fd == child.output ? child.output : child.error) = -1;
}
}
}
for (const std::int32_t fd : { child.input, child.output, child.error }) {
if (fd >= 0) close(fd);
}
captured.returnCode = Wait(child.pid).value_or(-1);
return captured;
}
void InstallSignalThread(std::function<void(std::int32_t)> onSignal) {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGHUP);
pthread_sigmask(SIG_BLOCK, &set, nullptr);
struct sigaction ignore {};
ignore.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &ignore, nullptr);
std::thread([set, onSignal = std::move(onSignal)] {
while (true) {
std::int32_t signal = 0;
if (sigwait(&set, &signal) == 0) onSignal(signal);
}
}).detach();
}
void BecomeSubreaper() {
prctl(PR_SET_CHILD_SUBREAPER, 1);
}
std::vector<std::string> Supervised(std::span<const std::string> argv) {
std::vector<std::string> out { Platform::SelfExe().string(), std::string(SuperviseVerb) };
out.insert(out.end(), argv.begin(), argv.end());
return out;
}
std::int32_t SupervisedReturnCode(std::int32_t supervisorCode) {
if (supervisorCode > 128 && supervisorCode <= 128 + 64) return -(supervisorCode - 128);
return supervisorCode;
}
std::int32_t SuperviseMain(std::span<const std::string_view> args) {
if (args.empty()) {
std::println(std::cerr, "{}: nothing to run", SuperviseVerb);
return 64;
}
prctl(PR_SET_CHILD_SUBREAPER, 1);
sigset_t watched;
sigemptyset(&watched);
sigaddset(&watched, SIGTERM);
sigaddset(&watched, SIGINT);
sigaddset(&watched, SIGHUP);
sigaddset(&watched, SIGCHLD);
sigprocmask(SIG_BLOCK, &watched, nullptr);
sigset_t empty;
sigemptyset(&empty);
std::vector<std::string> strings(args.begin(), args.end());
std::vector<char*> argv;
for (const std::string& arg : strings) argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(nullptr);
const std::optional<std::string> program = FindOnPath(strings.front(), Platform::GetEnv("PATH").value_or("/usr/bin:/bin"));
if (!program) {
std::println(std::cerr, "{}: {}", Dyn::ErrnoText(ENOENT), Dyn::Repr(strings.front()));
return 127;
}
const pid_t self = getpid();
const pid_t target = fork();
if (target == 0) {
struct sigaction defaults {};
defaults.sa_handler = SIG_DFL;
sigaction(SIGPIPE, &defaults, nullptr);
sigaction(SIGXFSZ, &defaults, nullptr);
sigprocmask(SIG_SETMASK, &empty, nullptr);
prctl(PR_SET_PDEATHSIG, SIGKILL);
if (getppid() != self) _exit(127);
execv(program->c_str(), argv.data());
_exit(127);
}
if (target < 0) return 127;
std::optional<std::int32_t> status;
while (!status) {
siginfo_t info {};
const std::int32_t signal = sigwaitinfo(&watched, &info);
if (signal < 0) continue;
if (signal == SIGCHLD) {
while (true) {
std::int32_t raw = 0;
const pid_t pid = waitpid(-1, &raw, WNOHANG);
if (pid <= 0) break;
if (pid == target) status = raw;
}
continue;
}
// A timeout, an abort, or the driver gone: the whole tree goes,
// the target with it, and its status is what that left.
KillTree(self, false);
std::int32_t raw = 0;
while (waitpid(target, &raw, 0) < 0 && errno == EINTR) {}
status = raw;
}
// The target is gone; whatever it started goes too, and every orphan
// that was reparented to this subreaper is reaped.
KillTree(self, false);
while (true) {
const pid_t pid = waitpid(-1, nullptr, 0);
if (pid < 0 && errno == EINTR) continue;
if (pid < 0) break;
}
if (WIFEXITED(*status)) return WEXITSTATUS(*status);
if (WIFSIGNALED(*status)) return 128 + WTERMSIG(*status);
return 1;
}
}