From 64fa9a63a7490372cf7a37739927d4a93a781edb Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Tue, 7 Jul 2026 00:43:41 +1000 Subject: [PATCH] add goal G-028: name the process locking a file on build deploy failures Motivated by a make.tcl project deploy failure ("could not delete target binary") where the locker turned out to be 7-Zip inspecting the exe - identifying it took several dead ends because Windows has no cheap file-to-process reverse lookup (name matching and Process.Modules scans both miss plain file handles; openfiles needs a pre-enabled global flag; handle.exe is external and elevation-bound). G-028: a punkboot::utils helper returns the locking processes via the Windows Restart Manager API (the installers-grade answer to "who is blocking this file"), with twapi or cffi required lazily at call time - punkboot::utils stays pure Tcl per the bootsupport no-compiled-extensions contract, and unavailability degrades to the existing message. make.tcl kit deploy failures append the locker report when determinable. Recurring customer: G-023 versioned/dev binaries rewritten every build. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- GOALS.md | 6 ++ goals/G-028-file-locker-identification.md | 67 +++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 goals/G-028-file-locker-identification.md diff --git a/GOALS.md b/GOALS.md index 3ec0c078..32dfab57 100644 --- a/GOALS.md +++ b/GOALS.md @@ -210,3 +210,9 @@ Scope: src/modules/punk/mix/commandset/project-999999.0a1.0.tm (project.new push Detail: goals/G-027-derived-project-pull-updates.md Goal: a project generated from a punkshell layout can pull infrastructure updates (make.tcl/build.tcl, bootsupport modules and libs, layout template payloads) from its originating punkshell project by running one command inside the derived project - replacing the current push model (`dev project.new -force 1 -update 1` run from punkshell) - with install provenance robust to derived-project workdir moves (not local relative paths alone), VCS-state awareness on both ends, and a recorded decision on pulling from remote sources. Acceptance: one documented command run inside a derived project updates its punkshell-derived infrastructure from the source punkshell project; the update still works after the derived project's working directory has been moved (proven by moving a scratch derived project and pulling); VCS integration on both ends: the pull applies the G-026 clean-checkout policy to the punkshell source, and reports (or refuses per option) when target files it would overwrite carry uncommitted local modifications in the derived project's git/fossil checkout; .punkcheck records remain the provenance basis (updates are recorded and unchanged targets skipped, as with existing punkcheck-tracked installs); the push flow keeps working until explicitly retired; the remote-pull question (updating from a remote punkshell repository rather than a local checkout) has a recorded design decision - implemented, or deferred with rationale in the detail file. + +### G-028 [proposed] Name the process locking a file when builds cannot replace a target +Scope: src/modules/punkboot/utils-999999.0a1.0.tm (locker-report helper), src/make.tcl (kit deploy failure reporting) +Detail: goals/G-028-file-locker-identification.md +Goal: when a build cannot delete/overwrite a target file (typically a built executable held open by another program), the failure message names the locking process(es) - via a punkboot::utils helper using the Windows Restart Manager API through optionally-available twapi or cffi, degrading cleanly to the current message when the API or bindings are unavailable or on other platforms. +Acceptance: on Windows with twapi or cffi loadable, a punkboot::utils proc given a file path returns the locking processes (at least pid and process name; empty list when unlocked); punkboot::utils itself stays pure Tcl - the binary binding is required lazily at call time and its absence yields a clean 'unavailable' result, not an error (bootsupport must not gain a compiled-extension dependency); make.tcl kit deploy failures include the locker report when determinable (e.g. "could not delete target binary ... in use by: 7zFM.exe (pid 1234)"); non-Windows platforms and binding-less environments produce the existing message unchanged; verified against a deliberately held handle (documented manual verification acceptable). diff --git a/goals/G-028-file-locker-identification.md b/goals/G-028-file-locker-identification.md new file mode 100644 index 00000000..71e16dcd --- /dev/null +++ b/goals/G-028-file-locker-identification.md @@ -0,0 +1,67 @@ +# G-028 Name the process locking a file when builds cannot replace a target + +Status: proposed +Scope: src/modules/punkboot/utils-999999.0a1.0.tm (locker-report helper), src/make.tcl (kit deploy failure reporting) +Acceptance: as in root GOALS.md index (canonical). + +## Context + +Motivating incident (2026-07-07, tomlish project): `make.tcl project` failed +with "could not delete target binary at .../bin/tomlish.exe" and no further +information. The lock turned out to be 7-Zip holding the exe open for archive +inspection - but identifying that took several dead ends, because Windows has +no cheap reverse lookup from file to holding process: + +- tasklist / Get-Process by name only finds processes *running as* the file. +- Scanning Process.Modules only sees files mapped as images (loaded EXE/DLLs), + not plain open handles, and silently skips inaccessible processes. +- Full handle enumeration (NtQuerySystemInformation) needs elevation and + per-handle queries; `openfiles` requires a globally pre-enabled tracking + flag; Sysinternals handle.exe / Resource Monitor are external/manual. + +The **Restart Manager API** (RmStartSession/RmRegisterResources/RmGetList) is +the practical programmatic answer: it is what installers use for the "these +applications are using this file" dialog, does not require elevation for the +common cases, and directly answers "who is blocking my replace/delete". It is +callable from Tcl via twapi or cffi, both already shipped in the punk Windows +kit. + +Deploy-target locks will recur - G-023's versioned/dev binary outputs are +rewritten every build, and a running or inspected punk8/9-dev.exe is exactly +the kind of file this fails on. + +## Approach + +- `punkboot::utils` helper (e.g. file_lockers ) returning a list of + {pid name ...} records, empty when unlocked. +- **Constraint: bootsupport carries no compiled extensions** (src/bootsupport + AGENTS.md). punkboot::utils stays pure Tcl; twapi (RmStartSession et al via + its API coverage) or cffi (direct Rstrtmgr.dll binding) is package-required + lazily inside the helper, from the environment's auto_path. Unavailable + binding, non-Windows platform, or API failure all yield a clean + 'unavailable' result the caller renders as the existing message. +- make.tcl kit deploy (and any other replace-target failure sites worth + covering) appends the locker report to its failure output when available. +- Prefer twapi if its Restart Manager coverage suffices; cffi binding as the + fallback implementation choice - record the choice here when made. + +## Alternatives considered + +- Full handle-table enumeration (NtQuerySystemInformation) - rejected: + elevation-dependent, expensive, and reimplements what Restart Manager + already provides for the blocking-file case. +- Shelling out to Sysinternals handle.exe - rejected: external download, + license prompt on first run, elevation required; not dependable build + tooling. +- `openfiles` - rejected: requires a pre-enabled global tracking flag with a + standing performance cost. + +## Notes + +- Related: G-023 (versioned binary deploys are the recurring customer), + G-020 (same optionally-available twapi/cffi pattern for platform features), + punkboot::utils chicken-and-egg ordering rules in src/bootsupport/AGENTS.md + apply when adding the helper. +- Unix analogues (lsof/fuser) could slot behind the same helper interface + later; out of scope for acceptance (Windows is where the pain is and where + the API answer is non-obvious).