Repository navigation
feat(service): checked Windows stop primitives and operations, unwired (#7771 slice 1) - #8564
Conversation
…wired Adds the library half of #7771 option B as tested code that nothing calls yet, so behavior is unchanged on every platform: - windows-process-native: x64 FFI from the trusted system directory for the process creation time (GetProcessTimes), a handle terminate that compares the creation time on the same handle it terminates, a bind-time exit watch, and the Task Scheduler service PID (QueryServiceStatusEx). - windows-process-evidence: the CIM snapshot with creation times (native times kept only when they agree with CIM to the ms), an sc.exe queryex fallback, and the bounded ARM64 PowerShell terminate (StartTime check within 1 s, Kill() on the same object, exit read back). - windows-stop-binding: the 030 binding rule as a pure function. - windows-stop-budget: the caps and the admission arithmetic, including the 0.5 s closing port sample part B can take past its window (48.5 / 56.5 / 42.5 s). - windows-checked-stop: part A with every effect injected, admission after the registration probe, and the authorizer-gated variant for the updater. - windows-stop-confirm: part B (6 s hold, final enumeration, ours vs foreign vs exited-owner listener) and the port resolution order. Contract in structure/ops/windows-checked-stop.md; slice plan and the points where this differs from 030 in devlog/_plan/261111_windows_service_stop/060. Refs #7771
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
✅ Deterministic PR hygiene checks passed. |
Walkthrough
Pre-merge checks:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c4456bda7f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const exitWaitMs = Math.max(0, Math.min(FALLBACK_TERMINATE_EXIT_WAIT_MS, options.timeoutMs - 500)); | ||
| try { | ||
| return parseFallbackTerminateOutput(await run({ | ||
| program: "powershell", | ||
| args: powershellArgs(windowsFallbackTerminateScript(pid, boundStartTime, exitWaitMs)), |
There was a problem hiding this comment.
Normalize deadline-derived timeouts before spawning the fallback
When the spawned terminate fallback is used, Part A passes windowEnd - clock.now() through runCapped, so the real performance.now() clock normally produces a fractional timeout. The default runner forwards that directly to execFile, which rejects non-integer timeouts before spawning PowerShell. With less than 1.5 seconds remaining, the computed exitWaitMs also becomes fractional and fails windowsFallbackTerminateScript's integer validation; passing timeoutMs: 1250.5 reproduces unavailable without invoking even an injected runner. Thus the fallback can leave bound wrappers running despite having time available. Floor the positive remaining timeout and derived exit wait to integer milliseconds before constructing the script and invoking the runner.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid; being fixed in this PR: positive remaining timeouts and derived exit waits are floored to integer milliseconds before the fallback script is built and the runner is called, with a regression test.
A cap derived from the deadline (windowEnd - now) is fractional on the real clock. execFile and windowsFallbackTerminateScript take whole milliseconds, so the spawned fallback answered unavailable. Floor the timeout and the derived exit wait, and spawn nothing when it floors to 0, which execFile would read as no timeout. Refs #7771
One test per fail-closed branch, each shown to fail when its rule is mutated: an unreadable parent PID never binds and, with the task registered, is unreadable; the child-ancestor lineage clause binds on its own; a terminate overrun, an unreadable task or WinSW state, an unreadable stray candidate and a denied bind-time watch end Part A as unknown or denied; an unnamed image carrying this home's path is unknown in Part B. Refs #7771
ARM64 ships the strict variant in slice 2 and its adapter must gate it, because terminateWindowsProcessBound falls back to PowerShell on its own. Record the Schedule PID read before admission, that openWatch and the authorizer are not capped, and the route's need for an outer race. Refs #7771
…hecked Windows stop Refs #7771
|
Maintainer judgment (#7771 Option B, slice 1: checked Windows stop primitives)
|
Summary
Slice 1 of 3 for #7771 Option B, the owner-approved design in
devlog/_plan/261111_windows_service_stop/(030 is the contract). It adds the checked Windows stop as tested library code. Nothing calls it yet, so behavior is unchanged on every platform; slice 2 wires the callers, and slice 3 (Desktop, #8560) is merged.src/service/windows-process-native.tsresolveTrustedWindowsSystemDirectory()under the #8487 rules. It reads a process creation time (GetProcessTimes). The terminate opens the process, reads the creation time on that same handle, refuses a mismatch asreused, then callsTerminateProcesson the same handle and closes it infinally. Also an exit watch opened at bind time and the Schedule service PID (QueryServiceStatusEx). Returns null on ARM64 and whenever a test simulates the platform.src/service/windows-process-evidence.tsCreationDate. On x64 a native creation time is kept only when it matches CIM to the millisecond, so a PID reused between the two reads counts as unreadable. Also a cappedsc.exe queryex Schedulefallback, and the ARM64 PowerShell terminate (StartTime within 1 s, thenKill()on the same object).src/service/windows-stop-binding.tswscript.exe,cscript.exeandcmd.execarrying this home's launcher or service-script path as a whole token, bound by lineage (ancestor of the identity-checked child, child of a bound wrapper, parent gone only while this home's task is registered, or the Schedule service host), with a creation time. The proxy child is never a target. Unreadable candidates make the resultunknown; they are never skipped or killed.src/service/windows-stop-budget.tssrc/service/windows-checked-stop.tsstopWindowsServiceChecked. Every effect is injected; every step is capped except the exit-watch open and the updater authorizer, which run inside the shared terminate window (recorded in 060). Admission runs after the registration probe and returnsunverified-budget, changing nothing, when the remaining deadline cannot hold the rest. Outcomes:absent/stopped/survived/denied/unknown, with a reason and the bound set.guardedStopWindowsServiceChecked(for the updater) re-authorizes before each terminate.src/service/windows-stop-confirm.tsconfirmWindowsStopFreed: the port stays free for a 6 s hold, then a final enumeration catches a relaunched wrapper. A remaining listener is classified as ours, foreign, or an exited owner (not foreign).Two recorded departures from 030's numbers and wording:
survived/unbound-wrapper, as 040 specifies.ARM64: the bounded PowerShell fallback exists, but per 050, with no ARM64 device run, slice 2 ships ARM64 with the strict variant (kill nothing, report
unknown).Docs: new
structure/ops/windows-checked-stop.md(linked fromruntime.md, manifest and INDEX regenerated), anddevlog/_plan/261111_windows_service_stop/060_implementation.mdwith the slice plan.Refs #7771.
Verification
tests/windows/use only an injected platform, injected effects and a virtual clock (no real schtasks, PowerShell or FFI calls): 72 pass, 2 skip (the live Windows x64 FFI cases, which run on the Windows lane), 0 fail, including one test per fail-closed rule (unreadable parent never binds; registered + unreadable parent is unknown; terminate overrun is unknown with no further kill; unreadable task or WinSW state is unknown; unreadable stray candidate is unknown; denied exit watch is denied; the child-ancestor lineage clause binds on its own; an unnamed-image listener is unknown), each shown to fail when its rule is mutated, and a regression test that fractional deadline-derived timeouts are floored before the fallback runs. They cover each 040 bullet for these modules: binding positives and negatives, PID reuse leaving the process untouched, denied stopping every further kill, unreadable evidence givingunknownwith nothing killed, a stray wrapper with the task not running, an orphan bound only while registered, WinSW survived, caps givingunknown, admission refusal changing nothing, Part B catching a late relaunch, ours vs foreign, and a ghost listener.bun run test:changedselects only the new files, so nothing imports the modules yet.bun run typecheck,bun run privacy:scan,bun run structure:check, test-layout, Lab boundary and file-size ratchet: pass (rebased on current dev).NOT RUN: Windows real device and ARM64; the live FFI tests on this host (Windows CI lane only); full
bun run test(nothing calls the new code).Checklist