singleton_process/
lib.rs

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(&current_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::env::{current_exe, temp_dir};
81    use std::fs::{File, OpenOptions};
82    use std::io::{Read, Seek, Write};
83    use std::mem::size_of_val;
84
85    use nix::errno::Errno;
86    use nix::fcntl::{Flock, FlockArg};
87    use nix::sys::signal::{kill, Signal};
88    use nix::unistd::Pid;
89
90    pub struct SingletonProcess {
91        _file_lock: Flock<File>,
92    }
93
94    impl SingletonProcess {
95        pub fn try_new(name: Option<&str>, keep_new_process: bool) -> crate::Result<Self> {
96            type PidType = u32;
97
98            let this_pid: PidType = std::process::id();
99            let pid_size = size_of_val(&this_pid);
100
101            let lock_file_name = temp_dir().join(format!("{}_singleton_process.lock", name.unwrap_or(&current_exe()?.file_name().unwrap().to_string_lossy())));
102            let lock_file = OpenOptions::new().read(true).write(true).create(true).open(&lock_file_name)?;
103
104            let (mut file_lock, is_first) = match Flock::lock(lock_file, FlockArg::LockExclusiveNonblock) {
105                Ok(lock) => {
106                    lock.relock(FlockArg::LockSharedNonblock)?;
107                    lock.set_len(pid_size as _)?;
108
109                    (lock, true)
110                }
111                Err((f, Errno::EAGAIN)) => (Flock::lock(f, FlockArg::LockSharedNonblock).map_err(|(_, e)| e)?, false),
112                Err((_, e)) => panic!("flock failed with errno: {}", e),
113            };
114
115            if !is_first {
116                let mut pid_buffer = this_pid.to_le_bytes();
117                file_lock.read_exact(&mut pid_buffer)?;
118                file_lock.rewind()?;
119
120                let other_pid = PidType::from_le_bytes(pid_buffer);
121                assert_ne!(other_pid, 0);
122
123                if other_pid != this_pid {
124                    if keep_new_process {
125                        kill(Pid::from_raw(other_pid as _), Signal::SIGTERM).ok();
126                    } else {
127                        std::process::exit(0);
128                    }
129                }
130            }
131
132            file_lock.write(&this_pid.to_le_bytes())?;
133
134            Ok(Self { _file_lock: file_lock })
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use std::path::PathBuf;
142    use std::process::Command;
143
144    use if_chain::if_chain;
145
146    use super::*;
147
148    fn get_parent_process_exe(system: &mut sysinfo::System) -> Option<PathBuf> {
149        use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, UpdateKind};
150
151        system.refresh_processes_specifics(ProcessesToUpdate::All, true, ProcessRefreshKind::nothing().with_exe(UpdateKind::OnlyIfNotSet));
152
153        if_chain! {
154            if let Ok(current_pid) = sysinfo::get_current_pid();
155            if let Some(current_process) = system.process(current_pid);
156            if let Some(parent_pid) = current_process.parent();
157            if let Some(parent_process) = system.process(parent_pid);
158            then {
159                parent_process.exe().map(|p| p.to_path_buf())
160            } else {
161                None
162            }
163        }
164    }
165
166    #[test]
167    fn test_with_name() -> Result<()> {
168        SingletonProcess::try_new(Some(&"my_unique_name"), true)?;
169
170        Ok(())
171    }
172
173    #[test]
174    fn test_reentrant() -> Result<()> {
175        std::mem::forget(SingletonProcess::try_new(None, true)?);
176        std::mem::forget(SingletonProcess::try_new(None, false)?);
177
178        Ok(())
179    }
180
181    #[test]
182    #[function_name::named]
183    fn test_keep_old_process() -> Result<()> {
184        let mut system = sysinfo::System::new();
185        let parent_exe_pre = get_parent_process_exe(&mut system);
186        std::mem::forget(SingletonProcess::try_new(None, false)?);
187        let current_exe = std::env::current_exe()?;
188
189        if let Some(p) = parent_exe_pre {
190            assert_ne!(p, current_exe);
191        }
192
193        let mut cmd = Command::new(current_exe);
194        cmd.arg(function_name!());
195        assert!(cmd.status()?.success());
196
197        Ok(())
198    }
199
200    #[test]
201    #[function_name::named]
202    fn test_keep_new_process() -> Result<()> {
203        let mut system = sysinfo::System::new();
204        let parent_exe_pre = get_parent_process_exe(&mut system);
205        std::mem::forget(SingletonProcess::try_new(None, true)?);
206        let current_exe = std::env::current_exe()?;
207
208        if_chain! {
209            if let Some(p) = parent_exe_pre;
210            if p == current_exe;
211            then {
212                assert!(get_parent_process_exe(&mut system).is_none());
213            } else {
214                use nix::sys::signal::*;
215
216                extern "C" fn exit_on_sigterm(_signal: i32) {
217                    std::process::exit(0);
218                }
219
220                // make process exit gracefully on SIGTERM to avoid test failure
221                unsafe { signal(Signal::SIGTERM, SigHandler::Handler(exit_on_sigterm)).unwrap(); }
222
223                let mut cmd = Command::new(current_exe);
224                cmd.arg(function_name!());
225                cmd.status()?;
226            }
227        }
228
229        Ok(())
230    }
231}