Repository navigation
Conversation
A Simulator.app older than the installed CoreSimulator (Xcode 16.4 selected after an Xcode 27 install) aborts within a second of every launch, so every non-headless boot-device popped a "Simulator quit unexpectedly" dialog. The first launch per app/CoreSimulator build pair is now watched and the outcome recorded in ~/.argent/simulator-app-launch.json; a crashing app is not opened again.
… record Crash reports on an affected host show aborts up to 1.64 s after launch, so the 2 s watch could record a crashing app as running. Match the process path case-insensitively, log instead of failing when the record cannot be written, and name the reset file in the skip message.
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 30 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…h watch An MCP server launched from a GUI or launchd context inherits a PATH without /bin, where macOS keeps ps. The bare "ps" then failed, the watch recorded nothing, and every boot reopened the crashing Simulator.app. Use PS_BIN like the other ps call sites. Also pin the CFBundleVersion key field, the end of the watch window and the skip notice in tests.
…he path's Unicode form open -a only activates a Simulator.app that is already running, so the watch saw the old instance and stored a lasting "runs" for a build that may crash at launch. Skip the watch when an instance is already up. ps prints the on-disk Unicode form (NFD for Finder-made names), while xcode-select echoes the form it was given, so a healthy app under a non-ASCII path was stored as "crashes". Compare NFC-normalized paths. Pin the Info.plist paths, both key fields and the plutil/ps timeouts in tests, and say in openSimulatorWindow's JSDoc that only an app on record as crashing is skipped.
writeFileSync truncates before it writes, so a second tool-server reading the record mid-write parsed nothing and rewrote it with its own entry alone, dropping every other verdict. Write a temp file and rename it. Pin in tests the ps arguments, a ps failure during the watch, the early return on a crash, decoy Simulator paths, and that boot-device's launch waits for open.
…ord write A launchd-started tool-server may have no locale, and ps then escapes non-ASCII bytes, so a running Simulator.app under such a path was stored as "crashes". Run ps with LC_ALL=en_US.UTF-8. Remove the temp file when the record cannot be written or renamed. Pin in tests the pre-launch probe's failure handling, the Device Hub fallthrough after a skip, and the reset hint.
| const watch = verdict !== "runs" && !(await isRunning(executable).catch(() => true)); | ||
| await launch(); | ||
| if (!watch) return true; | ||
| const runs = await survivesLaunch(executable).catch(() => null); |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: The watch cannot tell a crash from a clean quit. A healthy Simulator.app that exits for any reason within 5 s of its first watched launch, e.g. the user pressing Cmd-Q on the window that just popped up, is stored as crashes, and its window stays off until the record is deleted or a build changes. Reproduced with a healthy app killed 1.5-2 s after open -a: the record got crashes, and the next two calls returned false without launching.
| // A Simulator.app built against an older CoreSimulator than the installed one | ||
| // (e.g. Xcode 16.4 selected after an Xcode 27 install) aborts up to ~1.7 s into | ||
| // every launch, popping a "Simulator quit unexpectedly" dialog each time. | ||
| const LAUNCH_WATCH_MS = 5_000; |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: The opposite case: a crash that lands after the 5 s window is stored as a permanent runs, and verdict !== "runs" then skips the watch on every later boot, so the crash dialog comes back each time. The PR measured the abort at up to 1.64 s on an idle host; it was not measured on a heavily loaded host booting several simulators.
| const runs = await survivesLaunch(executable).catch(() => null); | ||
| if (runs === null) return true; | ||
| try { | ||
| persistVerdict(key, runs ? "runs" : "crashes"); |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: The verdict is only remembered on disk. When ~/.argent cannot be written (EACCES, ENOSPC), each later boot in the same tool-server reopens and re-watches the crashing app. Ran it with .argent at mode 555: two consecutive calls both launched the app and both logged could not record ... EACCES.
| } | ||
| } | ||
|
|
||
| function persistVerdict(key: string, verdict: Verdict): void { |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: The read-modify-write is serialized only inside one process. Two tool-servers sharing a HOME can each read the record, add their key and rename, and the later rename drops the earlier entry, so one more crash dialog is shown before the entry is re-recorded. With 24 processes recording distinct keys at once, 17-23 of 24 keys survived per round (20 rounds).
| const comparablePath = (p: string) => p.normalize("NFC").toLowerCase(); | ||
|
|
||
| async function isRunning(executable: string): Promise<boolean> { | ||
| const { stdout } = await execFileAsync(PS_BIN, ["-axo", "comm="], { |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: -ax lists every user's processes. On a Mac where another logged-in user runs the same Xcode's Simulator.app, the pre-launch check sees that instance and never watches, so a crashing app is never recorded. If the other user's instance starts during the watch instead, a crash in this session can be stored as runs. Traced from code only; this host has one login user.
| if (verdict === "crashes") return false; | ||
| const executable = path.join(simulatorApp, "Contents", "MacOS", "Simulator"); | ||
| const watch = verdict !== "runs" && !(await isRunning(executable).catch(() => true)); | ||
| await launch(); |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: Under heavy CPU load, open -a issued a few hundred ms after the previous instance died can exit 0 without starting a new process, as LaunchServices still has the dead instance registered. The pre-check saw nothing running, so the first poll then stores a permanent crashes for a healthy app. Measured with Calculator and TextEdit under yes x42 on 14 cores and a 300 ms gap after a kill: between 1 of 30 and 10 of 50 launches spawned nothing. With no load, or a 2.5 s gap, there were 0 misses.
| execFileAsync("open", ["-a", simulatorApp], { timeout }) | ||
| ); | ||
| if (!launched) { | ||
| process.stderr.write( |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: When the window is skipped, boot-device still returns { platform: "ios", udid, booted: true }, the same result as a boot that opened a window. This line goes to the tool-server's stderr, which the launcher points at ~/.argent/tool-server.log, so neither the agent nor the user is told why no window appeared or how to reset it.
| const simulatorApp = join(developerDir, "Applications", "Simulator.app"); | ||
| if (existsSync(simulatorApp)) { | ||
| await execFileAsync("open", ["-a", simulatorApp], { timeout }); | ||
| const launched = await launchSimulatorApp(simulatorApp, () => |
There was a problem hiding this comment.
[Diplomat, Opus 5.5]: The first watched launch per key adds about 6 s to boot-device (5.9-6.3 s measured, against about 70 ms on the cached path). boot-device is not longRunning, so argent-mcp aborts each attempt at 30 s and replays it, and the server keeps running the first one. A cold boot already near that cap now hits it once, and the replay runs the boot steps a second time in parallel on the same simulator.
Problem: a Simulator.app older than the installed CoreSimulator (e.g. Xcode 16.4 selected after an Xcode 27 install) aborts on every launch. #1198 opens the active Xcode's Simulator.app by path, so on such a host every non-headless
boot-devicepops a "Simulator quit unexpectedly" dialog.openexits 0, so nothing notices.Fix: the first launch of each Simulator.app / CoreSimulator build pair is watched for 5 s. If the process is gone, that's recorded in
~/.argent/simulator-app-launch.jsonand the app is not opened again; the device boots without a window, as withheadless: true. Parallel boots share the first verdict; a launch that only activates an instance already running is not judged. Device Hub is untouched.boot-devicecall 1main(v0.26.0)Docs:
reference/configuration.mdxdescribes the record and how to reset it.Verification
POST /tools/boot-deviceon a branch tool-server, crash reports matched to each call by theirprocLaunchtime. Every call returnedbooted: true. A second, fresh server read the record and opened nothing.Simulator-*.ipsreports on that host areSIGABRTinswift::ResolveAsSymbolicReferencefrom Xcode 16.4'sSimulatorKit, <2 s after launch (max 1.64 s, hence the 5 s watch).test/simulator-app-launch.test.ts(crash/run verdicts, watch window, parallel callers, app/CoreSimulator build change, per-app key, already-running instance, ps case/Unicode form/locale/decoy paths, ps/plutil failures and timeouts, unwritable~/.argent, corrupt record, rename-into-place) +boot-devicecases for the skip and its notice. Mutations of each rule (skip, watch window, lock, key fields and plist paths, ps binary/args, path match, merge, error handling) fail the suite.open -a//bin/ps/plutil): crash recorded and skipped withPATH=/usr/bin:/usr/sbin(no/bin); healthy app recorded as running; an already-running instance left unjudged; a healthy app under an NFC/NFD-mismatched or non-ASCII path (no locale) recorded as running; 24 processes recording at once never left a half-written record (20 rounds).typecheck:tests, eslint, prettier, knip,docusaurus build.