use std::io;
use std::os::windows::io::RawHandle;
use std::ptr;
#[derive(Debug)]
pub enum LockOutcome {
Acquired,
Held,
Failed(io::Error),
}
const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
const LOCKFILE_FAIL_IMMEDIATELY: u32 = 0x0000_0001;
const ERROR_LOCK_VIOLATION: i32 = 33;
#[repr(C)]
struct Overlapped {
internal: usize,
internal_high: usize,
offset_union: u64,
event: RawHandle,
}
const SENTINEL_OFFSET_LOW: u32 = 1 << 20;
impl Overlapped {
fn sentinel() -> Self {
Self {
internal: 0,
internal_high: 0,
offset_union: SENTINEL_OFFSET_LOW as u64,
event: ptr::null_mut(),
}
}
}
unsafe extern "system" {
fn LockFileEx(
file: RawHandle,
flags: u32,
reserved: u32,
bytes_low: u32,
bytes_high: u32,
overlapped: *const Overlapped,
) -> i32;
fn UnlockFileEx(
file: RawHandle,
reserved: u32,
bytes_low: u32,
bytes_high: u32,
overlapped: *const Overlapped,
) -> i32;
fn OpenProcess(desired_access: u32, inherit: i32, pid: u32) -> RawHandle;
fn GetCurrentProcess() -> RawHandle;
fn TerminateProcess(process: RawHandle, exit_code: u32) -> i32;
fn CloseHandle(object: RawHandle) -> i32;
fn QueryFullProcessImageNameW(
process: RawHandle,
flags: u32,
exe_name: *mut u16,
size: *mut u32,
) -> i32;
fn GetProcessTimes(
process: RawHandle,
creation: *mut Filetime,
exit: *mut Filetime,
kernel: *mut Filetime,
user: *mut Filetime,
) -> i32;
}
#[repr(C)]
struct Filetime {
low: u32,
high: u32,
}
impl Filetime {
fn ticks(self) -> u64 {
((self.high as u64) << 32) | self.low as u64
}
}
pub fn own_creation_ticks() -> Option<u64> {
let mut creation = Filetime { low: 0, high: 0 };
let mut ignored = Filetime { low: 0, high: 0 };
let ok = unsafe {
GetProcessTimes(
GetCurrentProcess(),
&mut creation,
&mut ignored,
&mut ignored,
&mut ignored,
)
};
(ok != 0).then(|| creation.ticks())
}
pub fn creation_ticks_of(pid: u32) -> Option<u64> {
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
let h = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if h.is_null() {
return None;
}
let mut creation = Filetime { low: 0, high: 0 };
let mut ignored = Filetime { low: 0, high: 0 };
let ok = unsafe { GetProcessTimes(h, &mut creation, &mut ignored, &mut ignored, &mut ignored) };
unsafe { CloseHandle(h) };
(ok != 0).then(|| creation.ticks())
}
pub fn exclusive(handle: RawHandle, nb: bool) -> LockOutcome {
let ov = Overlapped::sentinel();
let flags = LOCKFILE_EXCLUSIVE_LOCK | if nb { LOCKFILE_FAIL_IMMEDIATELY } else { 0 };
let ok = unsafe { LockFileEx(handle, flags, 0, u32::MAX, u32::MAX, &ov) };
if ok != 0 {
return LockOutcome::Acquired;
}
let err = io::Error::last_os_error();
if nb && err.raw_os_error() == Some(ERROR_LOCK_VIOLATION) {
LockOutcome::Held
} else {
LockOutcome::Failed(err)
}
}
pub fn unlock(handle: RawHandle) -> io::Result<()> {
let ov = Overlapped::sentinel();
if unsafe { UnlockFileEx(handle, 0, u32::MAX, u32::MAX, &ov) } != 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn terminate(pid: u32, expected_creation_ticks: Option<u64>) -> io::Result<()> {
const PROCESS_TERMINATE: u32 = 0x0001;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
let self_exe = match normalize_exe_path(&std::env::current_exe()?) {
Some(p) => p,
None => return Err(io::Error::other("cannot resolve own exe path")),
};
let Some(expected) = expected_creation_ticks else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"refusing to terminate PID {pid}: the lock stamp records no creation time, \
so PID reuse cannot be ruled out"
),
));
};
let rights = PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION;
let h = unsafe { OpenProcess(rights, 0, pid) };
if h.is_null() {
return Err(io::Error::last_os_error());
}
let mut buf = [0u16; 32_768];
let mut size = buf.len() as u32;
let queried = unsafe { QueryFullProcessImageNameW(h, 0, buf.as_mut_ptr(), &mut size) };
let outcome = if queried == 0 {
Err(io::Error::last_os_error())
} else {
let path = String::from_utf16_lossy(&buf[..size as usize]);
match normalize_exe_path(std::path::Path::new(&path)) {
Some(target) if target == self_exe => {
let mut creation = Filetime { low: 0, high: 0 };
let mut ignored = Filetime { low: 0, high: 0 };
if unsafe {
GetProcessTimes(h, &mut creation, &mut ignored, &mut ignored, &mut ignored)
} == 0
{
Err(io::Error::last_os_error())
} else if creation.ticks() == expected {
if unsafe { TerminateProcess(h, 1) } != 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"refusing to terminate PID {pid}: same image, but the process \
was created at a different instant than the lock stamp records \
(PID reuse)"
),
))
}
}
Some(_) => Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("refusing to terminate PID {pid}: image {path:?} is not this binary"),
)),
None => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("refusing to terminate PID {pid}: unreadable image path {path:?}"),
)),
}
};
unsafe { CloseHandle(h) };
outcome
}
fn normalize_exe_path(p: &std::path::Path) -> Option<String> {
Some(
p.to_string_lossy()
.to_lowercase()
.trim_end_matches('\\')
.to_string(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::windows::io::AsRawHandle;
#[test]
fn own_creation_ticks_is_present_and_stable() {
let a = own_creation_ticks().expect("GetProcessTimes must work on Windows");
let b = own_creation_ticks().expect("GetProcessTimes must work on Windows");
assert_eq!(a, b, "a process's creation time must not change");
assert!(a > 0, "a creation time of zero means the query lied");
}
#[test]
fn creation_ticks_of_matches_the_own_reading_and_refuses_a_bogus_pid() {
let me = std::process::id();
assert_eq!(
creation_ticks_of(me),
own_creation_ticks(),
"the reader and the writer must share one clock"
);
assert_eq!(
creation_ticks_of(u32::MAX),
None,
"an invalid PID has no creation time to report"
);
}
#[test]
fn non_blocking_second_handle_reports_held() {
let dir = std::env::temp_dir().join(format!("winlock-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test.lock");
let open = || {
std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&path)
.unwrap()
};
let a = open();
let b = open();
assert!(matches!(
exclusive(a.as_raw_handle(), true),
LockOutcome::Acquired
));
assert!(matches!(
exclusive(b.as_raw_handle(), true),
LockOutcome::Held
));
assert!(unlock(a.as_raw_handle()).is_ok());
assert!(matches!(
exclusive(b.as_raw_handle(), true),
LockOutcome::Acquired
));
drop(a);
drop(b);
std::fs::remove_dir_all(&dir).expect("test temp dir cleanup");
}
}