-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(v3): macOS SingleInstance + URL scheme — relay URL to first instance (#5089) #5170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
leaanthony
wants to merge
5
commits into
master
Choose a base branch
from
fix/5089-macos-single-instance-url-scheme
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+598
−2
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2b4ab57
test(v3): add repro example for macOS SingleInstance + URL scheme bug…
17ecb7e
examples/single-instance-url-scheme: document macOS 26+ behaviour + a…
leaanthony 11c880a
fix(v3): relay URL to first instance in macOS SingleInstance + URL sc…
leaanthony d1afeb8
chore(v3): address CodeRabbit review comments on fix/5089
leaanthony 8cbb9d6
chore(v3): address Copilot review comments on fix/5089
leaanthony File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| # single-instance-url-scheme — fix for issue #5089 | ||
|
|
||
| Demonstrates (and formerly reproduced) [wailsapp/wails#5089](https://github.com/wailsapp/wails/issues/5089): | ||
| combining `SingleInstance` with a custom URL scheme on macOS. | ||
|
|
||
| ## What the bug was | ||
|
|
||
| On macOS, URL-scheme launches do not place the URL in `os.Args`. | ||
| LaunchServices delivers the URL via an Apple Event (`kAEGetURL`) that is | ||
| dispatched by the target process's `NSAppleEventManager` — which is only | ||
| wired up once `NSApplication.run` is executing. | ||
|
|
||
| When a second instance hits the flock in `newSingleInstanceManager` it | ||
| immediately called `notifyFirstInstance()` and `os.Exit`. The Apple Event | ||
| handler was never installed, so the URL was discarded. The payload relayed | ||
| to the first instance (`SecondInstanceData{Args, WorkingDir, …}`) only | ||
| contained what was in `os.Args`, which on macOS does not include the URL. | ||
|
|
||
| On Windows and Linux the URL is passed through `argv`, so it surfaces | ||
| naturally as `SecondInstanceData.Args[1]`. macOS was the odd one out. | ||
|
|
||
| ## The fix | ||
|
|
||
| `v3/pkg/application/single_instance_darwin_url.go` adds a `captureLaunchURL()` | ||
| helper that: | ||
|
|
||
| 1. Creates `[NSApplication sharedApplication]` with `NSApplicationActivationPolicyProhibited` (no dock icon). | ||
| 2. Registers a `kAEGetURL` Apple Event handler **before** calling `[NSApp run]`. | ||
| 3. Calls `[NSApp run]` — which triggers `finishLaunching`, signalling to | ||
| LaunchServices that this process is ready to receive Apple Events. | ||
| 4. Stops the run loop immediately when the URL event arrives (or after a | ||
| 300 ms safety-net timeout if no event arrives). | ||
| 5. Returns the captured URL (or `""` on timeout). | ||
|
|
||
| `notifyFirstInstance()` (in `single_instance.go`) calls `captureLaunchURL()` | ||
| on darwin and appends the URL to `SecondInstanceData.Args` before notifying | ||
| the first instance, matching the Windows/Linux behaviour. | ||
|
|
||
| `ApplicationLaunchedWithUrl` is **not** fired on the second-instance relay | ||
| path, consistent with Windows and Linux behaviour. | ||
|
|
||
| ## Running the example | ||
|
|
||
| Requirements: macOS, Go, `wails3` CLI (`task` runner), Xcode command-line | ||
| tools. Uses `codesign --sign -` (ad-hoc) so no certificate needed. | ||
|
|
||
| ```sh | ||
| cd v3/examples/single-instance-url-scheme | ||
|
|
||
| # 1. Build + package a dev .app bundle and run it. This also registers | ||
| # the bundle's CFBundleURLTypes with LaunchServices so the custom | ||
| # scheme is routed to the app. | ||
| wails3 task run | ||
|
|
||
| # In a separate terminal, tail the log the app writes: | ||
| tail -f /tmp/wails-single-instance-url.log | ||
| ``` | ||
|
|
||
| With the first instance window visible, trigger a URL-scheme launch: | ||
|
|
||
| ```sh | ||
| # Either via the helper task: | ||
| wails3 task trigger URL='wails-single-url://hello?n=1' | ||
|
|
||
| # Or directly: | ||
| open 'wails-single-url://hello?n=1' | ||
| ``` | ||
|
|
||
| ### macOS version behaviour differences | ||
|
|
||
| **macOS 14 / 15:** `open 'wails-single-url://...'` causes LaunchServices to | ||
| spawn a second process (because `LSMultipleInstancesProhibited` is not set). | ||
| That second process detects the flock, captures the URL via the fix, and | ||
| relays it to the first instance. | ||
|
|
||
| **macOS 26+ (observed on 26.0/25A354, Apple Silicon):** `open 'wails-single-url://…'` | ||
| without `-n` routes the Apple Event directly to the running first instance | ||
| (`ApplicationLaunchedWithUrl` fires). No second process is spawned. | ||
|
|
||
| To trigger the second-instance relay path on macOS 26+, use `trigger:force` | ||
| which forces a new process via `open -n`: | ||
|
|
||
| ```sh | ||
| wails3 task trigger:force URL='wails-single-url://hello?n=1' | ||
| ``` | ||
|
|
||
| ### Fixed behaviour | ||
|
|
||
| After applying the fix, triggered via `trigger:force` (or via plain `trigger` | ||
| on macOS 14/15), the first instance log shows: | ||
|
|
||
| ```text | ||
| [first] OnSecondInstanceLaunch fired | ||
| [first] Args = [.../single-instance-url-scheme wails-single-url://hello?n=1] | ||
| [first] url-in-args? = true (url="wails-single-url://hello?n=1") | ||
| ``` | ||
|
|
||
| `ApplicationLaunchedWithUrl` does **not** fire on either instance for the | ||
| second-instance relay path. | ||
|
|
||
| Timing (measured on macOS 26, Apple Silicon): | ||
| - URL captured: second instance exits in **~120–160 ms** (early-exit on event arrival). | ||
| - No URL (e.g. `open -n app.app`): second instance exits in **~320–430 ms** (300 ms timeout). | ||
|
|
||
| ## Unfixed behaviour (pre-PR, for historical reference) | ||
|
|
||
| Triggered via `trigger:force` on the unfixed branch, the first instance logged: | ||
|
|
||
| ```text | ||
| [first] OnSecondInstanceLaunch fired | ||
| [first] Args = [.../single-instance-url-scheme] | ||
| [first] url-in-args? = false (url="") | ||
| ``` | ||
|
|
||
| `ApplicationLaunchedWithUrl` did **not** fire on either instance. | ||
|
|
||
| ## Notes / gotchas for testing | ||
|
|
||
| - Running the raw binary (`go run .`) will **not** exercise the bug path. | ||
| macOS only routes custom URL schemes to apps launched from a `.app` | ||
| bundle registered with LaunchServices. Use `wails3 task run`. | ||
| - If `open 'wails-single-url://…'` launches a different app, the scheme | ||
| is claimed by a previously-registered bundle. Re-register this one: | ||
| `/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -f bin/single-instance-url-scheme.dev.app`. | ||
| - Fresh launches (first instance) already go through the | ||
| `NSAppleEventManager` path and work correctly; the fix is specific to | ||
| the second-instance relay. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| version: '3' | ||
|
|
||
| includes: | ||
| common: ./build/Taskfile.yml | ||
| darwin: ./build/darwin/Taskfile.yml | ||
|
|
||
| vars: | ||
| APP_NAME: "single-instance-url-scheme" | ||
| BIN_DIR: "bin" | ||
|
|
||
| tasks: | ||
| build: | ||
| summary: Builds the application | ||
| cmds: | ||
| - task: "{{OS}}:build" | ||
|
|
||
| package: | ||
| summary: Packages a production build of the application | ||
| cmds: | ||
| - task: "{{OS}}:package" | ||
|
|
||
| run: | ||
| summary: Runs the application from a dev .app bundle (required so macOS routes URL scheme to it) | ||
| cmds: | ||
| - task: "{{OS}}:run" | ||
|
|
||
| trigger: | ||
| summary: 'Launch the custom URL. Usage: wails3 task trigger URL=wails-single-url://hello?n=1' | ||
| desc: | | ||
| On macOS 14/15, routes the URL to a second process which hits the SingleInstance | ||
| lock, captures the URL via the kAEGetURL Apple Event, and relays it to the first | ||
| instance via SecondInstanceData.Args before exiting. | ||
| On macOS 26+, LaunchServices sends the Apple Event directly to the running first | ||
| instance, so ApplicationLaunchedWithUrl fires (no second process spawned). | ||
| Use trigger:force to exercise the second-instance relay path on macOS 26+. | ||
| cmds: | ||
| - 'open "{{.URL | default "wails-single-url://hello?n=1"}}"' | ||
|
|
||
| trigger:force: | ||
| summary: 'Force a new process launch with URL (exercises second-instance relay). Usage: wails3 task trigger:force URL=wails-single-url://hello?n=1' | ||
| desc: | | ||
| Uses open -n to bypass LaunchServices reuse behaviour and force a new process. | ||
| The new process hits the SingleInstance lock, captures the URL via the kAEGetURL | ||
| Apple Event, and relays it to the first instance via SecondInstanceData.Args. | ||
| OnSecondInstanceLaunch fires on the first instance with url-in-args?=true. | ||
| cmds: | ||
| - 'open -n -a "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" "{{.URL | default "wails-single-url://hello?n=1"}}"' | ||
|
|
||
| tail: | ||
| summary: Tail the repro logfile written by main.go | ||
| cmds: | ||
| - tail -f /tmp/wails-single-instance-url.log |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Single Instance URL Scheme Repro</title> | ||
| <style> | ||
| body { font-family: -apple-system, system-ui, sans-serif; margin: 2rem; background: #1b2636; color: #eee; } | ||
| h1 { margin-top: 0; } | ||
| .row { margin: 0.5rem 0; padding: 0.75rem 1rem; background: #223043; border-radius: 6px; } | ||
| .label { color: #8aa4c8; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; } | ||
| pre { margin: 0.25rem 0 0; white-space: pre-wrap; word-break: break-all; } | ||
| .missing { color: #ff6b6b; } | ||
| .ok { color: #7bd88f; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>wails#5089 repro</h1> | ||
| <p>Launch a custom-scheme URL from Terminal while this window is open:</p> | ||
| <pre>open 'wails-single-url://hello?n=1'</pre> | ||
|
|
||
| <div class="row"> | ||
| <div class="label">Last <code>ApplicationLaunchedWithUrl</code></div> | ||
| <pre id="evt-url" class="missing">(never fired)</pre> | ||
| </div> | ||
|
|
||
| <div class="row"> | ||
| <div class="label">Last <code>OnSecondInstanceLaunch</code></div> | ||
| <pre id="evt-si" class="missing">(never fired)</pre> | ||
| </div> | ||
|
|
||
| <script type="module"> | ||
| import { Events } from "/wails/runtime.js"; | ||
|
|
||
| Events.On("launchedWithUrl", (ev) => { | ||
| const el = document.getElementById("evt-url"); | ||
| el.className = "ok"; | ||
| el.textContent = JSON.stringify(ev.data, null, 2); | ||
| }); | ||
|
|
||
| Events.On("secondInstance", (ev) => { | ||
| const el = document.getElementById("evt-si"); | ||
| const data = ev.data; | ||
| el.className = data.found ? "ok" : "missing"; | ||
| el.textContent = JSON.stringify(data, null, 2); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| version: '3' | ||
|
|
||
| tasks: | ||
| go:mod:tidy: | ||
| internal: true | ||
| cmds: | ||
| - go mod tidy |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions
34
v3/examples/single-instance-url-scheme/build/darwin/Info.dev.plist
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>CFBundlePackageType</key> | ||
| <string>APPL</string> | ||
| <key>CFBundleName</key> | ||
| <string>Single Instance URL Scheme Repro (dev)</string> | ||
| <key>CFBundleExecutable</key> | ||
| <string>single-instance-url-scheme</string> | ||
| <key>CFBundleIdentifier</key> | ||
| <string>com.wails.example.single-instance-url-scheme.dev</string> | ||
| <key>CFBundleVersion</key> | ||
| <string>0.0.1</string> | ||
| <key>CFBundleShortVersionString</key> | ||
| <string>0.0.1</string> | ||
| <key>CFBundleIconFile</key> | ||
| <string>icons</string> | ||
| <key>LSMinimumSystemVersion</key> | ||
| <string>10.15.0</string> | ||
| <key>NSHighResolutionCapable</key> | ||
| <string>true</string> | ||
| <key>CFBundleURLTypes</key> | ||
| <array> | ||
| <dict> | ||
| <key>CFBundleURLName</key> | ||
| <string>com.wails.example.single-instance-url-scheme.dev</string> | ||
| <key>CFBundleURLSchemes</key> | ||
| <array> | ||
| <string>wails-single-url</string> | ||
| </array> | ||
| </dict> | ||
| </array> | ||
| </dict> | ||
| </plist> |
34 changes: 34 additions & 0 deletions
34
v3/examples/single-instance-url-scheme/build/darwin/Info.plist
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>CFBundlePackageType</key> | ||
| <string>APPL</string> | ||
| <key>CFBundleName</key> | ||
| <string>Single Instance URL Scheme Repro</string> | ||
| <key>CFBundleExecutable</key> | ||
| <string>single-instance-url-scheme</string> | ||
| <key>CFBundleIdentifier</key> | ||
| <string>com.wails.example.single-instance-url-scheme</string> | ||
| <key>CFBundleVersion</key> | ||
| <string>0.0.1</string> | ||
| <key>CFBundleShortVersionString</key> | ||
| <string>0.0.1</string> | ||
| <key>CFBundleIconFile</key> | ||
| <string>icons</string> | ||
| <key>LSMinimumSystemVersion</key> | ||
| <string>10.15.0</string> | ||
| <key>NSHighResolutionCapable</key> | ||
| <string>true</string> | ||
| <key>CFBundleURLTypes</key> | ||
| <array> | ||
| <dict> | ||
| <key>CFBundleURLName</key> | ||
| <string>com.wails.example.single-instance-url-scheme</string> | ||
| <key>CFBundleURLSchemes</key> | ||
| <array> | ||
| <string>wails-single-url</string> | ||
| </array> | ||
| </dict> | ||
| </array> | ||
| </dict> | ||
| </plist> |
49 changes: 49 additions & 0 deletions
49
v3/examples/single-instance-url-scheme/build/darwin/Taskfile.yml
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| version: '3' | ||
|
|
||
| tasks: | ||
| build: | ||
| summary: Builds the macOS binary | ||
| cmds: | ||
| - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} | ||
| vars: | ||
| BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' | ||
| DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' | ||
| OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' | ||
| env: | ||
| GOOS: darwin | ||
| CGO_ENABLED: 1 | ||
| GOARCH: '{{.ARCH | default ARCH}}' | ||
| CGO_CFLAGS: "-mmacosx-version-min=10.15" | ||
| CGO_LDFLAGS: "-mmacosx-version-min=10.15" | ||
| MACOSX_DEPLOYMENT_TARGET: "10.15" | ||
| PRODUCTION: '{{.PRODUCTION | default "false"}}' | ||
|
|
||
| package: | ||
| summary: Packages the build into an `.app` bundle | ||
| deps: | ||
| - task: build | ||
| vars: | ||
| PRODUCTION: "true" | ||
| cmds: | ||
| - task: create:app:bundle | ||
|
|
||
| create:app:bundle: | ||
| summary: Creates an `.app` bundle | ||
| cmds: | ||
| - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} | ||
| - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources | ||
| - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS | ||
| - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents | ||
| - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app | ||
|
|
||
| run: | ||
| cmds: | ||
| - task: build | ||
| - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources} | ||
| - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources | ||
| - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS | ||
| - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist | ||
| - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app | ||
| # Register the .app with LaunchServices so the custom URL scheme is recognised. | ||
| - /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -f "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app" | ||
| - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' | ||
Binary file not shown.
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.