Skip to main content

plat/
instance.rs

1// instance -- one running instance per app and user (ui: AppDesc AF_SINGLE_INSTANCE): a later
2// start hands its working folder and arguments to the running instance and ends
3//
4//   claim(app) -> Claim        Primary: this process is the app's instance; the requests of
5//                              later starts arrive on a thread of their own (take(); each one
6//                              calls wake()); Forwarded: the running instance has the request,
7//                              end this process; Alone: no instance channel (browser, Android,
8//                              an error): carry on alone
9//   take() -> Vec<Request>     the requests that arrived, oldest first
10//   send(app, cwd, args) -> bool    hand a request to the running instance (claim uses it)
11//   Request { cwd, args }; Request::paths()   the arguments that are no options, as absolute
12//                              paths (local file:// URIs as paths, other names with a scheme
13//                              and URLs as they are)
14//
15// Channel (control_ipc_win.rs / control_ipc_linux.rs): Windows the pipe
16// \\.\pipe\jfgui-instance-<user SID>-<app> (current user, local clients, one pipe instance:
17// the first process that creates it is the instance; the starting process lets it take the
18// foreground); Linux the abstract socket jfgui-instance-<uid>-<app>.  Both ends make sure the
19// other runs as the same user.  Message: "JFI1", u32 length of the rest, u32 count, count x
20// (u32 length, UTF-8): the working folder, then the arguments (without the program), on Linux
21// then "XAT1", u32 length, the start's $XDG_ACTIVATION_TOKEN when it has one (the running
22// instance's window comes forward with it on Wayland: linux_wl_act.rs; an instance of an older
23// version refuses the block, then the request goes again without it); answer "OK".  At most
24// 1 MiB, 3 s per exchange.
25
26use alloc::string::String;
27use alloc::vec::Vec;
28
29use rt::sync::SpinLock;
30
31/// what claim found
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum Claim {
35    /// this process is the app's instance
36    Primary,
37    /// the running instance got this start's request: end the process
38    Forwarded,
39    /// no instance channel here: carry on alone
40    Alone,
41}
42
43/// the command line of a later start
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct Request {
46    /// the working folder of the starting process
47    pub cwd: String,
48    /// its arguments, without the program
49    pub args: Vec<String>,
50}
51
52const MAGIC: &[u8; 4] = b"JFI1";
53const TOKEN_TAG: &[u8; 4] = b"XAT1";
54const MSG_MAX: usize = 1 << 20;
55#[cfg(any(windows, target_os = "linux"))]
56const QUEUE_MAX: usize = 64;
57
58static QUEUE: SpinLock<Vec<Request>> = SpinLock::new(Vec::new());
59
60impl Request {
61    /// the arguments that are no options ("-..."): relative paths against cwd; local file://
62    /// URIs as paths (Linux desktops hand files over that way for %U); other names with a
63    /// scheme ("config://..", "https://..") as they are
64    pub fn paths(&self) -> Vec<String> {
65        let mut out = Vec::new();
66        for a in self.args.iter().filter(|a| !a.is_empty() && !a.starts_with('-')) {
67            if let Some(p) = crate::assoc::file_uri_path(a) {
68                out.push(p);
69                continue;
70            }
71            let b = a.as_bytes();
72            let absolute = a.contains("://")
73                || b[0] == b'/'
74                || b[0] == b'\\'
75                || (b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':');
76            if absolute || self.cwd.is_empty() {
77                out.push(a.clone());
78            } else {
79                out.push(alloc::format!("{}/{}", self.cwd.trim_end_matches(['/', '\\']), a));
80            }
81        }
82        out
83    }
84}
85
86/// the requests that arrived since the last call, oldest first
87pub fn take() -> Vec<Request> {
88    core::mem::take(&mut *QUEUE.lock())
89}
90
91fn encode(cwd: &str, args: &[String], token: Option<&str>) -> Option<Vec<u8>> {
92    let mut body = Vec::new();
93    body.extend_from_slice(&(args.len() as u32 + 1).to_le_bytes());
94    for s in core::iter::once(cwd).chain(args.iter().map(|a| a.as_str())) {
95        body.extend_from_slice(&u32::try_from(s.len()).ok()?.to_le_bytes());
96        body.extend_from_slice(s.as_bytes());
97    }
98    if let Some(t) = token {
99        body.extend_from_slice(TOKEN_TAG);
100        body.extend_from_slice(&u32::try_from(t.len()).ok()?.to_le_bytes());
101        body.extend_from_slice(t.as_bytes());
102    }
103    if body.len() > MSG_MAX {
104        return None;
105    }
106    let mut out = Vec::with_capacity(body.len() + 8);
107    out.extend_from_slice(MAGIC);
108    out.extend_from_slice(&(body.len() as u32).to_le_bytes());
109    out.extend_from_slice(&body);
110    Some(out)
111}
112
113// -> the request and the start's activation token
114fn decode(body: &[u8]) -> Option<(Request, Option<String>)> {
115    let rd = |p: usize| -> Option<usize> { Some(u32::from_le_bytes(body.get(p..p + 4)?.try_into().ok()?) as usize) };
116    let count = rd(0)?;
117    let mut p = 4;
118    let mut items = Vec::new();
119    for _ in 0..count {
120        let n = rd(p)?;
121        let s = core::str::from_utf8(body.get(p + 4..p.checked_add(4 + n)?)?).ok()?;
122        items.push(String::from(s));
123        p += 4 + n;
124    }
125    let mut token = None;
126    if body.get(p..p + 4) == Some(&TOKEN_TAG[..]) {
127        let n = rd(p + 4)?;
128        token = Some(String::from(core::str::from_utf8(body.get(p + 8..p.checked_add(8 + n)?)?).ok()?));
129        p += 8 + n;
130    }
131    if p != body.len() || items.is_empty() {
132        return None;
133    }
134    let cwd = items.remove(0);
135    Some((Request { cwd, args: items }, token))
136}
137
138// ---------------------------------------------------------------- native
139
140#[cfg(any(windows, target_os = "linux"))]
141mod native {
142    use super::*;
143    use crate::control_ipc::{InstConn, inst_connect, inst_listen};
144
145    const STEP_MS: u32 = 2;
146    // never set: the serving thread runs until the process ends
147    static STOP: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
148    const LIMIT_MS: u64 = 3000;
149
150    fn deadline() -> u64 {
151        crate::time_us() + LIMIT_MS * 1000
152    }
153
154    fn write_all(c: &mut InstConn, mut data: &[u8], end: u64) -> bool {
155        while !data.is_empty() {
156            match c.write(data) {
157                Ok(Some(n)) if n > 0 => data = &data[n..],
158                Ok(Some(_)) | Err(_) => return false,
159                Ok(None) if crate::time_us() < end => crate::sleep_ms(STEP_MS),
160                Ok(None) => return false,
161            }
162        }
163        true
164    }
165
166    fn read_exact(c: &mut InstConn, buf: &mut [u8], end: u64) -> bool {
167        let mut got = 0;
168        while got < buf.len() {
169            match c.read(&mut buf[got..]) {
170                Ok(Some(n)) if n > 0 => got += n,
171                Ok(Some(_)) | Err(_) => return false,
172                Ok(None) if crate::time_us() < end => crate::sleep_ms(STEP_MS),
173                Ok(None) => return false,
174            }
175        }
176        true
177    }
178
179    // one request from a starting process -> queued, answered
180    fn serve_one(c: &mut InstConn) -> bool {
181        let end = deadline();
182        let mut head = [0u8; 8];
183        if !read_exact(c, &mut head, end) || &head[..4] != MAGIC {
184            return false;
185        }
186        let len = u32::from_le_bytes([head[4], head[5], head[6], head[7]]) as usize;
187        if len > MSG_MAX {
188            return false;
189        }
190        let mut body = alloc::vec![0u8; len];
191        if !read_exact(c, &mut body, end) {
192            return false;
193        }
194        let Some((req, _token)) = decode(&body) else { return false };
195        #[cfg(target_os = "linux")]
196        if let Some(t) = &_token {
197            crate::act_provide(t); // before the UI brings its window forward
198        }
199        {
200            let mut q = QUEUE.lock();
201            if q.len() == QUEUE_MAX {
202                q.remove(0);
203            }
204            q.push(req);
205        }
206        let _ = write_all(c, b"OK", end);
207        crate::wake();
208        true
209    }
210
211    pub fn claim(app: &str) -> Claim {
212        if !app_ok(app) {
213            return Claim::Alone;
214        }
215        for _ in 0..2 {
216            match inst_listen(app) {
217                Ok(Some(server)) => {
218                    // the instance's lifetime: the thread ends with the process
219                    let started = crate::thread_spawn(64 << 10, alloc::boxed::Box::new(move || {
220                        while !STOP.load(core::sync::atomic::Ordering::Relaxed) {
221                            match server.accept() {
222                                Ok(mut c) => {
223                                    serve_one(&mut c);
224                                }
225                                Err(_) => crate::sleep_ms(100),
226                            }
227                        }
228                    }));
229                    return if started.is_some() { Claim::Primary } else { Claim::Alone };
230                }
231                Ok(None) => {
232                    let args: Vec<String> = crate::args().into_iter().skip(1).collect();
233                    let token = if cfg!(target_os = "linux") { crate::env("XDG_ACTIVATION_TOKEN") } else { None };
234                    let sent = |t: Option<&str>| encode(&crate::cwd(), &args, t).is_some_and(|m| send_msg(app, &m));
235                    // (an older instance refuses the token's block: again without it)
236                    if sent(token.as_deref()) || token.is_some() && sent(None) {
237                        return Claim::Forwarded;
238                    }
239                    // the instance just ended (or does not answer): try to become it
240                }
241                Err(_) => return Claim::Alone,
242            }
243        }
244        Claim::Alone
245    }
246
247    pub fn send(app: &str, cwd: &str, args: &[String]) -> bool {
248        encode(cwd, args, None).is_some_and(|m| send_msg(app, &m))
249    }
250
251    fn send_msg(app: &str, msg: &[u8]) -> bool {
252        if !app_ok(app) {
253            return false;
254        }
255        let Ok(mut c) = inst_connect(app) else { return false };
256        let end = deadline();
257        let mut ok = [0u8; 2];
258        write_all(&mut c, &msg, end) && read_exact(&mut c, &mut ok, end) && &ok == b"OK"
259    }
260}
261
262// names of the channel: the app id rules of vfs::set_app_id
263fn app_ok(app: &str) -> bool {
264    !app.is_empty() && app.len() <= 64 && app.bytes().all(|c| c.is_ascii_alphanumeric() || b"._-".contains(&c))
265}
266
267/// become the app's single instance, or hand this start's arguments to the running one
268pub fn claim(app: &str) -> Claim {
269    #[cfg(any(windows, target_os = "linux"))]
270    {
271        native::claim(app)
272    }
273    #[cfg(not(any(windows, target_os = "linux")))]
274    {
275        let _ = (app, app_ok as fn(&str) -> bool);
276        Claim::Alone
277    }
278}
279
280/// hand (cwd, args) to the running instance of app -> it confirmed the request
281pub fn send(app: &str, cwd: &str, args: &[String]) -> bool {
282    #[cfg(any(windows, target_os = "linux"))]
283    {
284        native::send(app, cwd, args)
285    }
286    #[cfg(not(any(windows, target_os = "linux")))]
287    {
288        let _ = (
289            app,
290            cwd,
291            args,
292            encode as fn(&str, &[String], Option<&str>) -> Option<Vec<u8>>,
293            decode as fn(&[u8]) -> Option<(Request, Option<String>)>,
294        );
295        false
296    }
297}