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