Skip to main content

gpui/platform/
single_instance.rs

1/// Cross-platform single instance enforcement for applications.
2///
3/// Uses Unix domain sockets on macOS/Linux and named mutexes on Windows
4/// to ensure only one instance of an application runs at a time.
5use anyhow::Result;
6use std::path::PathBuf;
7
8/// Error returned when another instance of the application is already running.
9#[derive(Debug)]
10pub struct AlreadyRunning;
11
12impl std::fmt::Display for AlreadyRunning {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        write!(f, "Another instance is already running")
15    }
16}
17
18impl std::error::Error for AlreadyRunning {}
19
20/// A guard that enforces single-instance behavior for an application.
21///
22/// When acquired successfully, this struct holds a platform-specific lock
23/// that prevents other instances from starting. The lock is released when
24/// this struct is dropped.
25pub struct SingleInstance {
26    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))]
27    _listener: std::os::unix::net::UnixListener,
28    #[cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))]
29    _socket_path: PathBuf,
30    #[cfg(target_os = "windows")]
31    _mutex: WindowsMutexHandle,
32}
33
34#[cfg(target_os = "windows")]
35struct WindowsMutexHandle {
36    handle: windows::Win32::Foundation::HANDLE,
37}
38
39#[cfg(target_os = "windows")]
40impl Drop for WindowsMutexHandle {
41    fn drop(&mut self) {
42        unsafe {
43            let _ = windows::Win32::Foundation::CloseHandle(self.handle);
44        }
45    }
46}
47
48#[cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))]
49fn socket_path(app_id: &str) -> PathBuf {
50    let dir = std::env::var("XDG_RUNTIME_DIR")
51        .or_else(|_| std::env::var("TMPDIR"))
52        .unwrap_or_else(|_| "/tmp".to_string());
53    PathBuf::from(dir).join(format!("{}.sock", app_id))
54}
55
56impl SingleInstance {
57    /// Attempt to acquire the single-instance lock for the given application ID.
58    ///
59    /// Returns `Ok(SingleInstance)` if this is the first instance, or
60    /// `Err(AlreadyRunning)` if another instance already holds the lock.
61    pub fn acquire(app_id: &str) -> std::result::Result<Self, AlreadyRunning> {
62        Self::platform_acquire(app_id)
63    }
64
65    /// Register a callback to be invoked when another instance attempts to start
66    /// and sends an activation message.
67    ///
68    /// On Unix platforms, this spawns a background thread that listens for
69    /// incoming connections on the Unix domain socket.
70    pub fn on_activate(&self, callback: Box<dyn Fn() + Send + 'static>) {
71        self.platform_on_activate(callback);
72    }
73}
74
75/// Send an activation message to an already-running instance of the application.
76///
77/// This is typically called after `SingleInstance::acquire` returns `Err(AlreadyRunning)`
78/// to signal the existing instance to come to the foreground.
79pub fn send_activate_to_existing(app_id: &str) -> Result<()> {
80    platform_send_activate(app_id)
81}
82
83#[cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))]
84impl SingleInstance {
85    fn platform_acquire(app_id: &str) -> std::result::Result<Self, AlreadyRunning> {
86        use std::os::unix::net::UnixListener;
87
88        let path = socket_path(app_id);
89
90        if std::os::unix::net::UnixStream::connect(&path).is_ok() {
91            return Err(AlreadyRunning);
92        }
93
94        let _ = std::fs::remove_file(&path);
95        let listener = UnixListener::bind(&path).map_err(|_| AlreadyRunning)?;
96        listener.set_nonblocking(true).ok();
97
98        Ok(Self {
99            _listener: listener,
100            _socket_path: path,
101        })
102    }
103
104    fn platform_on_activate(&self, callback: Box<dyn Fn() + Send + 'static>) {
105        use std::io::Read;
106        use std::os::unix::net::UnixListener;
107
108        let listener = unsafe {
109            use std::os::unix::io::{AsRawFd, FromRawFd};
110            let fd = self._listener.as_raw_fd();
111            let dup_fd = libc::dup(fd);
112            if dup_fd < 0 {
113                return;
114            }
115            UnixListener::from_raw_fd(dup_fd)
116        };
117        listener.set_nonblocking(false).ok();
118
119        std::thread::spawn(move || {
120            for stream in listener.incoming() {
121                match stream {
122                    Ok(mut stream) => {
123                        let mut buf = [0u8; 64];
124                        if let Ok(n) = stream.read(&mut buf) {
125                            if n > 0 && &buf[..n.min(8)] == b"activate" {
126                                callback();
127                            }
128                        }
129                    }
130                    Err(_) => break,
131                }
132            }
133        });
134    }
135}
136
137#[cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))]
138impl Drop for SingleInstance {
139    fn drop(&mut self) {
140        let _ = std::fs::remove_file(&self._socket_path);
141    }
142}
143
144#[cfg(any(target_os = "macos", target_os = "linux", target_os = "freebsd"))]
145fn platform_send_activate(app_id: &str) -> Result<()> {
146    use std::io::Write;
147    use std::os::unix::net::UnixStream;
148
149    let path = socket_path(app_id);
150    let mut stream = UnixStream::connect(&path)?;
151    stream.write_all(b"activate")?;
152    Ok(())
153}
154
155#[cfg(target_os = "windows")]
156impl SingleInstance {
157    fn platform_acquire(app_id: &str) -> std::result::Result<Self, AlreadyRunning> {
158        use windows::Win32::Foundation::ERROR_ALREADY_EXISTS;
159        use windows::Win32::Foundation::GetLastError;
160        use windows::Win32::System::Threading::CreateMutexW;
161        use windows::core::HSTRING;
162
163        let name = HSTRING::from(format!("Global\\{}", app_id));
164        unsafe {
165            let handle = CreateMutexW(None, true, &name).map_err(|_| AlreadyRunning)?;
166            if GetLastError() == ERROR_ALREADY_EXISTS {
167                let _ = windows::Win32::Foundation::CloseHandle(handle);
168                return Err(AlreadyRunning);
169            }
170            Ok(Self {
171                _mutex: WindowsMutexHandle { handle },
172            })
173        }
174    }
175
176    fn platform_on_activate(&self, _callback: Box<dyn Fn() + Send + 'static>) {}
177}
178
179#[cfg(target_os = "windows")]
180fn platform_send_activate(_app_id: &str) -> Result<()> {
181    Ok(())
182}