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®
// The machine as the driver sees it.
//
// - Shared files: an atomic write never leaves half a file and gives the
// temp file the mode asked for; LockedUpdate re-reads under the lock, keeps
// keys it does not know, falls back on a damaged file, and serialises
// concurrent writers — including another process holding flock(2) on the
// same side file, as every driver on the machine does.
// - /proc: the start time read after the last ')' (a command name may hold
// spaces and parentheses), a zombie reading as gone, MemInfo in MB.
// - Identity: natively a driver's own pid and start time; in the sandbox the
// instance's host pid from bwrapinfo.json with the start time of /proc/1.
// On a fake /proc and a fake instance directory, because the suite itself
// runs inside `flatpak build` in the Flatpak.
// - The lifetime lock: held while its driver lives, refused to a second
// driver under the same name, gone when it ends, and a stale one reads as
// dead.
// - The host XDG directories coming back in the sandbox.
# include <sys/stat.h>
# include <stdlib.h>
# include <unistd.h>
import std ;
import Crafter . Review ;
namespace fs = std : : filesystem ;
using namespace Crafter : : Review ;
using namespace std : : chrono_literals ;
namespace {
std : : int32_t Failures = 0 ;
void Check ( bool cond , std : : string_view msg ) {
if ( ! cond ) {
std : : println ( std : : cerr , " FAIL: {} " , msg ) ;
+ + Failures ;
}
}
void Write ( const fs : : path & path , std : : string_view text ) {
fs : : create_directories ( path . parent_path ( ) ) ;
std : : ofstream ( path , std : : ios : : binary ) < < text ;
}
// A /proc/<pid>/stat whose command name is designed to trip a naive split.
std : : string Stat ( std : : int64_t pid , char state , std : : int64_t start ) {
std : : string fields = std : : format ( " {} (we (ird) name) {} 1 " , pid , state ) ;
for ( std : : int32_t field = 5 ; field < = 21 ; + + field ) fields + = " 0 " ;
return std : : format ( " {} {} 0 0 \n " , fields , start ) ;
}
}
int main ( ) {
const fs : : path dir = fs : : temp_directory_path ( ) / std : : format ( " crafter-review-platform-{} " , getpid ( ) ) ;
fs : : remove_all ( dir ) ;
fs : : create_directories ( dir ) ;
Platform : : SetMode ( Platform : : Mode : : Native ) ;
{
const mode_t previous = umask ( 022 ) ;
const fs : : path file = dir / " atomic.json " ;
Platform : : WriteAtomic ( file , " { \" a \" : 1} " , 0600 ) ;
struct stat info { } ;
stat ( file . c_str ( ) , & info ) ;
Check ( Platform : : ReadFile ( file ) = = " { \" a \" : 1} " , " the atomic write lands " ) ;
Check ( ( info . st_mode & 0777 ) = = 0600 , std : : format ( " with the mode asked for ({:o}) " , info . st_mode & 0777 ) ) ;
Check ( ! fs : : exists ( std : : format ( " {}.{}.tmp " , file . string ( ) , Platform : : PublishedPid ( ) ) ) , " and leaves no temp file " ) ;
umask ( previous ) ;
}
{
const fs : : path file = dir / " board.json " ;
Write ( file , " { \" version \" : 1, \" cost7d \" : 3.5, \" seats \" : {}} " ) ;
const Json seen = Platform : : LockedUpdate ( file , [ ] ( Json & data ) {
data [ " seats " ] [ " local " ] = { { " id " , " local " } } ;
return true ;
} , Json : : object ( ) , 0600 , { } ) ;
const Json back = Json : : parse ( * Platform : : ReadFile ( file ) ) ;
Check ( back [ " cost7d " ] = = 3.5 & & back [ " seats " ] [ " local " ] [ " id " ] = = " local " , " a key this driver does not know survives the rewrite " ) ;
Check ( * Platform : : ReadFile ( file ) = = DumpFile ( back ) , " written in DumpFile's layout " ) ;
Check ( seen = = back , " LockedUpdate returns what the change saw " ) ;
Write ( file , " {not json " ) ;
const Json fallback = { { " version " , 1 } } ;
Platform : : LockedUpdate ( file , [ ] ( Json & data ) {
return data . contains ( " version " ) ;
} , fallback , 0600 , { } ) ;
Check ( Json : : parse ( * Platform : : ReadFile ( file ) ) = = fallback , " a damaged file is replaced by the fallback the change kept " ) ;
const std : : string before = * Platform : : ReadFile ( file ) ;
Platform : : LockedUpdate ( file , [ ] ( Json & data ) {
data [ " ignored " ] = true ;
return false ;
} , Json : : object ( ) , 0600 , { } ) ;
Check ( * Platform : : ReadFile ( file ) = = before , " a change that returns false leaves the file alone " ) ;
}
{
// Two writers in one process: flock is per open file description, so
// two FileLocks exclude each other just as two processes would.
const fs : : path file = dir / " counter.json " ;
Write ( file , " { \" n \" : 0} " ) ;
auto bump = [ & file ] {
for ( std : : int32_t i = 0 ; i < 100 ; + + i ) {
Platform : : LockedUpdate ( file , [ ] ( Json & data ) {
data [ " n " ] = data [ " n " ] . get < std : : int64_t > ( ) + 1 ;
return true ;
} , Json : : object ( ) , 0600 , { } ) ;
}
} ;
std : : thread a ( bump ) ;
std : : thread b ( bump ) ;
a . join ( ) ;
b . join ( ) ;
Check ( Json : : parse ( * Platform : : ReadFile ( file ) ) [ " n " ] = = 200 , " concurrent read-modify-writes lose nothing " ) ;
}
{
// Another process holds flock on the side file for a moment (flock(1),
// util-linux): the lock here must wait for it, as every driver on the
// machine shares these files.
const fs : : path file = dir / " shared.json " ;
const fs : : path lock = std : : format ( " {}.lock " , file . string ( ) ) ;
const fs : : path ready = dir / " other-holds " ;
try {
const Process : : Child other = Process : : Start ( { . argv = { " flock " , lock . string ( ) , " sh " , " -c " , " touch \" $1 \" && sleep 0.4 " , " sh " , ready . string ( ) } } ) ;
for ( std : : int32_t i = 0 ; i < 100 & & ! fs : : exists ( ready ) ; + + i ) std : : this_thread : : sleep_for ( 20 ms ) ;
const auto start = std : : chrono : : steady_clock : : now ( ) ;
{
const Platform : : FileLock ours ( lock ) ;
Check ( ours . Held ( ) , " the lock is taken once the other process lets go " ) ;
}
const auto waited = std : : chrono : : steady_clock : : now ( ) - start ;
Process : : Wait ( other . pid ) ;
Check ( waited > = 200 ms , std : : format ( " and not before: flock(2) excludes the other process's flock ({} ms) " , std : : chrono : : duration_cast < std : : chrono : : milliseconds > ( waited ) . count ( ) ) ) ;
} catch ( const std : : runtime_error & ) {
std : : println ( " SKIP no flock here to hold the other end of the lock " ) ;
}
}
{
const fs : : path proc = dir / " proc " ;
Write ( proc / " 4242 " / " stat " , Stat ( 4242 , ' S ' , 987654 ) ) ;
Write ( proc / " 4243 " / " stat " , Stat ( 4243 , ' Z ' , 5 ) ) ;
Write ( proc / " sys " / " kernel " / " random " / " boot_id " , " 0123456789abcdef-the-rest \n " ) ;
Write ( dir / " meminfo " , " MemTotal: 65536000 kB \n MemAvailable: 32768000 kB \n HugePages_Total: 0 \n " ) ;
Platform : : SetPaths ( { . proc = proc , . flatpakInfo = dir / " no-flatpak-info " , . runtimeDir = dir / " run " } ) ;
Check ( Platform : : ProcStart ( 4242 ) = = 987654 , " the start time is field 22, read after the last ')' " ) ;
Check ( ! Platform : : ProcStart ( 4243 ) . has_value ( ) , " a zombie reads as gone " ) ;
Check ( ! Platform : : ProcStart ( 4244 ) . has_value ( ) , " so does a pid that is not there " ) ;
Check ( Platform : : BootId ( ) = = " 01234567 " , " boot_id's first eight characters " ) ;
const auto memory = Platform : : MemInfo ( dir / " meminfo " ) ;
Check ( memory . at ( " MemTotal " ) = = 64000 & & memory . at ( " MemAvailable " ) = = 32000 & & memory . at ( " HugePages_Total " ) = = 0 , " meminfo in MB, unitless fields as they are " ) ;
Write ( proc / std : : to_string ( getpid ( ) ) / " stat " , Stat ( getpid ( ) , ' R ' , 111 ) ) ;
Platform : : SetMode ( Platform : : Mode : : Native ) ;
const std : : optional < Platform : : Identity > native = Platform : : Self ( ) ;
Check ( native & & native - > pid = = getpid ( ) & & native - > start = = 111 & & native - > Key ( ) = = std : : format ( " {}:111 " , getpid ( ) ) , " natively a driver is its own pid and start time " ) ;
Write ( dir / " flatpak-info " , " [Application] \n name=net.catcrafts.CrafterReview \n \n [Instance] \n instance-id=2509661707 \n app-path=/x \n " ) ;
Write ( dir / " run " / " .flatpak " / " 2509661707 " / " bwrapinfo.json " , " { \n \" child-pid \" : 416559, \n \" pid-namespace \" : 4026536671 \n } \n " ) ;
Write ( proc / " 1 " / " stat " , Stat ( 1 , ' S ' , 841601 ) ) ;
Platform : : SetPaths ( { . proc = proc , . flatpakInfo = dir / " flatpak-info " , . runtimeDir = dir / " run " } ) ;
Platform : : SetMode ( Platform : : Mode : : Sandbox ) ;
const std : : optional < Platform : : Identity > sandboxed = Platform : : Self ( ) ;
Check ( sandboxed & & sandboxed - > Key ( ) = = " 416559:841601 " , " in the sandbox: the instance's host pid and /proc/1's start time " ) ;
fs : : remove ( dir / " run " / " .flatpak " / " 2509661707 " / " bwrapinfo.json " ) ;
Check ( ! Platform : : Self ( ) . has_value ( ) , " no bwrapinfo.json (flatpak build): no identity a host driver could check " ) ;
Platform : : SetMode ( Platform : : Mode : : Native ) ;
}
{
const fs : : path drivers = dir / " drivers " ;
const Platform : : Identity me { . pid = 416559 , . start = 841601 } ;
{
const Platform : : LifetimeLock lock ( drivers , me ) ;
Check ( fs : : exists ( drivers / " 416559-841601.lock " ) , " the lifetime lock is named <pid>-<start>.lock " ) ;
Check ( Platform : : ProbeLifetimeLock ( drivers , me . pid , me . start ) = = true , " a probe finds it held " ) ;
bool refused = false ;
try {
const Platform : : LifetimeLock second ( drivers , me ) ;
} catch ( const std : : runtime_error & ) {
refused = true ;
}
Check ( refused , " a second driver under the same name is refused " ) ;
const std : : vector < fs : : directory_entry > entries ( fs : : directory_iterator ( drivers ) , { } ) ;
Check ( entries . size ( ) = = 1 , " and leaves no temp file behind " ) ;
}
Check ( ! Platform : : ProbeLifetimeLock ( drivers , me . pid , me . start ) . has_value ( ) , " a driver that ended leaves no lock file " ) ;
Write ( Platform : : LifetimeLockFile ( drivers , 7 , 8 ) , " " ) ;
Check ( Platform : : ProbeLifetimeLock ( drivers , 7 , 8 ) = = false , " a lock file nobody holds is a dead driver's " ) ;
Check ( ! Platform : : ProbeLifetimeLock ( drivers , 9 , 9 ) . has_value ( ) , " no lock file: a driver that takes none " ) ;
}
{
const std : : string home = Platform : : Home ( ) ;
setenv ( " XDG_CONFIG_HOME " , " /home/x/.var/app/net.catcrafts.CrafterReview/config " , 1 ) ;
setenv ( " XDG_CACHE_HOME " , " /home/x/.var/app/net.catcrafts.CrafterReview/cache " , 1 ) ;
setenv ( " HOST_XDG_CONFIG_HOME " , " /elsewhere/config " , 1 ) ;
unsetenv ( " HOST_XDG_CACHE_HOME " ) ;
Platform : : RestoreHostXdg ( ) ;
Check ( Platform : : GetEnv ( " XDG_CONFIG_HOME " ) = = " /elsewhere/config " , " a host XDG directory the host set comes back " ) ;
Check ( Platform : : GetEnv ( " XDG_CACHE_HOME " ) = = Dyn : : JoinPath ( home , " .cache " ) , " one it left unset is the XDG default under $HOME " ) ;
Check ( Platform : : GetEnv ( " XDG_STATE_HOME " ) = = Dyn : : JoinPath ( home , " .local/state " ) , " state too " ) ;
2026-10-31 16:09:40 +01:00
// The shell: the sandbox's /bin/sh becomes bash; a bash or zsh that is
// there stays.
const std : : optional < std : : string > shell = Platform : : GetEnv ( " SHELL " ) ;
setenv ( " SHELL " , " /bin/sh " , 1 ) ;
Platform : : RestoreShell ( ) ;
const std : : optional < std : : string > fromSh = Platform : : GetEnv ( " SHELL " ) ;
setenv ( " SHELL " , " /no/such/zsh " , 1 ) ;
Platform : : RestoreShell ( ) ;
const std : : optional < std : : string > fromMissing = Platform : : GetEnv ( " SHELL " ) ;
setenv ( " SHELL " , " /bin/bash " , 1 ) ;
Platform : : RestoreShell ( ) ;
const std : : optional < std : : string > fromBash = Platform : : GetEnv ( " SHELL " ) ;
if ( shell ) setenv ( " SHELL " , shell - > c_str ( ) , 1 ) ;
Check ( ! fs : : exists ( " /bin/bash " ) | | ( fromSh = = " /bin/bash " & & fromMissing = = " /bin/bash " & & fromBash = = " /bin/bash " ) ,
" SHELL: /bin/sh and a shell that is not there become /bin/bash; bash stays " ) ;
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
}
{
const std : : string today = Platform : : TodayIso ( ) ;
Check ( today . size ( ) = = 10 & & today [ 4 ] = = ' - ' & & today [ 7 ] = = ' - ' , " date.today().isoformat() " ) ;
Check ( Platform : : LocalTime ( " %H:%M:%S " , 0 ) . size ( ) = = 8 , " strftime in local time " ) ;
Check ( Platform : : SelfExe ( ) . is_absolute ( ) , " the running binary's path " ) ;
}
fs : : remove_all ( dir ) ;
return Failures = = 0 ? 0 : 1 ;
}