Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/linux/window_manager.rs
StratoSDK / crates / strato-ui-renderer / src / windowing / winit / linux / window_manager.rs
1use std::os::unix::fs::{FileTypeExt, MetadataExt};
2use std::process::{Command, Stdio};
3use std::{env, fs, path};
4 
5/// Attempt to find a running process that we believe is the window compositor.
6///
7/// The name comes from `/proc/$pid/comm`, and so it will be truncated to the first 15 chars of the
8/// actual process name.
9/// https://superuser.com/questions/567648/ps-comm-format-always-cuts-the-process-name
10pub(crate) fn look_for_wayland_compositor() -> Option<String> {
11 // First, try to determine the compositor by looking at the Wayland display
12 // socket and seeing which process is listening on it.
13 //
14 // TODO(CORE-3034): Re-enable this codepath once we've understood and
15 // addressed the lsof performance issues.
16 // if let Some(compositor_name) = get_wayland_compositor_from_socket() {
17 // return Some(compositor_name);
18 // }
19 
20 // If the above method didn't work, fallback to a less precise method. Simply use `ps
21 // -u` and grep for a recognized set of names among the running processes. This may
22 // have false positives, like processes that name-clash with these compositors.
23 let uid = nix::unistd::getuid();
24 let euid = nix::unistd::geteuid();
25 if let Some(ps_output) = Command::new("ps")
26 .args(["-u", &format!("{euid}"), "-U", &format!("{uid}")])
27 .stdout(Stdio::piped())
28 .spawn()
29 .ok()
30 .and_then(|output| output.stdout)
31 {
32 let wm_match_cmd = Command::new("grep")
33 .args(
34 ["-m", "1", "-o", "-F", "-i"].iter().chain(
35 WAYLAND_TILING_WM
36 .iter()
37 .flat_map(|wm_name| [&"-e", wm_name]),
38 ),
39 )
40 .stdin(Stdio::from(ps_output))
41 .output()
42 .ok()
43 .filter(|out| out.status.success());
44 
45 if let Some(wm_name_raw) = wm_match_cmd {
46 if let Ok(wm_name) = String::from_utf8(wm_name_raw.stdout) {
47 if !wm_name.is_empty() {
48 return Some(wm_name);
49 }
50 }
51 }
52 }
53 None
54}
55 
56/// Returns the name of the Wayland compositor by looking at the Wayland
57/// display socket and seeing which process is listening on it, or [`None`] if
58/// we were unable to compute it for any reason.
59///
60/// TODO(CORE-3034): Re-enable this codepath and remove the allow(dead_code)
61/// attribute.
62#[allow(dead_code)]
63fn get_wayland_compositor_from_socket() -> Option<String> {
64 // https://discourse.ubuntu.com/t/environment-variables-for-wayland-hackers/12750
65 let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR")
66 .ok()
67 .filter(|val| !val.is_empty())?;
68 let wayland_display = env::var("WAYLAND_DISPLAY")
69 .ok()
70 .filter(|val| !val.is_empty())
71 .unwrap_or("wayland-0".to_owned());
72 
73 // Wayland compositors communicate with their clients using a UNIX socket. This path is the
74 // standard location of that socket.
75 let wayland_socket_path = path::Path::new(xdg_runtime_dir.as_str()).join(wayland_display);
76 let socket_metadata = fs::metadata(&wayland_socket_path).ok()?;
77 
78 // Validate that this file is a socket owned by the effective user ID.
79 if !socket_metadata.file_type().is_socket()
80 || socket_metadata.uid() != nix::unistd::geteuid().as_raw()
81 {
82 return None;
83 }
84 
85 let path_str = wayland_socket_path.to_str()?;
86 
87 // If we found a valid socket, try either `lsof` or `fuser` to identify the process
88 // which is listening at this socket. This is the most precise method of doing this,
89 // but not all Linux systems have these tools installed, and if they do they may still
90 // require elevated privileges.
91 let get_pid_cmd = Command::new("lsof")
92 .args(["-t", path_str])
93 .stderr(Stdio::null())
94 .output()
95 .ok()
96 .filter(|output| output.status.success())
97 .map(|output| output.stdout)
98 .or_else(|| {
99 Command::new("fuser")
100 .arg(path_str)
101 .stderr(Stdio::null())
102 .output()
103 .ok()
104 .filter(|output| output.status.success())
105 .map(|output| output.stdout)
106 });
107 
108 // If the above method worked, lookup the name of that pid.
109 if let Some(raw_pid) = get_pid_cmd {
110 let pid = String::from_utf8(raw_pid).ok()?.trim().to_owned();
111 // Validate that an integer pid was returned.
112 pid.parse::<i32>().ok()?;
113 if let Ok(wm_name_raw) = Command::new("ps")
114 .args(["-p", pid.as_str(), "-o", "comm="])
115 .output()
116 {
117 if let Ok(wm_name) = String::from_utf8(wm_name_raw.stdout) {
118 return Some(wm_name);
119 }
120 }
121 }
122 
123 None
124}
125 
126/// Hand-picked tiling wayland compositors. These are the two most starred on GitHub.
127const WAYLAND_TILING_WM: &[&str] = &["hyprland", "sway"];
128 
129pub(crate) fn is_tiling_window_manager(name: &str) -> bool {
130 // List of X11 tiling window managers copied from Chromium repo:
131 // https://source.chromium.org/chromium/chromium/src/+/6fa59a48:ui/base/x/x11_util.cc;l=374
132 const X11_TILING_WM: &[&str] = &["i3", "ion3", "notion", "ratpoison", "stumpwm"];
133 // Dynamic window managers can be configured to function as either tiling or stacking. It is
134 // impractical for us to introspect how these are configured, so for now we copy Chrome's
135 // approach to assume they are used as tiling.
136 const X11_DYNAMIC_WM: &[&str] = &["awesome", "qtile", "xmonad", "wmii"];
137 
138 let normalized = name.trim().to_lowercase();
139 X11_TILING_WM.contains(&normalized.as_str())
140 || X11_DYNAMIC_WM.contains(&normalized.as_str())
141 || WAYLAND_TILING_WM.contains(&normalized.as_str())
142}
143