Robustness: bounded ASIO close, native service self-update, rollback pinned (review Phase 3)

- AsioApartment gains a bounded Invoke; the close path uses it (8s cap - a healthy
  close is ~5ms). A driver that wedges inside Stop/Dispose can no longer hang a live
  driver-switch or the resume path: the caller logs the timeout and abandons the
  driver (old park semantics, reclaimed at process exit). Apartment test extended to
  pin the timeout path.
- Service self-update is now NATIVE code: a detached copy of the NEW build runs
  --service-selfupdate (stop -> CopyProgramTo -> start, every step logged to the
  update log). The old PowerShell restart script silently died wherever Group Policy
  enforces execution policy (Bypass is ignored there), stranding the service on the
  old build. Verb wired through IsServiceInvocation + ServiceEntry; verb-gate test
  now pins all six verbs.
- UpdateApplier SwapInNewFiles/RollBack made internal + pinned by a real-folder test:
  swap lands exactly the release's files (user files untouched), backup holds the
  originals, rollback restores BYTE-EXACT including deleting newly-created files -
  the contract that stops a failed update bricking an install.
- RouterPortMapper: discovery callback now checks disposed under the gate, so an
  in-flight callback can't re-open the port map Dispose just removed.

Gate 59/59.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-23 16:10:37 +01:00
co-authored by Claude Fable 5
parent e356ee46dd
commit 9f13ef3aae
9 changed files with 170 additions and 40 deletions
+4
View File
@@ -237,6 +237,10 @@ internal sealed class RouterPortMapper : IDisposable
private void OnDeviceFound(object? sender, DeviceEventArgs args)
{
// A discovery callback can already be in flight when Stop()/Dispose() unsubscribes. Without this
// guard it would re-open the port map that Dispose just removed, leaving the router forwarding to
// us until the lease expires. Narrow race, cheap check (review sweep).
lock (gate) { if (disposed) return; }
try
{
var found = args.Device;