win_desktop_utils/
instance.rs1use std::ffi::OsStr;
4use std::os::windows::ffi::OsStrExt;
5
6use windows::core::PCWSTR;
7use windows::Win32::Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HANDLE};
8use windows::Win32::System::Threading::CreateMutexW;
9
10use crate::error::{Error, Result};
11
12#[must_use = "keep this guard alive for as long as you want to hold the single-instance lock"]
16#[derive(Debug)]
17pub struct InstanceGuard {
18 handle: HANDLE,
19}
20
21impl Drop for InstanceGuard {
22 fn drop(&mut self) {
23 unsafe {
24 let _ = CloseHandle(self.handle);
25 }
26 }
27}
28
29fn to_wide_str(value: &str) -> Vec<u16> {
30 OsStr::new(value)
31 .encode_wide()
32 .chain(std::iter::once(0))
33 .collect()
34}
35
36fn validate_app_id(app_id: &str) -> Result<()> {
37 if app_id.trim().is_empty() {
38 return Err(Error::InvalidInput("app_id cannot be empty"));
39 }
40
41 if app_id.contains('\0') {
42 return Err(Error::InvalidInput("app_id cannot contain NUL bytes"));
43 }
44
45 if app_id.contains('\\') {
46 return Err(Error::InvalidInput("app_id cannot contain backslashes"));
47 }
48
49 Ok(())
50}
51
52#[must_use = "store the returned guard for as long as the process should be considered the active instance"]
79pub fn single_instance(app_id: &str) -> Result<Option<InstanceGuard>> {
80 validate_app_id(app_id)?;
81
82 let mutex_name = format!("Local\\win_desktop_utils_{app_id}");
83 let mutex_name_w = to_wide_str(&mutex_name);
84
85 let handle =
86 unsafe { CreateMutexW(None, false, PCWSTR(mutex_name_w.as_ptr())) }.map_err(|e| {
87 Error::WindowsApi {
88 context: "CreateMutexW",
89 code: e.code().0,
90 }
91 })?;
92
93 let last_error = unsafe { GetLastError() };
94
95 if last_error == ERROR_ALREADY_EXISTS {
96 unsafe {
97 let _ = CloseHandle(handle);
98 }
99 Ok(None)
100 } else {
101 Ok(Some(InstanceGuard { handle }))
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::validate_app_id;
108
109 #[test]
110 fn validate_app_id_rejects_empty_string() {
111 let result = validate_app_id("");
112 assert!(matches!(
113 result,
114 Err(crate::Error::InvalidInput("app_id cannot be empty"))
115 ));
116 }
117
118 #[test]
119 fn validate_app_id_rejects_backslashes() {
120 let result = validate_app_id(r"demo\app");
121 assert!(matches!(
122 result,
123 Err(crate::Error::InvalidInput(
124 "app_id cannot contain backslashes"
125 ))
126 ));
127 }
128
129 #[test]
130 fn validate_app_id_rejects_nul_bytes() {
131 let result = validate_app_id("demo\0app");
132 assert!(matches!(
133 result,
134 Err(crate::Error::InvalidInput(
135 "app_id cannot contain NUL bytes"
136 ))
137 ));
138 }
139}