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 }