Skip to main content

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}