tauri-apps / tauri-apps/plugins-workspace
single-instance (Windows): second instance survives when FindWindowW misses the startup window
- Dominant language
- Rust
- Stars
- 1.8k
- Forks
- 602
- Avg merge
- 4d 14h
- Merged PRs (30d)
- 9
Description
### Summary
On Windows, a second instance survives `tauri-plugin-single-instance` when it starts during the window between the first instance's `CreateMutexW` and its `create_event_target_window`. The exit path is nested inside `if !hwnd.is_null()` and there is no `else`, so a `FindWindowW` miss falls through and the second process boots completely.
Version: `2.4.2`. The same code is present on `v2` HEAD (`2.4.4`) — `2.4.3` was a macOS thread fix and `2.4.4` was documentation, so upgrading does not change this.
### The code
`single-instance/src/platform_impl/windows.rs` (2.4.2, lines ~70-105):
```rust
let hmutex = unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) };
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
unsafe {
let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr());
if !hwnd.is_null() {
// ... SendMessageW(WM_COPYDATA) ...
app.cleanup_before_exit();
std::process::exit(0);
}
// <-- no `else`: hwnd == null falls through and the instance keeps booting
}
} else {
app.manage(MutexHandle(hmutex as _));
// ...
let hwnd = create_event_target_window::(&class_name, &window_name, userdata);
app.manage(TargetWindowHandle(hwnd as _));
}
```
The first instance publishes the mutex *before* it creates the event target window. Any launch that lands in that interval observes `ERROR_ALREADY_EXISTS`, finds no window to message, and then simply continues into `Builder::run` as a full second instance.
### Reproduction
Two launches with no pause between them — e.g. a fast double-click on the shortcut, or:
```powershell
Start-Process .\my-app.exe; Start-Process .\my-app.exe
```
Expected: one process. Observed (intermittently, the window is a few milliseconds wide): two `my-app.exe` in Task Manager, both fully initialized.
Autostart plus an impatient user double-clicking the shortcut makes this land more often than the raw timing suggests.
### Impact
For a plain UI app the visible symptom is two windows. For an app whose single-instance guarantee protects an exclusive resource it is worse: in our case each instance opens its own SQLite connection pool over the same file, and the second writer produced a sustained `database is locked` on a point-of-sale machine until the operator restarted. The plugin is the documented way to prevent exactly that, so the failure is silent — nothing logs, nothing errors.
### Suggested fix
Exit in the `hwnd.is_null()` case as well. The first instance is known to exist (the mutex proves it); it simply is not ready to be messaged yet, which is not a reason to keep running:
```rust
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
let hwnd = unsafe { FindWindowW(class_name.as_ptr(), window_name.as_ptr()) };
if !hwnd.is_null() {
// ... SendMessageW ...
}
app.cleanup_before_exit();
std::process::exit(0);
}
```
A short bounded retry on `FindWindowW` before exiting would additionally preserve deep-link forwarding for launches that land in the gap, instead of dropping the arguments. Either way the process should not survive.
Creating the event target window *before* the mutex would also close the gap, at the cost of a window with no mutex behind it if setup fails afterwards.
### Workaround
We create our own named mutex as the first statement of `run()` (before `tauri::Builder`, therefore before any plugin `setup`), and exit only when the plugin's event window is absent — so the plugin keeps handling focus and deep links in every other case.
Contributor guide
Research direction
Read single-instance/src/platform_impl/windows.rs around CreateMutexW, FindWindowW, and the startup branch. Reproduce the rapid two-launch case on Windows, then verify that the second process no longer completes startup when the mutex exists but the event window is not yet found; the linked pull request may already cover the work.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, tauri
- Domain
- desktop, operating-systems
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100