use command::blocking::Command; use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::process::Stdio; use std::{env, fs, path}; /// Attempt to find a running process that we believe is the window compositor. /// /// The name comes from `/proc/$pid/comm`, and so it will be truncated to the first 15 chars of the /// actual process name. /// https://superuser.com/questions/567648/ps-comm-format-always-cuts-the-process-name pub(crate) fn look_for_wayland_compositor() -> Option { // First, try to determine the compositor by looking at the Wayland display // socket and seeing which process is listening on it. // // TODO(CORE-3034): Re-enable this codepath once we've understood and // addressed the lsof performance issues. // if let Some(compositor_name) = get_wayland_compositor_from_socket() { // return Some(compositor_name); // } // If the above method didn't work, fallback to a less precise method. Simply use `ps // -u` and grep for a recognized set of names among the running processes. This may // have false positives, like processes that name-clash with these compositors. let uid = nix::unistd::getuid(); let euid = nix::unistd::geteuid(); if let Some(ps_output) = Command::new("ps") .args(["-u", &format!("{euid}"), "-U", &format!("{uid}")]) .stdout(Stdio::piped()) .spawn() .ok() .and_then(|output| output.stdout) { let wm_match_cmd = Command::new("grep") .args( ["-m", "1", "-o", "-F", "-i"].iter().chain( WAYLAND_TILING_WM .iter() .flat_map(|wm_name| [&"-e", wm_name]), ), ) .stdin(Stdio::from(ps_output)) .output() .ok() .filter(|out| out.status.success()); if let Some(wm_name_raw) = wm_match_cmd { if let Ok(wm_name) = String::from_utf8(wm_name_raw.stdout) { if !wm_name.is_empty() { return Some(wm_name); } } } } None } /// Returns the name of the Wayland compositor by looking at the Wayland /// display socket and seeing which process is listening on it, or [`None`] if /// we were unable to compute it for any reason. /// /// TODO(CORE-3034): Re-enable this codepath and remove the allow(dead_code) /// attribute. #[allow(dead_code)] fn get_wayland_compositor_from_socket() -> Option { // https://discourse.ubuntu.com/t/environment-variables-for-wayland-hackers/12750 let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR") .ok() .filter(|val| !val.is_empty())?; let wayland_display = env::var("WAYLAND_DISPLAY") .ok() .filter(|val| !val.is_empty()) .unwrap_or("wayland-0".to_owned()); // Wayland compositors communicate with their clients using a UNIX socket. This path is the // standard location of that socket. let wayland_socket_path = path::Path::new(xdg_runtime_dir.as_str()).join(wayland_display); let socket_metadata = fs::metadata(&wayland_socket_path).ok()?; // Validate that this file is a socket owned by the effective user ID. if !socket_metadata.file_type().is_socket() || socket_metadata.uid() != nix::unistd::geteuid().as_raw() { return None; } let path_str = wayland_socket_path.to_str()?; // If we found a valid socket, try either `lsof` or `fuser` to identify the process // which is listening at this socket. This is the most precise method of doing this, // but not all Linux systems have these tools installed, and if they do they may still // require elevated privileges. let get_pid_cmd = Command::new("lsof") .args(["-t", path_str]) .stderr(Stdio::null()) .output() .ok() .filter(|output| output.status.success()) .map(|output| output.stdout) .or_else(|| { Command::new("fuser") .arg(path_str) .stderr(Stdio::null()) .output() .ok() .filter(|output| output.status.success()) .map(|output| output.stdout) }); // If the above method worked, lookup the name of that pid. if let Some(raw_pid) = get_pid_cmd { let pid = String::from_utf8(raw_pid).ok()?.trim().to_owned(); // Validate that an integer pid was returned. pid.parse::().ok()?; if let Ok(wm_name_raw) = Command::new("ps") .args(["-p", pid.as_str(), "-o", "comm="]) .output() { if let Ok(wm_name) = String::from_utf8(wm_name_raw.stdout) { return Some(wm_name); } } } None } /// Hand-picked tiling wayland compositors. These are the two most starred on GitHub. const WAYLAND_TILING_WM: &[&str] = &["hyprland", "sway"]; pub(crate) fn is_tiling_window_manager(name: &str) -> bool { // List of X11 tiling window managers copied from Chromium repo: // https://source.chromium.org/chromium/chromium/src/+/6fa59a48:ui/base/x/x11_util.cc;l=374 const X11_TILING_WM: &[&str] = &["i3", "ion3", "notion", "ratpoison", "stumpwm"]; // Dynamic window managers can be configured to function as either tiling or stacking. It is // impractical for us to introspect how these are configured, so for now we copy Chrome's // approach to assume they are used as tiling. const X11_DYNAMIC_WM: &[&str] = &["awesome", "qtile", "xmonad", "wmii"]; let normalized = name.trim().to_lowercase(); X11_TILING_WM.contains(&normalized.as_str()) || X11_DYNAMIC_WM.contains(&normalized.as_str()) || WAYLAND_TILING_WM.contains(&normalized.as_str()) }