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>
2026-10-31 05:49:46 +01:00
// 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 ) ) ;
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
if ( fd < 0 ) throw std : : runtime_error ( std : : format ( " {}: {} " , Dyn : : ErrnoText ( errno ) , Dyn : : Repr ( temp . string ( ) ) ) ) ;
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>
2026-10-31 05:49:46 +01:00
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 ( ) ) ;
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
throw std : : runtime_error ( Dyn : : ErrnoText ( failure ) ) ;
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>
2026-10-31 05:49:46 +01:00
}
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 ( ) ) ;
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
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 ( ) ) ) ) ;
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>
2026-10-31 05:49:46 +01:00
}
}
FileLock : : FileLock ( const fs : : path & lockPath ) {
std : : error_code error ;
if ( lockPath . has_parent_path ( ) ) fs : : create_directories ( lockPath . parent_path ( ) , error ) ;
feat(pipeline): the pure pipeline, checked against its goldens
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>
2026-10-31 06:48:55 +01:00
fd = open ( lockPath . c_str ( ) , O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC , 0666 ) ;
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>
2026-10-31 05:49:46 +01:00
if ( fd < 0 ) {
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
problem = std : : format ( " {}: {} " , Dyn : : ErrnoText ( errno ) , Dyn : : Repr ( lockPath . string ( ) ) ) ;
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>
2026-10-31 05:49:46 +01:00
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 {
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
if ( const std : : optional < std : : string > text = ReadFile ( path ) ) data = ParseLoose ( * text ) ;
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>
2026-10-31 05:49:46 +01:00
} 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 ;
}
}