Skip to main content

agentmux_common/
xauthority.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3//
4// XWayland authorization for processes that talk to X on a Wayland session
5// (#4011, #1648).
6//
7// GNOME's Mutter starts Xwayland with `-auth $XDG_RUNTIME_DIR/.mutter-Xwaylandauth.*`
8// and exports XAUTHORITY only into the systemd user environment, so a process
9// started by a login entry, the AppImage binfmt path or an agent shell can lack
10// it. Without the cookie the X server refuses the connection:
11// - the host, when forced onto XWayland (`AGENTMUX_OZONE_PLATFORM=x11`), has
12//   Chromium fail with "Missing X server or $DISPLAY" and CEF exit before any
13//   window opens;
14// - the launcher's startup splash fails its X probe and falls back to the
15//   native-Wayland backend, which Mutter places top-left instead of centered.
16
17use std::path::{Path, PathBuf};
18
19/// Make sure XAUTHORITY points at a readable cookie, setting it from the
20/// session when it's missing. Sets the process environment, so call it while
21/// the process is still single-threaded and before spawning anything that must
22/// inherit it: the host before CefInitialize (GPU and renderer processes), the
23/// launcher before the splash's X probe (and the host it spawns later).
24pub fn ensure_xauthority() -> Option<PathBuf> {
25    if let Some(p) = std::env::var_os("XAUTHORITY").map(PathBuf::from).filter(|p| p.is_file()) {
26        return Some(p);
27    }
28    let found = systemd_user_xauthority()
29        .or_else(|| {
30            std::env::var_os("XDG_RUNTIME_DIR")
31                .and_then(|d| newest_mutter_cookie(Path::new(&d)))
32        })
33        .or_else(|| {
34            std::env::var_os("HOME")
35                .map(|h| PathBuf::from(h).join(".Xauthority"))
36                .filter(|p| p.is_file())
37        })?;
38    std::env::set_var("XAUTHORITY", &found);
39    tracing::info!("XAUTHORITY was unset; using {}", found.display());
40    Some(found)
41}
42
43fn systemd_user_xauthority() -> Option<PathBuf> {
44    let out = std::process::Command::new("systemctl")
45        .args(["--user", "show-environment"])
46        .output()
47        .ok()?;
48    if !out.status.success() {
49        return None;
50    }
51    parse_show_environment(&String::from_utf8_lossy(&out.stdout)).filter(|p| p.is_file())
52}
53
54/// `XAUTHORITY` from `systemctl --user show-environment` output.
55fn parse_show_environment(out: &str) -> Option<PathBuf> {
56    out.lines()
57        .find_map(|l| l.strip_prefix("XAUTHORITY="))
58        .map(|v| v.trim().trim_matches(|c| c == '\'' || c == '"'))
59        .filter(|v| !v.is_empty())
60        .map(PathBuf::from)
61}
62
63/// The most recently modified `.mutter-Xwaylandauth.*` file in `dir`.
64fn newest_mutter_cookie(dir: &Path) -> Option<PathBuf> {
65    std::fs::read_dir(dir)
66        .ok()?
67        .filter_map(Result::ok)
68        .filter(|e| e.file_name().to_string_lossy().starts_with(".mutter-Xwaylandauth."))
69        .filter_map(|e| {
70            let meta = e.metadata().ok()?;
71            if !meta.is_file() {
72                return None;
73            }
74            Some((meta.modified().ok()?, e.path()))
75        })
76        .max_by_key(|(mtime, _)| *mtime)
77        .map(|(_, path)| path)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn parses_xauthority_from_show_environment() {
86        let out = "DISPLAY=:0\nXAUTHORITY=/run/user/1000/.mutter-Xwaylandauth.EVK3V3\nWAYLAND_DISPLAY=wayland-0\n";
87        assert_eq!(
88            parse_show_environment(out),
89            Some(PathBuf::from("/run/user/1000/.mutter-Xwaylandauth.EVK3V3"))
90        );
91    }
92
93    #[test]
94    fn strips_quotes_systemd_adds_to_values() {
95        assert_eq!(parse_show_environment("XAUTHORITY='/tmp/a b'\n"), Some(PathBuf::from("/tmp/a b")));
96    }
97
98    #[test]
99    fn missing_or_empty_xauthority_is_none() {
100        assert_eq!(parse_show_environment("DISPLAY=:0\n"), None);
101        assert_eq!(parse_show_environment("XAUTHORITY=\n"), None);
102    }
103
104    #[test]
105    fn picks_the_newest_mutter_cookie_and_ignores_other_files() {
106        let dir = tempfile::tempdir().unwrap();
107        let old = dir.path().join(".mutter-Xwaylandauth.OLD111");
108        let new = dir.path().join(".mutter-Xwaylandauth.NEW222");
109        std::fs::write(&old, b"x").unwrap();
110        std::fs::write(dir.path().join("pulse-cookie"), b"x").unwrap();
111        std::thread::sleep(std::time::Duration::from_millis(20));
112        std::fs::write(&new, b"x").unwrap();
113        assert_eq!(newest_mutter_cookie(dir.path()), Some(new));
114    }
115
116    #[test]
117    fn no_cookie_in_dir_is_none() {
118        let dir = tempfile::tempdir().unwrap();
119        std::fs::write(dir.path().join("bus"), b"x").unwrap();
120        assert_eq!(newest_mutter_cookie(dir.path()), None);
121    }
122}