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::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(&current_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}