Skip to content

Commit d6f1e4e

Browse files
committed
fix(macos): attach to a running app instead of launching a duplicate on open
A macOS app started directly — by a supervisor or a dev script — is not listed by LaunchServices until half a second or so after it spawns. During that window `open -b` sees no instance and starts a second copy with no arguments, which the session then drives instead of the copy the caller owns (#3406). The bundle open now reads the same view `open -b` consults and, when it lists nothing while a process of that bundle is running, waits until LaunchServices names the running copy before running `open` unchanged. An already-listed bundle and a bundle with nothing running reach `open` on the first read; the wait, every read, and the process scan share one budget, and anything the probe cannot answer fails open so an open is never withheld.
1 parent 5007540 commit d6f1e4e

6 files changed

Lines changed: 845 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Opening a macOS app by bundle identifier now waits briefly when the app is already running but not yet registered with LaunchServices — the case when a supervisor or dev script started it directly — and if registration arrives within that bounded window the open attaches to the running copy, so the session drives the app the caller started, with its arguments. A copy still unlisted when the window closes gets the plain `open` as before, which can still duplicate; the wait only bounds the race, it does not close it. Launching an app that is not running costs one extra LaunchServices lookup and one process-table read, plus one bundle-identifier read when app-shaped processes exist.
56
- Limrun iOS snapshots no longer fail when the instance reports a numeric or boolean accessibility value (the system photo picker's tabs report `1`/`0`): the provider seam normalizes variant scalars to text, the way the Apple tree does. Limrun nodes now also publish `selected: true` for nodes carrying the `Selected` trait, so snapshot output and `selected:` selectors can tell a picked photo from an unpicked one.
67
- `settings` refuses a setting the target does not serve before anything changes, with `UNSUPPORTED_OPERATION`, `settings <setting> is not supported on this device`, and the target's `error.details.reason` (such as `unsupported-platform-leaf`). This replaces the macOS `INVALID_ARGS` refusal and the Apple `setting-unsupported-on-leaf` reason, and now also covers settings that used to reach the device first, such as Android `reset-keychain` or Apple `animations`.
78
- `settings` refuses, before touching the device, a state its setting does not accept (`Invalid <setting> state: …`), a permission action or mode its target does not take (such as `permission deny accessibility`), and a stray extra argument (such as `clear-app-state <app> <extra>`). The CLI and the daemon accept the same state spellings, in any casing or surrounding whitespace.

‎packages/platform-apple/src/core/__tests__/tool-provider.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ test('local Apple tool provider exposes macOS host operations as semantic method
177177
const calls: Array<[string, string[], string | undefined]> = [];
178178
const provider = createLocalAppleToolProvider({
179179
runCommand: async (cmd, args, options) => {
180+
// The bundle open's LaunchServices registration probe is the seam's own read, asserted in
181+
// host-provider-open.test.ts; this routing test only reports it as listing the bundle.
182+
if (cmd === '/usr/bin/lsappinfo') {
183+
return { exitCode: 0, stdout: 'ASN:0x0-0x1234-"Demo":\n', stderr: '' };
184+
}
180185
calls.push([cmd, args, typeof options?.stdin === 'string' ? options.stdin : undefined]);
181186
if (cmd === 'pbpaste') {
182187
return { exitCode: 0, stdout: 'copied\r\n', stderr: '' };

‎packages/platform-apple/src/os/macos/host-provider.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export function createLocalAppleMacOsHostProvider(
1818
): AppleMacOsHostProvider {
1919
return {
2020
openBundle: async (bundleId, url) => {
21+
// Loaded on the first bundle open, not when this provider is constructed: the probe's host-kit
22+
// reads stay off the Apple façades' eager closure (ADR 0019), and an open of an already-listed
23+
// app is the common path that never needs them until `open` actually runs.
24+
const { waitForLaunchServicesRegistration } =
25+
await import('./launch-services-registration.ts');
26+
await waitForLaunchServicesRegistration(runCommand, bundleId);
2127
await runCommand('open', buildMacOpenArgs(bundleId, url));
2228
},
2329
openTarget: async (target) => {

0 commit comments

Comments
 (0)