fix: gate HID connection reuse on launchd_sim process identity#58
Merged
Conversation
The boot-identity gate keyed cache reuse solely on the mtime of <dataDirectory>/var/run/launchd_bootstrap.plist. The issue #55 investigation showed CoreSimulator does not reliably rewrite that marker on every boot (observed frozen across reboots during the Xcode 27 B4 / CoreSimulator 1169.1 session of 2026-07-22), so two different boots could carry equal mtimes and the daemon kept sending HID events into the previous boot's dead mach port — reporting success while delivering nothing. The token is now compound: the launchd_sim process identity (pid + kernel start time — that process's lifetime IS the boot, and the start time guards pid reuse) is authoritative, with the marker mtime kept only as a fallback when the process probe is unavailable. The probe reads the process table via sysctl(KERN_PROC_ALL) + KERN_PROCARGS2 (~1-2 ms; the /bin/ps pattern used elsewhere measures ~40 ms, too slow to run per verb). As defence in depth, each HID send now runs under a deadline (default 30 s, SIM_USE_HID_SEND_TIMEOUT_MS overrides, 0 disables): a send into a dead port that previously hung the daemon forever now fails loudly. The timeout message deliberately matches none of HIDPerformRecovery's dead-transport markers so a timed-out composite is never blind-retried (pinned by a unit test). New opt-in E2E suite HIDRebootRecoveryTests covers the regression end-to-end: tap -> simctl shutdown && boot -> tap through the same daemon; a pidfile assertion guards against a daemon respawn making the run vacuous. Verified live on Xcode 27 Beta 4 (iOS 27.0) and an iOS 26.5 runtime: tap lands after an out-of-band reboot through a surviving daemon, same daemon pid throughout. Fixes #55 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: onevcat <onevcat@gmail.com>
Two review findings on #58: - HIDSendDeadline trapped on the ms→ns multiply for any parseable SIM_USE_HID_SEND_TIMEOUT_MS above ~UInt64.max/1e6. Saturate instead: an absurdly large value now means "effectively no deadline" (UInt64.max ns ≈ 584 years), pinned by a regression test. - HIDRebootRecoveryTests could leave the shared simulator shut down when the mid-test boot/bootstatus failed, cascading unrelated failures through the rest of the sequential runner. Best-effort re-boot before surfacing the real error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: onevcat <onevcat@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #55 — after an out-of-band
simctl shutdown && boot, the surviving per-UDID daemon kept sending HID events through the previous boot's dead mach port:tapreported success while delivering nothing, the worst failure mode for an agent-facing tool.Root cause
The existing boot-identity gate (
HIDBootIdentity, #23-era) keyed cache reuse solely on the mtime of<dataDirectory>/var/run/launchd_bootstrap.plist, assuming CoreSimulator rewrites it on every boot. The investigation showed that assumption is conditional: forensics on the 2026-07-22 repro device found the marker frozen at17:46:09whilevar/runsiblings showed boot-cycle activity through19:20:39(Xcode 27 B4 / CoreSimulator 1169.1; a CoreSimulatorService restart at 20:57 that evening suggests a mixed-version service state during the repro window). Equal mtimes across different boots ⇒ the dead connection was judged reusable. Notably, a clean-state re-run of the exact repro steps on both Xcode 27 B4 and an iOS 26.5 runtime did not reproduce — the marker advanced and the gate rebuilt correctly — which is exactly why the fix moves off the marker as the primary signal.Fix
LaunchdSimLocator(new) — resolves thelaunchd_simprocess serving a UDID viasysctl(KERN_PROC_ALL)+KERN_PROCARGS2(~1–2 ms per probe; the/bin/pspattern used elsewhere measures ~40 ms).launchd_sim's lifetime IS the boot; its(pid, kernel start time)is an unforgeable boot identity, immune to marker-rewrite semantics and pid reuse.HIDBootIdentity— token becomes compound. Decision table: process identities on both sides → reuse iff equal (authoritative, the iOS daemon holds a stale HID connection across simulator reboot — tap reports success but is not delivered #55 fix); one-sided → fail closed; neither → previous marker-mtime rule as fallback.HIDSendDeadline(new) — defence in depth for the residual window (reboot mid-command, or an unknown future token failure): each send runs under a 30 s deadline (SIM_USE_HID_SEND_TIMEOUT_MS,0disables) so a dead-port hang (the Xcode 26.5-observed mode) fails loudly instead of wedging the daemon. The timeout wording deliberately matches none ofHIDPerformRecovery's dead-transport markers, so a timed-out composite is never blind-retried — pinned by a unit test.The durable logic (token + reuse decision + locator) is pure and transport-agnostic; the planned idb/DTUHID migration replaces
HIDInteractorand re-wires the same gate in one line.Tests
HIDBootIdentityTests— compound-token decision table, including the iOS daemon holds a stale HID connection across simulator reboot — tap reports success but is not delivered #55 regression row (marker equal, process identity changed → reject).LaunchdSimLocatorTests— pure matching on fixture tables (exact-comm filter, per-UDID argv scoping, overlapping-boot resolution, case-insensitivity) + a live sysctl smoke test.HIDSendDeadlineTests— race mechanics, cancellation propagation, and the invalidateOnly classification pin.HIDRebootRecoveryTests(opt-in E2E, registered inscripts/test-runner.sh) — tap →simctl shutdown && boot→ tap through the same daemon (pidfile-asserted), with a dtuhidd-confounder guard.Verification
make build/make test: 1115 unit tests green, no warnings.SIMULATOR_UDID=… ./scripts/test-runner.sh HIDRebootRecoveryTestson Xcode 27 Beta 4 / iOS 27.0: passed live (27 s).21858throughout) — landed.🤖 Generated with Claude Code