single_instance/single_instance.rs
1//! A rust library for single instance application.
2//!
3//! single-instance provides a single API to check if there are any other running instance.
4//!
5//! ## Detail
6//! On Linux and Android, [`SingleInstance::new`] binds an abstract unix domain socket
7//! with the given name. On Apple platforms (macOS, iOS, tvOS, watchOS), it creates or
8//! opens the file at the given path and takes an exclusive advisory lock on it.
9//!
10//! ### Examples
11//! ```rust
12//! extern crate single_instance;
13//!
14//! use std::thread;
15//! use single_instance::SingleInstance;
16//!
17//! fn main() {
18//! let instance = SingleInstance::new("whatever").unwrap();
19//! assert!(instance.is_single());
20//! }
21//! ```
22//!
23//! #### Keeping the instance alive for the process lifetime
24//!
25//! The [`SingleInstance`] must stay alive for as long as the exclusivity
26//! guarantee is needed, because dropping it releases the lock. Store it for the
27//! lifetime of the process, for example by leaking it:
28//!
29//! ```rust
30//! use single_instance::SingleInstance;
31//!
32//! fn ensure_single_instance(unique_id: &str) -> bool {
33//! let instance = Box::new(SingleInstance::new(unique_id).unwrap());
34//! if instance.is_single() {
35//! Box::leak(instance);
36//! true
37//! } else {
38//! false
39//! }
40//! }
41//! ```
42
43/// The error returned when a [`SingleInstance`] cannot be created.
44#[derive(thiserror::Error, Debug)]
45pub enum Error {
46 /// The underlying operating system operation failed.
47 ///
48 /// Binding the abstract socket (Linux/Android) or creating and locking the
49 /// file (Apple platforms) returned an error.
50 #[error("single instance error")]
51 Io(#[from] std::io::Error),
52}
53
54pub use self::inner::*;
55
56#[cfg(any(target_os = "linux", target_os = "android"))]
57mod inner {
58 #[cfg(target_os = "android")]
59 use std::os::android::net::SocketAddrExt;
60 #[cfg(target_os = "linux")]
61 use std::os::linux::net::SocketAddrExt;
62 use std::{
63 io,
64 os::unix::net::{SocketAddr, UnixListener},
65 };
66
67 use super::Error;
68
69 /// A struct representing one running instance.
70 ///
71 /// The abstract socket is owned by the instance and released on drop, at
72 /// which point another process may claim the same name.
73 pub struct SingleInstance {
74 maybe_listener: Option<UnixListener>,
75 }
76
77 impl SingleInstance {
78 /// Returns a new SingleInstance object.
79 ///
80 /// Returns an error if the abstract socket address cannot be built or if
81 /// binding it fails for a reason other than another instance already
82 /// holding the name.
83 pub fn new(name: &str) -> Result<Self, Error> {
84 let addr = SocketAddr::from_abstract_name(name.as_bytes())?;
85
86 let maybe_listener = match UnixListener::bind_addr(&addr) {
87 Ok(listener) => Some(listener),
88 // Another instance already bound this abstract address.
89 Err(e) if e.kind() == io::ErrorKind::AddrInUse => None,
90 Err(e) => return Err(e.into()),
91 };
92
93 Ok(Self {
94 maybe_listener,
95 })
96 }
97
98 /// Returns whether this instance is single.
99 pub fn is_single(&self) -> bool {
100 self.maybe_listener.is_some()
101 }
102 }
103}
104
105#[cfg(target_vendor = "apple")]
106mod inner {
107 use std::{
108 fs::{File, TryLockError},
109 path::Path,
110 };
111
112 use super::Error;
113
114 /// A struct representing one running instance.
115 ///
116 /// The file is kept open for the lifetime of the instance; the advisory lock
117 /// is released on drop.
118 pub struct SingleInstance {
119 _file: File,
120 is_single: bool,
121 }
122
123 impl SingleInstance {
124 /// Returns a new SingleInstance object.
125 ///
126 /// Returns an error if the file cannot be created or opened, or if locking
127 /// it fails for a reason other than the lock already being held by another
128 /// instance.
129 pub fn new(name: &str) -> Result<Self, Error> {
130 let path = Path::new(name);
131 let file = if path.exists() {
132 File::open(path)?
133 } else {
134 File::create(path)?
135 };
136
137 let is_single = match file.try_lock() {
138 Ok(()) => true,
139 Err(TryLockError::WouldBlock) => false,
140 Err(TryLockError::Error(e)) => return Err(e.into()),
141 };
142
143 Ok(Self {
144 _file: file,
145 is_single,
146 })
147 }
148
149 /// Returns whether this instance is single.
150 pub fn is_single(&self) -> bool {
151 self.is_single
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 static UNIQ_ID: &'static str = "aa2d0258-ffe9-11e7-ba89-0ed5f89f718b";
160 #[test]
161 fn test_single_instance() {
162 {
163 let instance_a = SingleInstance::new(UNIQ_ID).unwrap();
164 assert!(instance_a.is_single());
165 let instance_b = SingleInstance::new(UNIQ_ID).unwrap();
166 assert!(!instance_b.is_single());
167 }
168 let instance_c = SingleInstance::new(UNIQ_ID).unwrap();
169 assert!(instance_c.is_single());
170 }
171
172 #[test]
173 fn test_create_error() {
174 // On Linux and Android the name is an abstract socket name, so any
175 // string is a valid identifier. The file backends fail when the path
176 // cannot be created.
177 let result = SingleInstance::new("/nonexistent-stdx-dir/single_instance.lock");
178 #[cfg(target_vendor = "apple")]
179 assert!(result.is_err());
180 let _ = result;
181 }
182}