1pub use self::inner::*;
2
3#[derive(Debug, thiserror::Error)]
4pub enum SingletonProcessError {
5 #[error("I/O error: {0}")]
6 Io(#[from] std::io::Error),
7
8 #[cfg(target_os = "windows")]
9 #[error("Windows error: {0}")]
10 Windows(windows::core::Error),
11
12 #[cfg(any(target_os = "linux", target_os = "android"))]
13 #[error("POSIX error: {0}")]
14 Posix(#[from] nix::errno::Errno),
15}
16
17type Result<T> = std::result::Result<T, SingletonProcessError>;
18
19#[cfg(target_os = "windows")]
20mod inner {
21 use std::env::current_exe;
22 use std::mem::size_of_val;
23
24 use windows::core::PCSTR;
25 use windows::Win32::Foundation::{GetLastError, ERROR_ALREADY_EXISTS, HANDLE, INVALID_HANDLE_VALUE};
26 use windows::Win32::System::Memory::{CreateFileMappingA, MapViewOfFile, UnmapViewOfFile, FILE_MAP_READ, FILE_MAP_WRITE, PAGE_READWRITE};
27 use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
28
29 use crate::SingletonProcessError;
30
31 pub struct SingletonProcess {
32 _h_mapping: windows::core::Owned<HANDLE>,
33 }
34
35 impl SingletonProcess {
36 pub fn try_new(name: Option<&str>, keep_new_process: bool) -> crate::Result<Self> {
37 let this_pid = std::process::id();
38 let pid_size = size_of_val(&this_pid);
39
40 let mapping_name = format!("Global\\{}\0", name.unwrap_or(¤t_exe()?.file_name().unwrap().to_string_lossy()));
41
42 unsafe {
43 let h_mapping = CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, pid_size as _, PCSTR(mapping_name.as_ptr()))?;
44 let mapped_buffer = MapViewOfFile(h_mapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, pid_size);
45 let mapped_value = mapped_buffer.Value as *mut _;
46
47 if GetLastError() == ERROR_ALREADY_EXISTS {
48 let other_pid = *mapped_value;
49 assert_ne!(other_pid, 0);
50
51 if other_pid != this_pid {
52 if keep_new_process {
53 let h_other_proc = OpenProcess(PROCESS_TERMINATE, false, other_pid)?;
54 TerminateProcess(h_other_proc, 0)?;
55 } else {
56 std::process::exit(0);
57 }
58 }
59 }
60
61 *mapped_value = this_pid;
62 UnmapViewOfFile(mapped_buffer)?;
63
64 Ok(SingletonProcess {
65 _h_mapping: windows::core::Owned::new(h_mapping),
66 })
67 }
68 }
69 }
70
71 impl From<windows::core::Error> for SingletonProcessError {
72 fn from(e: windows::core::Error) -> Self {
73 SingletonProcessError::Windows(e)
74 }
75 }
76}
77
78#[cfg(any(target_os = "linux", target_os = "android"))]
79mod inner {
80 use std::convert::TryInto;
81 use std::env::{current_exe, temp_dir};
82 use std::fs::{File, OpenOptions};
83 use std::io::{Read, Seek, Write};
84 use std::mem::size_of_val;
85
86 use nix::errno::Errno;
87 use nix::fcntl::{Flock, FlockArg};
88 use nix::sys::signal::{kill, Signal};
89 use nix::unistd::Pid;
90
91 pub struct SingletonProcess {
92 _file_lock: Flock<File>,
93 }
94
95 impl SingletonProcess {
96 pub fn try_new(name: Option<&str>, keep_new_process: bool) -> crate::Result<Self> {
97 let this_pid = Pid::this();
98 let pid_size = size_of_val(&this_pid);
99
100 let lock_file_name = temp_dir().join(format!("{}_singleton_process.lock", name.unwrap_or(¤t_exe()?.file_name().unwrap().to_string_lossy())));
101 let lock_file = OpenOptions::new().read(true).write(true).create(true).open(&lock_file_name)?;
102
103 let (mut file_lock, is_first) = match Flock::lock(lock_file, FlockArg::LockExclusiveNonblock) {
104 Ok(lock) => {
105 lock.relock(FlockArg::LockSharedNonblock)?;
106 lock.set_len(pid_size as _)?;
107
108 (lock, true)
109 }
110 Err((f, Errno::EAGAIN)) => (Flock::lock(f, FlockArg::LockSharedNonblock).map_err(|(_, e)| e)?, false),
111 Err((_, e)) => panic!("flock failed with errno: {}", e),
112 };
113
114 if !is_first {
115 let mut pid_buffer = vec![0; pid_size];
116 file_lock.read_exact(&mut pid_buffer)?;
117 file_lock.rewind()?;
118
119 let other_pid = Pid::from_raw(libc::pid_t::from_le_bytes(pid_buffer.try_into().unwrap()));
120 assert_ne!(other_pid.as_raw(), 0);
121
122 if other_pid != this_pid {
123 if keep_new_process {
124 kill(other_pid, Signal::SIGTERM).ok();
125 } else {
126 std::process::exit(0);
127 }
128 }
129 }
130
131 file_lock.write(&this_pid.as_raw().to_le_bytes())?;
132
133 Ok(Self { _file_lock: file_lock })
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use std::path::PathBuf;
141 use std::process::Command;
142
143 use if_chain::if_chain;
144
145 use super::*;
146
147 fn get_parent_process_exe(system: &mut sysinfo::System) -> Option<PathBuf> {
148 use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, UpdateKind};
149
150 system.refresh_processes_specifics(ProcessesToUpdate::All, true, ProcessRefreshKind::nothing().with_exe(UpdateKind::OnlyIfNotSet));
151
152 if_chain! {
153 if let Ok(current_pid) = sysinfo::get_current_pid();
154 if let Some(current_process) = system.process(current_pid);
155 if let Some(parent_pid) = current_process.parent();
156 if let Some(parent_process) = system.process(parent_pid);
157 then {
158 parent_process.exe().map(|p| p.to_path_buf())
159 } else {
160 None
161 }
162 }
163 }
164
165 #[test]
166 fn test_with_name() -> Result<()> {
167 SingletonProcess::try_new(Some(&"my_unique_name"), true)?;
168
169 Ok(())
170 }
171
172 #[test]
173 fn test_reentrant() -> Result<()> {
174 std::mem::forget(SingletonProcess::try_new(None, true)?);
175 std::mem::forget(SingletonProcess::try_new(None, false)?);
176
177 Ok(())
178 }
179
180 #[test]
181 #[function_name::named]
182 fn test_keep_old_process() -> Result<()> {
183 let mut system = sysinfo::System::new();
184 let parent_exe_pre = get_parent_process_exe(&mut system);
185 std::mem::forget(SingletonProcess::try_new(None, false)?);
186 let current_exe = std::env::current_exe()?;
187
188 if let Some(p) = parent_exe_pre {
189 assert_ne!(p, current_exe);
190 }
191
192 let mut cmd = Command::new(current_exe);
193 cmd.arg(function_name!());
194 assert!(cmd.status()?.success());
195
196 Ok(())
197 }
198
199 #[test]
200 #[function_name::named]
201 fn test_keep_new_process() -> Result<()> {
202 let mut system = sysinfo::System::new();
203 let parent_exe_pre = get_parent_process_exe(&mut system);
204 std::mem::forget(SingletonProcess::try_new(None, true)?);
205 let current_exe = std::env::current_exe()?;
206
207 if_chain! {
208 if let Some(p) = parent_exe_pre;
209 if p == current_exe;
210 then {
211 assert!(get_parent_process_exe(&mut system).is_none());
212 } else {
213 let mut cmd = Command::new(current_exe);
214 cmd.arg(function_name!());
215 cmd.status()?;
216 }
217 }
218
219 Ok(())
220 }
221}