singleton_process/
lib.rs

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