144 lines
5.7 KiB
Rust
144 lines
5.7 KiB
Rust
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<String> {
|
|
// 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<String> {
|
|
// 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::<i32>().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())
|
|
}
|