tauri-apps / tauri-apps/plugins-workspace

single-instance: Windows secondary should grant foreground rights to the primary before notifying it

Open Beginner friendly
#3,548 9 comments 0 reactions 0 assignees View on GitHub
platform: windows plugin: single-instance type: feature request
Dominant language
Rust
Stars
1.8k
Forks
602
Avg merge
4d 14h
Merged PRs (30d)
9

Description

## Summary

On Windows, the secondary instance hands `argv`/`cwd` to the primary and exits without granting it the right to set the foreground window. The primary's `SetForegroundWindow` is therefore refused, and applications fall back on the toolkit's focus hack — which synthesizes keyboard input — to come to the front.

One call to `AllowSetForegroundWindow` in the secondary, before the `WM_COPYDATA`, removes the need for that fallback and restores the OS's own handling of "the user changed their mind".

## Current behaviour (2.4.3)

`src/platform_impl/windows.rs`, secondary branch:

```rust
let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr()); // L74
if !hwnd.is_null() {
// … pack cwd|argv into COPYDATASTRUCT …
SendMessageW(hwnd, WM_COPYDATA, 0, &cds as *const _ as _); // L91
app.cleanup_before_exit();
std::process::exit(0); // L94
}
```

The launching shell transfers foreground rights to the **secondary**, since that is the process it started. The process that has to raise a window is the **primary**, which has usually been in the background for some time. Measured against the primary, none of the documented conditions for `SetForegroundWindow` hold — it is not the foreground process, it was not started by the foreground process, and it did not receive the last input event. The call is refused, and per the same page Windows flashes the taskbar button instead.

## What that costs applications

The natural thing for a callback to do is `window.set_focus()`. In tao 0.35.3 that reaches `force_window_active`, which calls `SetForegroundWindow` and, when the OS refuses, synthesizes an Alt keypress through `SendInput` to fake recent user input, then retries. tao's own comment scopes the hack to window creation — *"We only call this function in the window creation, so it should be fine"* — but the single-instance path invokes it much later, from a process that has been idle.

Two consequences:

1. The synthesized Alt is delivered to whatever window is currently foreground, which is the user's, not the application's. A lone Alt press activates the menu bar in most Windows applications.
2. It activates unconditionally. If the user double-clicked a file and then switched away while the handoff was still in flight, the application takes focus back regardless.

## Proposed change

In the secondary branch, between `FindWindowW` and `SendMessageW`:

```rust
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, &mut pid);
if pid != 0 {
AllowSetForegroundWindow(pid);
}
```

Both symbols live in `Win32::UI::WindowsAndMessaging`, already enabled in the crate's `windows-sys` features — no new dependency and no new feature flag.

## Why this does not add a focus-stealing vector

`AllowSetForegroundWindow` grants a right the OS revokes on its own:

> The process specified by the *dwProcessId* parameter loses the ability to set the foreground window the next time that either the user generates input, unless the input is directed at that process […]

That fixes consequence (2) for free: if the user moves on during the handoff, the grant lapses, the primary's `SetForegroundWindow` fails, and Windows flashes the taskbar button — which is what the user's action now implies. The plugin does not have to make that judgement itself, and callback policy is unchanged: an application that calls `set_focus()` makes the same call, it simply succeeds through the documented path when it should and fails when it should.

## Prior art

- **Chromium**, `chrome/browser/win/chrome_process_finder.cc`, inside `AttemptToNotifyRunningChrome`, immediately before the `WM_COPYDATA` send: `::AllowSetForegroundWindow(process_id);`, commented *"Allow the current running browser window to make itself the foreground window (otherwise it will just flash in the taskbar)."*
- **VS Code**, `src/vs/code/electron-main/main.ts`, `windowsAllowSetForegroundWindow()`, called from `claimInstance` before the second instance exits. Electron does not do this on an application's behalf, so VS Code ships a native module (`windows-foreground-love`) for this single call.

## Scope

- Windows only. macOS lets any application activate itself, and the X11/Wayland path is timestamp-based; neither has a right that disappears when the notifying process exits.
- No change to the public callback signature, or to what an application decides to do on notification.
- Independent of #3495 and #3542, which concern ownership and delivery races. This concerns what the secondary owes the primary once delivery is happening at all, and applies to whichever branch ends up sending the notification.

## Willing to submit a PR

Happy to open one against `plugins/single-instance` with the change above, if the direction is agreeable.

## Environment

- `tauri-plugin-single-instance` 2.4.3
- tao 0.35.3, for the fallback behaviour described above
- Windows

Contributor guide

Open the contributing guide

Research direction

Start in src/platform_impl/windows.rs at the secondary-instance branch, reviewing the existing FindWindowW and WM_COPYDATA handoff. Verify the Windows API imports and the foreground-rights call sequence, then test the single-instance handoff on Windows. Done means the primary can follow the documented foreground-window behavior without changing the callback or non-Windows paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
desktop, operating-systems
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.