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 ) ) ;
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 ) ;
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 ) {
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 ;
}
}