StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use std::os::unix::fs::{FileTypeExt, MetadataExt}; |
| 2 | use std::process::{Command, Stdio}; |
| 3 | use 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 |
| 10 | pub(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)] |
| 63 | fn 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. |
| 127 | const WAYLAND_TILING_WM: &[&str] = &["hyprland", "sway"]; |
| 128 | |
| 129 | pub(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 |