make.tcl bootsupport re-vendor: punkcheck 0.6.0 (G-095 lock protocol live for all writers)
Per the G-095 note, the advisory protocol is only effective once all
writers carry it - bootsupport re-vendored immediately after landing.
Verification under 0.6.0: 'make.tcl modules' + full 'make.tcl project'
exit 0 with zero lock timeouts, zero tripwire warnings, no spurious
recopies, all kit installs skip 'no change detected', and no
.punkcheck.lock / .punkcheck.tmp.* leftovers anywhere in the tree.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
dict set held $lockfile [dict create channel $fd refcount 1]
return $lockfile
}
#contended - identify the holder (best effort: the file can vanish between the
#failed EXCL open and this read)
set holderinfo ""
catch {
set rfd [open $lockfile r]
set holderinfo [string trim [read $rfd]]
close $rfd
}
set holder_pid ""
set holder_host ""
if {[llength $holderinfo] % 2 == 0} {
set holder_pid [dict_getwithdefault $holderinfo pid ""]
set holder_host [dict_getwithdefault $holderinfo host ""]
}
set broke 0
#liveness stale-break: holder recorded on this host and provably dead (capability-gated)
if {[string is integer -strict $holder_pid] && $holder_host eq [info hostname] && $holder_pid != [pid]} {
if {[pid_liveness $holder_pid] eq "dead"} {
catch {puts $warnchannel "punkcheck::lock warning: breaking stale lock '$lockfile' - holder pid $holder_pid on this host is no longer running (holder: $holderinfo)"}
catch {file delete -- $lockfile}
set broke 1
}
}
if {!$broke} {
#age-based stale-break: the portable baseline
if {[catch {file mtime $lockfile} lockmtime]} {
#lockfile vanished since the failed open - retry immediately
set refcount [expr {[dict get $held $token refcount] - 1}]
if {$refcount > 0} {
dict set held $token refcount $refcount
return 1
}
catch {close [dict get $held $token channel]}
catch {file delete -- $token}
dict unset held $token
return 1
}
#alive | dead | unknown for a pid on this host (capability-gated liveness)
proc pid_liveness {pid} {
if {"windows" eq $::tcl_platform(platform)} {
if {![catch {package require twapi}]} {
if {[catch {twapi::process_exists $pid} exists]} {
return unknown
}
return [expr {$exists ? "alive" : "dead"}]
}
return unknown
} else {
if {![catch {exec kill -0 $pid}]} {
return alive
} else {
#kill -0 errors for both nonexistent (No such process) and not-permitted (alive,
#other user) - only the former is a confident 'dead'
catch {exec kill -0 $pid} errkill
if {[string match -nocase "*no such process*" $errkill]} {
return dead
}
return unknown
}
}
}
}
#Concurrent multi-process writers of one .punkcheck file are G-095's scope - the single save chokepoint
#(installtrack method save_working_recordset) is the attachment point for its locking/atomic-rename/merge measures.
#Concurrent multi-process writer safety (G-095) lives at the single save chokepoint (installtrack
#method save_working_recordset) and its callers: saves are write-temp-then-atomic-rename, whole
#installer events hold the advisory .punkcheck.lock (punkcheck::lock, start_event through end),
#deferred flushes merge own records into freshly-loaded file state, and an optimistic
#mtime/size tripwire warns when a non-protocol writer touched the file between load and save.
#an installtrack object represents an installation path from sourceroot to targetroot
#The source and target folders should be as specific as possible but it is valid to specify for example c:/ -> c:/ (or / -> /) if source and targets within the installation operation are spread around.
#G-095: unattended runs get a clean actionable error instead of a prompt
#that would block on open stdin or mis-answer on closed/piped stdin
set errmsg "punkcheck installtrack sanity check failed: punkcheck file '$o_checkfile' contains multiple INSTALLER records with -name '$insane'."
append errmsg \n "Likely cause: concurrent writers racing the file (writers predating the G-095 lock protocol) or an interrupted run."
append errmsg \n "Session is non-interactive - not prompting. Recovery: inspect the file, or delete it and rerun."
append errmsg \n "Deleting loses install history and checksums (unnecessary recopies next run); for synced-targets consumers (e.g bin/ deploys) existing targets then skip until manually reconciled, since skipped records never store target cksums."
error $errmsg
}
set msg "Sanity check: punkcheck file '$o_checkfile' contains multiple records for INSTALLER -name '$insane'."
append msg \n "This may indicate a problem such as multiple concurrent installtrack instances using the same punkcheck file,"
append msg \n " or a previous installtrack instance that did not complete properly."
append msg \n " Do you want to DELETE the .punkcheck file?"
append msg \n " It is safe to delete .punkcheck files, at the cost of loss of history and checksums used to optimize installs."
append msg \n " They are a record of installation events and checksums used to avoid unnecessary reinstalls."
append msg \n " Caveat: for synced-targets consumers (e.g bin/ deploys) deletion wedges existing targets into skip-until-manually-reconciled - skipped records never store the target cksums that mode compares against."
append msg \n " If not confirmed, an error will be raised - likely aborting the current operation."
append msg \n "confirm deletion and continue by regenerating the file, by typing the 3 letters: 'yes'."
set answer [punk::lib::askuser $msg]
@ -651,7 +922,11 @@ namespace eval punkcheck {
}
destructor {
#puts "[self] destructor called"
#release any lock still held (an abandoned event, or an error path that skipped end)
if {$o_event_lock ne ""} {
catch {punkcheck::lock::release $o_event_lock}
set o_event_lock ""
}
}
method test {} {
return [self]
@ -685,6 +960,14 @@ namespace eval punkcheck {
#review/fix to allow multiple installtrack objects on same punkcheck file.
method load_all_records {} {
set o_record_list [punkcheck::load_records_from_file $o_checkfile]
set o_load_sig [my Checkfile_signature]
}
#file signature for the optimistic tripwire ({} when the file doesn't exist yet)
#sync this installer's own record (from the live event objects) into the working recordset,
#prune unreferenced events beyond -keep_events, and persist via the chokepoint.
#This is the event-scoped persistence point for deferred events.
set transient 0
if {$o_event_lock eq ""} {
set o_event_lock [punkcheck::lock::acquire $o_checkfile -installer $o_name]
set transient 1
}
try {
set current_sig [my Checkfile_signature]
if {$o_load_sig ne {} && $current_sig ne {} && $current_sig ne $o_load_sig} {
catch {puts stderr "punkcheck warning: '$o_checkfile' changed on disk since this installer last loaded it (advisory lock protocol bypassed by another writer?) - saving anyway (trigger: $trigger installer: $o_name)"}
}
set saveresult [punkcheck::save_records_to_file $o_record_list $o_checkfile $trigger $o_debugchannel]
set o_load_sig [my Checkfile_signature]
return $saveresult
} finally {
if {$transient} {
punkcheck::lock::release $o_event_lock
set o_event_lock ""
}
}
}
#sync this installer's own records into freshly-loaded file state and persist via the
#chokepoint - the event-scoped persistence point for deferred events.
#Merge-on-flush (G-095): the file is re-loaded under the lock and only this installer's
#own records are carried over - the INSTALLER record by -name and touched FILEINFO
#records by -targets - so records other processes wrote since our load are preserved
#instead of being wholesale-overwritten.
method flush {{trigger flush}} {
set transient 0
if {$o_event_lock eq ""} {
set o_event_lock [punkcheck::lock::acquire $o_checkfile -installer $o_name]
set transient 1
}
try {
#fresh file state under the lock
set base [punkcheck::load_records_from_file $o_checkfile]