Skip to main content

win_desktop_utils/
instance.rs

1use std::ffi::OsStr;
2use std::os::windows::ffi::OsStrExt;
3
4use windows::core::PCWSTR;
5use windows::Win32::Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HANDLE};
6use windows::Win32::System::Threading::CreateMutexW;
7
8use crate::error::{Error, Result};
9
10/// Guard that keeps the named single-instance mutex alive for the current process.
11///
12/// Dropping this value releases the underlying mutex handle.
13#[must_use = "keep this guard alive for as long as you want to hold the single-instance lock"]
14#[derive(Debug)]
15pub struct InstanceGuard {
16    handle: HANDLE,
17}
18
19impl Drop for InstanceGuard {
20    fn drop(&mut self) {
21        unsafe {
22            let _ = CloseHandle(self.handle);
23        }
24    }
25}
26
27fn to_wide_str(value: &str) -> Vec<u16> {
28    OsStr::new(value)
29        .encode_wide()
30        .chain(std::iter::once(0))
31        .collect()
32}
33
34/// Attempts to acquire a named process-wide single-instance guard.
35///
36/// Returns `Ok(Some(InstanceGuard))` for the first instance and `Ok(None)` if another
37/// instance with the same `app_id` is already running.
38///
39/// The mutex name is derived from `app_id` using a `Local\...` namespace, so the
40/// single-instance behavior is scoped to the current Windows session.
41///
42/// Keep the returned guard alive for as long as the current process should continue
43/// to own the single-instance lock.
44///
45/// # Errors
46///
47/// Returns [`Error::InvalidInput`] if `app_id` is empty or whitespace only.
48/// Returns [`Error::WindowsApi`] if `CreateMutexW` fails.
49///
50/// # Examples
51///
52/// ```
53/// let app_id = format!("demo-app-{}", std::process::id());
54/// let guard = win_desktop_utils::single_instance(&app_id)?;
55/// assert!(guard.is_some());
56/// # Ok::<(), win_desktop_utils::Error>(())
57/// ```
58#[must_use = "store the returned guard for as long as the process should be considered the active instance"]
59pub fn single_instance(app_id: &str) -> Result<Option<InstanceGuard>> {
60    if app_id.trim().is_empty() {
61        return Err(Error::InvalidInput("app_id cannot be empty"));
62    }
63
64    let mutex_name = format!("Local\\win_desktop_utils_{app_id}");
65    let mutex_name_w = to_wide_str(&mutex_name);
66
67    let handle =
68        unsafe { CreateMutexW(None, false, PCWSTR(mutex_name_w.as_ptr())) }.map_err(|e| {
69            Error::WindowsApi {
70                context: "CreateMutexW",
71                code: e.code().0,
72            }
73        })?;
74
75    let last_error = unsafe { GetLastError() };
76
77    if last_error == ERROR_ALREADY_EXISTS {
78        unsafe {
79            let _ = CloseHandle(handle);
80        }
81        Ok(None)
82    } else {
83        Ok(Some(InstanceGuard { handle }))
84    }
85}