Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions niri-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub struct Config {
pub overview: Overview,
#[knuffel(child, default)]
pub environment: Environment,
#[knuffel(child, default)]
pub xwayland_satellite: XwaylandSatellite,
#[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>,
#[knuffel(children(name = "layer-rule"))]
Expand Down Expand Up @@ -1364,6 +1366,23 @@ pub struct EnvironmentVariable {
pub value: Option<String>,
}

#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct XwaylandSatellite {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = Self::default().path)]
pub path: String,
}

impl Default for XwaylandSatellite {
fn default() -> Self {
Self {
off: false,
path: String::from("xwayland-satellite"),
}
}
}

#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct Workspace {
#[knuffel(argument)]
Expand Down Expand Up @@ -4796,6 +4815,10 @@ mod tests {
},
],
),
xwayland_satellite: XwaylandSatellite {
off: false,
path: "xwayland-satellite",
},
window_rules: [
WindowRule {
matches: [
Expand Down
17 changes: 15 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ use niri::dbus;
use niri::ipc::client::handle_msg;
use niri::niri::State;
use niri::utils::spawning::{
spawn, store_and_increase_nofile_rlimit, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE,
spawn, store_and_increase_nofile_rlimit, CHILD_DISPLAY, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE,
REMOVE_ENV_RUST_LIB_BACKTRACE,
};
use niri::utils::watcher::Watcher;
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
use niri::utils::{cause_panic, version, xwayland, IS_SYSTEMD_SERVICE};
use niri_config::Config;
use niri_ipc::socket::SOCKET_PATH_ENV;
use portable_atomic::Ordering;
Expand Down Expand Up @@ -200,6 +200,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("IPC listening on: {}", socket_path.to_string_lossy());
}

// Setup xwayland-satellite integration.
xwayland::satellite::setup(&mut state);
if let Some(satellite) = &state.niri.satellite {
let name = satellite.display_name();
*CHILD_DISPLAY.write().unwrap() = Some(name.to_owned());
env::set_var("DISPLAY", name);
info!("listening on X11 socket: {name}");
} else {
// Avoid spawning children in the host X11.
env::remove_var("DISPLAY");
}

if cli.session {
// We're starting as a session. Import our variables.
import_environment();
Expand Down Expand Up @@ -275,6 +287,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
fn import_environment() {
let variables = [
"WAYLAND_DISPLAY",
"DISPLAY",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_TYPE",
SOCKET_PATH_ENV,
Expand Down
38 changes: 36 additions & 2 deletions src/niri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,12 @@ use crate::ui::hotkey_overlay::HotkeyOverlay;
use crate::ui::screen_transition::{self, ScreenTransition};
use crate::ui::screenshot_ui::{OutputScreenshot, ScreenshotUi, ScreenshotUiRenderElement};
use crate::utils::scale::{closest_representable_scale, guess_monitor_scale};
use crate::utils::spawning::CHILD_ENV;
use crate::utils::spawning::{CHILD_DISPLAY, CHILD_ENV};
use crate::utils::xwayland::satellite::Satellite;
use crate::utils::{
center, center_f64, expand_home, get_monotonic_time, ipc_transform_to_smithay, is_mapped,
logical_output, make_screenshot_path, output_matches_name, output_size, send_scale_transform,
write_png_rgba8,
write_png_rgba8, xwayland,
};
use crate::window::mapped::MappedId;
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef};
Expand Down Expand Up @@ -376,6 +377,8 @@ pub struct Niri {
pub ipc_server: Option<IpcServer>,
pub ipc_outputs_changed: bool,

pub satellite: Option<Satellite>,

// Casts are dropped before PipeWire to prevent a double-free (yay).
pub casts: Vec<Cast>,
pub pipewire: Option<PipeWire>,
Expand Down Expand Up @@ -1332,6 +1335,7 @@ impl State {
let mut layer_rules_changed = false;
let mut shaders_changed = false;
let mut cursor_inactivity_timeout_changed = false;
let mut xwls_changed = false;
let mut old_config = self.niri.config.borrow_mut();

// Reload the cursor.
Expand Down Expand Up @@ -1443,6 +1447,10 @@ impl State {
output_config_changed = true;
}

if config.xwayland_satellite != old_config.xwayland_satellite {
xwls_changed = true;
}

*old_config = config;

if let Some(outputs) = preserved_output_config {
Expand Down Expand Up @@ -1506,6 +1514,30 @@ impl State {
self.niri.reset_pointer_inactivity_timer();
}

if xwls_changed {
// If xwl-s was previously working and is now off, we don't try to kill it or stop
// watching the sockets, for simplicity's sake.
let was_working = self.niri.satellite.is_some();

// Try to start, or restart in case the user corrected the path or something.
xwayland::satellite::setup(self);

let config = self.niri.config.borrow();
let display_name = (!config.xwayland_satellite.off)
.then_some(self.niri.satellite.as_ref())
.flatten()
.map(|satellite| satellite.display_name().to_owned());

if let Some(name) = &display_name {
if !was_working {
info!("listening on X11 socket: {name}");
}
}

// This won't change the systemd environment, but oh well.
*CHILD_DISPLAY.write().unwrap() = display_name;
}

// Can't really update xdg-decoration settings since we have to hide the globals for CSD
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
Expand Down Expand Up @@ -2569,6 +2601,8 @@ impl Niri {
ipc_server,
ipc_outputs_changed: false,

satellite: None,

pipewire: None,
casts: vec![],
#[cfg(feature = "xdp-gnome-screencast")]
Expand Down
1 change: 1 addition & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod scale;
pub mod spawning;
pub mod transaction;
pub mod watcher;
pub mod xwayland;

pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);

Expand Down
9 changes: 9 additions & 0 deletions src/utils/spawning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::utils::expand_home;
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
pub static CHILD_ENV: RwLock<Environment> = RwLock::new(Environment(Vec::new()));
pub static CHILD_DISPLAY: RwLock<Option<String>> = RwLock::new(None);

static ORIGINAL_NOFILE_RLIMIT_CUR: Atomic<rlim_t> = Atomic::new(0);
static ORIGINAL_NOFILE_RLIMIT_MAX: Atomic<rlim_t> = Atomic::new(0);
Expand Down Expand Up @@ -116,6 +117,14 @@ fn spawn_sync(
process.env_remove("RUST_LIB_BACKTRACE");
}

// Set DISPLAY if needed.
let display = CHILD_DISPLAY.read().unwrap();
if let Some(display) = &*display {
process.env("DISPLAY", display);
} else {
process.env_remove("DISPLAY");
}

// Set configured environment.
let env = CHILD_ENV.read().unwrap();
for var in &env.0 {
Expand Down
151 changes: 151 additions & 0 deletions src/utils/xwayland/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use std::os::fd::OwnedFd;
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixListener};

use anyhow::{anyhow, ensure, Context as _};
use rustix::fs::{lstat, mkdir};
use rustix::io::Errno;
use rustix::process::getuid;
use smithay::reexports::rustix::fs::{unlink, OFlags};
use smithay::reexports::rustix::process::getpid;
use smithay::reexports::rustix::{self};

pub mod satellite;

const TMP_UNIX_DIR: &str = "/tmp";
const X11_TMP_UNIX_DIR: &str = "/tmp/.X11-unix";

struct X11Connection {
display_name: String,
abstract_fd: OwnedFd,
unix_fd: OwnedFd,
_unix_guard: Unlink,
_lock_guard: Unlink,
}

struct Unlink(String);
impl Drop for Unlink {
fn drop(&mut self) {
let _ = unlink(&self.0);
}
}

// Adapted from Mutter code:
// https://gitlab.gnome.org/GNOME/mutter/-/blob/48.3.1/src/wayland/meta-xwayland.c?ref_type=tags#L513
fn ensure_x11_unix_dir() -> anyhow::Result<()> {
match mkdir(X11_TMP_UNIX_DIR, 0o1777.into()) {
Ok(()) => Ok(()),
Err(Errno::EXIST) => {
ensure_x11_unix_perms().context("wrong X11 directory permissions")?;
Ok(())
}
Err(err) => Err(err).context("error creating X11 directory"),
}
}

fn ensure_x11_unix_perms() -> anyhow::Result<()> {
let x11_tmp = lstat(X11_TMP_UNIX_DIR).context("error checking X11 directory permissions")?;
let tmp = lstat(TMP_UNIX_DIR).context("error checking /tmp directory permissions")?;

ensure!(
x11_tmp.st_uid == tmp.st_uid || x11_tmp.st_uid == getuid().as_raw(),
"wrong ownership for X11 directory"
);
ensure!(
(x11_tmp.st_mode & 0o022) == 0o022,
"X11 directory is not writable"
);
ensure!(
(x11_tmp.st_mode & 0o1000) == 0o1000,
"X11 directory is missing the sticky bit"
);

Ok(())
}

fn pick_x11_display(start: u32) -> anyhow::Result<(u32, OwnedFd, Unlink)> {
for n in start..start + 50 {
let lock_path = format!("/tmp/.X{n}-lock");
let flags = OFlags::WRONLY | OFlags::CLOEXEC | OFlags::CREATE | OFlags::EXCL;
let Ok(lock_fd) = rustix::fs::open(&lock_path, flags, 0o444.into()) else {
// FIXME: check if the target process is dead and reuse the lock.
continue;
};
return Ok((n, lock_fd, Unlink(lock_path)));
}

Err(anyhow!("no free X11 display found after 50 attempts"))
}

fn bind_to_socket(addr: &SocketAddr) -> anyhow::Result<UnixListener> {
let listener = UnixListener::bind_addr(addr).context("error binding socket")?;
Ok(listener)
}

fn bind_to_abstract_socket(display: u32) -> anyhow::Result<UnixListener> {
let name = format!("/tmp/.X11-unix/X{display}");
let addr = SocketAddr::from_abstract_name(name).unwrap();
bind_to_socket(&addr)
}

fn bind_to_unix_socket(display: u32) -> anyhow::Result<(UnixListener, Unlink)> {
let name = format!("/tmp/.X11-unix/X{display}");
let addr = SocketAddr::from_pathname(&name).unwrap();
// Unlink old leftover socket if any.
let _ = unlink(&name);
let guard = Unlink(name);
bind_to_socket(&addr).map(|listener| (listener, guard))
}

fn open_display_sockets(display: u32) -> anyhow::Result<(UnixListener, UnixListener, Unlink)> {
let a = bind_to_abstract_socket(display).context("error binding to abstract socket")?;
let (u, g) = bind_to_unix_socket(display).context("error binding to unix socket")?;
Ok((a, u, g))
}

fn setup_connection() -> anyhow::Result<X11Connection> {
let _span = tracy_client::span!("open_x11_sockets");

ensure_x11_unix_dir()?;

let mut n = 0;
let mut attempt = 0;
let (display, lock_guard, a, u, unix_guard) = loop {
let (display, lock_fd, lock_guard) = pick_x11_display(n)?;

// Write our PID into the lock file.
let pid_string = format!("{:>10}\n", getpid().as_raw_nonzero());
if let Err(err) = rustix::io::write(&lock_fd, pid_string.as_bytes()) {
return Err(err).context("error writing PID to X11 lock file");
}
drop(lock_fd);

match open_display_sockets(display) {
Ok((a, u, g)) => {
break (display, lock_guard, a, u, g);
}
Err(err) => {
if attempt == 50 {
return Err(err)
.context("error opening X11 sockets after creating a lock file");
}

n = display + 1;
attempt += 1;
continue;
}
}
};

let display_name = format!(":{display}");
let abstract_fd = OwnedFd::from(a);
let unix_fd = OwnedFd::from(u);

Ok(X11Connection {
display_name,
abstract_fd,
unix_fd,
_unix_guard: unix_guard,
_lock_guard: lock_guard,
})
}
Loading
Loading