* test: red regression for termination watchdog (#6758) cmux can hang the main thread for ~30s on Cmd+Q when a clipboard-history manager (Paste, Raycast, Maccy, …) is mid-read of cmux's promised pasteboard data: AppKit's will-terminate gauntlet runs CFPasteboardResolveAllPromisedData, which blocks on a stuck mach round-trip to the pasteboard server until the OS force-kills the app. This is the third "an observer blocks the main thread during quit" report (cf. #6415 PostHog flush, #6381 ghostty lock); the structural gap is that quit has no global "return within N seconds no matter what" guard. Add TerminationWatchdog plus its tests, with the watchdog deliberately inert (it never starts the firing thread) so the tests go red. The end-to- end pasteboard deadlock is not unit-testable — reproducing it requires the real pasteboard server and would wedge the test process — so the tests cover the watchdog mechanism that bounds it. The fix commit starts the thread and arms the watchdog from the terminate path. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Bound app termination with a force-exit watchdog (#6758) Implement TerminationWatchdog.arm and arm it from the terminate path so a committed quit always returns within a bounded time, even when AppKit's will-terminate gauntlet wedges on an Apple-owned observer we don't control (CFPasteboardResolveAllPromisedData blocking on a stuck pasteboard-server round-trip while a clipboard-history manager reads cmux's promised data). The watchdog runs on a dedicated background thread with no run-loop, GCD, or main-actor dependency, so it fires even while the main thread is parked in mach_msg. It is armed in prepareForConfirmedAppTermination() — after the critical session/state save and before AppKit posts will-terminate — and, as a backstop, at the start of applicationWillTerminate(). Arming is idempotent, so the two sites and repeated quit attempts never stack threads. If the process has not exited within the deadline it force-exits cleanly, turning a ~30s hang into a bounded quit. This closes the structural gap shared with #6415 and #6381: quit now has a global "return within N seconds no matter what" guard. Fixes #6758 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Address review: lock-free watchdog exit, drop singleton (#6758) - Codex/autoreview P1 (correctness): the watchdog's onFire logged a StartupBreadcrumbLog entry (flock + Foundation/file I/O) before _exit. If that logging stalled or contended during an already-wedged termination, the watchdog thread could block before reaching _exit and the quit hang would stay unbounded — defeating the guarantee. Drop the breadcrumb: the firing path is now an unconditional, lock-free _exit (the default onFire), which does zero Foundation/filesystem work before exiting. - Greptile P1 (no-ambient-global-state): replace the TerminationWatchdog.shared singleton with an AppDelegate-owned instance, next to the existing terminate-control state (terminateKillWatchdogTask). The type was already injectable, so this is a small wiring change. - Greptile P2: document why the deadline uses a raw Thread + Thread.sleep rather than a GCD timer (the wedged termination can sit on GCD/run-loop infrastructure, so the firing path must not depend on it). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Document lock-not-actor choice in TerminationWatchdog (#6758) cmux-policy (Aziz concurrency) prefers actor isolation over locks for new runtime state. Rejected here with rationale recorded in-code: an actor would force `arm()` async, but it is called synchronously from the terminate delegate methods and the deadline fires on a raw Thread — and the watchdog must not depend on the Swift concurrency runtime, which may itself be wedged during the termination it guards against. This is the same sanctioned NSLock + nonisolated(unsafe) shape TerminalPasteboardService uses for synchronous- callback state. Comment-only change. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Determinize TerminationWatchdog test via injected scheduler (#6758) CI's test-determinism gate (scripts/check-test-determinism.py --strict) flagged the prior tests for real sleeps / wall-clock timeouts (sleep-then-assert and assert-on-duration). Invert the time dependency per the gate's contract instead of allowlisting: extract the deadline scheduler as an injectable `DeadlineScheduler`. Production keeps the raw background Thread (`TerminationWatchdog.threadScheduler`); the tests inject a synchronous capturing scheduler and advance the deadline by hand. The tests now assert idempotency (three arms schedule the deadline exactly once) and exactly-once firing with zero real sleeps, timeouts, or wall-clock reads, so they are deterministic by construction. arm() is now a thin idempotent latch over scheduleDeadline(deadline, onFire); behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Use atomic termination watchdog latch * Use non-deprecated termination watchdog latch * Use C11 atomic termination watchdog latch * Save termination state before watchdog fallback * Avoid growing AppDelegate termination path --------- Co-authored-by: cmux <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
3 lines
66 B
Objective-C
3 lines
66 B
Objective-C
@import GhosttyKit;
|
|
#import "Sources/TerminationWatchdogAtomic.h"
|