git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

src/instance.rs (3.9K)

  1 //! Single-instance forwarding over the CCE socket convention.
  2 //!
  3 //! Every external open (`xdg-open` via the desktop entry's `%u`) spawns a
  4 //! fresh `cce-browser <url>` process. A second full instance is not just
  5 //! clutter: the profile dir holds a plaintext cookie jar two engines must
  6 //! not share. So the first instance listens on
  7 //! `/tmp/cce-browser-<WAYLAND_DISPLAY>.sock` (keyed by display, which keeps
  8 //! shadow sessions isolated for free), and every later launch hands its
  9 //! argument to it and exits before any Wayland or engine work happens.
 10 //!
 11 //! The claim, the race it closes, the parked listener and the bounded reads
 12 //! are `cce_ui::ipc::instance`'s; this module is the browser's protocol on
 13 //! top: `open <arg>` / `new-tab`, each answered `ok`. `src/bin/open.rs`
 14 //! speaks the same protocol without linking cce-ui — keep the two in step.
 15 
 16 use crate::Message;
 17 
 18 /// Socket prefix; `cce_ui::ipc::socket_path` appends `-<WAYLAND_DISPLAY>`.
 19 const PREFIX: &str = "cce-browser";
 20 
 21 /// Hand `arg` to a running instance, or claim the instance socket.
 22 ///
 23 /// Returns `true` when a running instance took the launch (the caller should
 24 /// exit without starting the engine). Returns `false` when this process is
 25 /// the instance — with the listener parked for [`spawn_listener`] — or when
 26 /// single-instance handling failed entirely and the launch should proceed
 27 /// standalone.
 28 pub fn forward_or_claim(arg: Option<&str>) -> bool {
 29     cce_ui::ipc::instance::forward_or_claim(PREFIX, &launch_line(arg))
 30 }
 31 
 32 /// The line a launch sends. A relative file path is resolved against *this*
 33 /// process's cwd — the instance's differs, so it must travel absolute.
 34 fn launch_line(arg: Option<&str>) -> String {
 35     let Some(a) = arg else {
 36         return "new-tab".to_string();
 37     };
 38     let p = std::path::Path::new(a);
 39     let abs = if p.exists() {
 40         std::fs::canonicalize(p).ok().and_then(|c| c.to_str().map(String::from))
 41     } else {
 42         None
 43     };
 44     format!("open {}", abs.as_deref().unwrap_or(a))
 45 }
 46 
 47 /// Serve the listener claimed in `main()`, pushing each received launch into
 48 /// the app's calloop channel. No-op when this process runs standalone.
 49 pub fn spawn_listener(sender: calloop::channel::Sender<Message>) {
 50     cce_ui::ipc::instance::serve(move |line| {
 51         let msg = parse_command(line)?;
 52         sender.send(msg).ok()?;
 53         Some("ok".into())
 54     });
 55 }
 56 
 57 /// `open <arg>` / `new-tab` → the message the app loop handles.
 58 fn parse_command(line: &str) -> Option<Message> {
 59     if let Some(arg) = line.strip_prefix("open ") {
 60         let arg = arg.trim();
 61         (!arg.is_empty()).then(|| Message::OpenExternal(Some(arg.to_string())))
 62     } else if line == "new-tab" {
 63         Some(Message::OpenExternal(None))
 64     } else {
 65         None
 66     }
 67 }
 68 
 69 /// Unlink the socket if this process bound it. Called after the engine loop
 70 /// returns.
 71 pub fn cleanup() {
 72     cce_ui::ipc::instance::cleanup();
 73 }
 74 
 75 #[cfg(test)]
 76 mod tests {
 77     use super::*;
 78 
 79     #[test]
 80     fn commands_parse() {
 81         assert!(matches!(
 82             parse_command("open https://example.com"),
 83             Some(Message::OpenExternal(Some(u))) if u == "https://example.com"
 84         ));
 85         assert!(matches!(
 86             parse_command("new-tab"),
 87             Some(Message::OpenExternal(None))
 88         ));
 89         assert!(parse_command("open ").is_none());
 90         assert!(parse_command("bogus").is_none());
 91     }
 92 
 93     #[test]
 94     fn launch_lines_round_trip() {
 95         assert_eq!(launch_line(None), "new-tab");
 96         assert_eq!(launch_line(Some("https://example.com")), "open https://example.com");
 97         assert!(matches!(
 98             parse_command(&launch_line(Some("https://example.com"))),
 99             Some(Message::OpenExternal(Some(u))) if u == "https://example.com"
100         ));
101         // A path that exists here travels absolute.
102         let line = launch_line(Some("Cargo.toml"));
103         assert!(line.starts_with("open /") && line.ends_with("/Cargo.toml"), "{line}");
104     }
105 }