use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows::core::PCWSTR;
use windows::Win32::Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, HANDLE};
use windows::Win32::System::Threading::CreateMutexW;
use crate::error::{Error, Result};
#[must_use = "keep this guard alive for as long as you want to hold the single-instance lock"]
#[derive(Debug)]
pub struct InstanceGuard {
handle: HANDLE,
}
impl Drop for InstanceGuard {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.handle);
}
}
}
fn to_wide_str(value: &str) -> Vec<u16> {
OsStr::new(value)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn validate_app_id(app_id: &str) -> Result<()> {
if app_id.trim().is_empty() {
return Err(Error::InvalidInput("app_id cannot be empty"));
}
if app_id.contains('\0') {
return Err(Error::InvalidInput("app_id cannot contain NUL bytes"));
}
if app_id.contains('\\') {
return Err(Error::InvalidInput("app_id cannot contain backslashes"));
}
Ok(())
}
#[must_use = "store the returned guard for as long as the process should be considered the active instance"]
pub fn single_instance(app_id: &str) -> Result<Option<InstanceGuard>> {
validate_app_id(app_id)?;
let mutex_name = format!("Local\\win_desktop_utils_{app_id}");
let mutex_name_w = to_wide_str(&mutex_name);
let handle =
unsafe { CreateMutexW(None, false, PCWSTR(mutex_name_w.as_ptr())) }.map_err(|e| {
Error::WindowsApi {
context: "CreateMutexW",
code: e.code().0,
}
})?;
let last_error = unsafe { GetLastError() };
if last_error == ERROR_ALREADY_EXISTS {
unsafe {
let _ = CloseHandle(handle);
}
Ok(None)
} else {
Ok(Some(InstanceGuard { handle }))
}
}
#[cfg(test)]
mod tests {
use super::validate_app_id;
#[test]
fn validate_app_id_rejects_empty_string() {
let result = validate_app_id("");
assert!(matches!(
result,
Err(crate::Error::InvalidInput("app_id cannot be empty"))
));
}
#[test]
fn validate_app_id_rejects_backslashes() {
let result = validate_app_id(r"demo\app");
assert!(matches!(
result,
Err(crate::Error::InvalidInput(
"app_id cannot contain backslashes"
))
));
}
#[test]
fn validate_app_id_rejects_nul_bytes() {
let result = validate_app_id("demo\0app");
assert!(matches!(
result,
Err(crate::Error::InvalidInput(
"app_id cannot contain NUL bytes"
))
));
}
}