Skip to content

fix(macos): register UTV-bin with LaunchServices so its window is AXStandardWindow - #92

Open
alfonsocasadodiez wants to merge 3 commits into
OpenUTV:mainfrom
alfonsocasadodiez:fix/macos-launcher-ax-window
Open

alfonsocasadodiez wants to merge 3 commits into
OpenUTV:mainfrom
alfonsocasadodiez:fix/macos-launcher-ax-window

Conversation

@alfonsocasadodiez

@alfonsocasadodiez alfonsocasadodiez commented Oct 3, 2026 •

Copy link
Copy Markdown

Summary

Launched from Finder/Dock/open, UTV's window is invisible to Accessibility. System Events finds no windows for UTV-bin, while RV shows AXWindow, AXStandardWindow, Untitled. That breaks window managers and anything else that relies on AX.

The window is fine. The problem is that the launcher execvs into UTV-bin, so LaunchServices still thinks the process is UTV (NSWorkspace reports pid -1). RV doesn't have this because its CFBundleExecutable is the real binary.

This PR makes the launcher posix_spawn UTV-bin and exit when started through LaunchServices, so UTV-bin registers itself. Terminal/CLI launches and --run still execv. I didn't set CFBundleExecutable to UTV-bin because that would drop the dependency-check dialogs.

Documents and rvlink:// URLs arrive as Apple Events for the launcher, not in argv. Before spawning, the launcher now runs a short (0.4s) accessory-mode event loop, collects them, and passes them to UTV-bin as arguments.

Linked Issues

None.

Testing Done

  • macOS (Apple Silicon)
  • Windows 10 / 11
  • Linux
  • Automated tests passed

Full ./build.sh --release build of this branch, launched with UTV not running:

  • plain launch: real pid in NSWorkspace, AXWindow / AXStandardWindow
  • open -a UTV.app file.exr: media opens
  • open -a UTV.app 'rvlink://...': media opens
  • dropping one or several files on the Dock icon: media opens

Known side effects of the launcher exiting right after the spawn: open -W returns immediately, and the Dock icon may briefly disappear and reappear. Plain launches also take about 0.4s longer because of the event grace period.

…tandardWindow

When started through LaunchServices, the launcher execv'd into UTV-bin, leaving LaunchServices with a stale record of the process (NSWorkspace reported pid -1) and hiding the window from System Events. Spawn UTV-bin instead so it registers itself. Terminal and --run launches still execv.
@alfonsocasadodiez
alfonsocasadodiez marked this pull request as draft October 3, 2026 13:59
@alfonsocasadodiez
alfonsocasadodiez marked this pull request as ready for review October 3, 2026 14:00
@mcoliver

mcoliver commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this, and for tracking it down. I can reproduce it with 2026.11 from /Applications: NSWorkspace reports UTV with pid -1, while querying the process directly with the AX API finds the AXStandardWindow. So the window is fine and the registration is what breaks, as you describe.

One concern before merging. When UTV is launched to open something (double-clicking an .exr associated with UTV, dropping files on the Dock icon, or an rvlink:// URL; the Info.plist declares both document types and the rvlink scheme), LaunchServices doesn't pass the file in argv. It sends an odoc / GURL Apple Event to the process it launched. Today that process execvs into UTV-bin, which then receives the event. With posix_spawn + return 0, I'd expect the event to go to the launcher, which exits without handling it, so UTV would start empty.

I haven't been able to confirm this on a full build, so I may be wrong. Could you try, on a build of this branch, with UTV not running:

  1. double-click an .exr (or open -a UTV.app file.exr);
  2. open 'rvlink://...' with a valid link.

If both open the media, great. If not, one way to keep your approach is for the launcher to collect those events before spawning: install kAEOpenDocuments / kAEGetURL handlers, run the event loop briefly (until applicationDidFinishLaunching plus a short grace period), then posix_spawn UTV-bin with the paths / URL appended to argv. Making UTV-bin the CFBundleExecutable isn't an option for the reason you gave: the launcher's library check (#88) has to run before dyld loads UTV-bin.

Two smaller things worth noting in the PR description: open -W will return immediately, and the Dock icon may briefly disappear and reappear as the launcher exits.

I've approved the CI workflows for this PR.

LaunchServices delivers odoc/GURL Apple Events to the launcher, not argv. Collect them with a short accessory-mode event loop before posix_spawn and pass them to UTV-bin as arguments.
@alfonsocasadodiez

Copy link
Copy Markdown
Author

Hey! With posix_spawn alone, opening an .exr or an rvlink:// URL started UTV empty.

I've pushed a follow-up where the launcher collects the open/URL events before spawning (short accessory-mode event loop, 0.4s) and passes the paths/URLs to UTV-bin as arguments.

Tested on a build of this branch, with UTV not running:

  • open -a UTV.app file.exr opens the media in an AXStandardWindow
  • open -a UTV.app 'rvlink://…' does the same
  • dropping one or several files onto the Dock icon does the same as well
  • plain launch gives an empty window, also AXStandardWindow, with a valid pid in NSWorkspace

P.S. I needed some help from Claude to get this up and running

@mcoliver

mcoliver commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

Thanks, this works and the testing is thorough. Two small changes before we merge:

1. Stop waiting at applicationDidFinishLaunching: instead of after a fixed 0.4 s.

I checked the delivery order with a small test app built the same way as the launcher (no ARC, accessory policy, finishLaunching, then the same nextEventMatchingMask loop), registered for a custom URL scheme and a custom document type, launched by LaunchServices while not running, on macOS 27.0.1. Times are from process start:

Launch application:openURLs: applicationDidFinishLaunching:
URL (open 'scheme://...'), 5 runs 65–85 ms 0.1 ms later, every run
Document (open -a App file), 2 runs 92 ms 0.0–0.1 ms later
Plain launch not called 82 ms

The launch Apple Event (GURL, odoc or oapp) is handled on the first pass through the loop, and applicationDidFinishLaunching: is posted right after it, so URLs and documents always arrive first. If the loop ends at applicationDidFinishLaunching:, launches start about 0.4 s sooner, and a busy machine can no longer push the event past the deadline. I'd keep a generous upper bound (say 3 s) in case the notification never arrives. Roughly:

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    self.finished = YES;
}
...
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:3.0];
while (!collector.finished && [deadline timeIntervalSinceNow] > 0) {
    NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:deadline inMode:NSDefaultRunLoopMode dequeue:YES];
    if (event) [NSApp sendEvent:event];
}

(I tested with a custom scheme rather than rvlink so as not to change the rvlink handler on my machine; LaunchServices delivers every URL scheme the same way, as a GURL event.)

2. Retain the collected array. UTVLauncher.mm is compiled without ARC, so _requests = [NSMutableArray array]; in init assigns an autoreleased object without retaining it. It works today only because nothing drains the pool before the array is read. _requests = [[NSMutableArray alloc] init]; fixes it.

Our CI doesn't run the full macOS build for pull requests from forks, so once these are in I'll push a copy of the branch to our repo to get one before merging.

…ching

Launch Apple Events are delivered before applicationDidFinishLaunching, so end the event loop there instead of after a fixed 0.4s, with a 3s upper bound. Also retain the collected array, since the launcher is built without ARC.
@alfonsocasadodiez

Copy link
Copy Markdown
Author

Thanks, both done and pushed.

  1. The loop now stops at applicationDidFinishLaunching:, with a 3s upper bound in case it never arrives.
  2. _requests = [[NSMutableArray alloc] init];

I rebuilt with ./build.sh --release, and tested with UTV not running as the previous run.

Hope this works!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants