Skip to content

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

Description

@PathGao

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:

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:

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

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions