atagen

monolith

public
Star0
Watch0
47b86d08d05a97fea42c27b6ed194c0ceaa30374
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/monolith/src/xwayland.rsRust418 lines13.1 KB
1
//! Lazy Xwayland. monolith owns an X display socket from startup and only
2
//! starts Xwayland when a client connects, handing it the socket so the
3
//! connection that woke it is served. Xwayland exits once its last client
4
//! leaves and the socket is watched again.
5
6
use crate::state::State;
7
use smithay::desktop::Window;
8
use smithay::reexports::calloop::generic::Generic;
9
use smithay::reexports::calloop::{Interest, Mode, PostAction, RegistrationToken};
10
use smithay::reexports::rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd};
11
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
12
use smithay::utils::{Logical, Rectangle, Size};
13
use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};
14
use smithay::xwayland::xwm::{Reorder, ResizeEdge, WmWindowProperty, WmWindowType, XwmId};
15
use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};
16
use std::os::fd::AsFd;
17
use std::os::linux::net::SocketAddrExt;
18
use std::os::unix::net::{SocketAddr, UnixListener};
19
use std::process::Stdio;
20
21
pub struct Xwayland {
22
    pub display: u32,
23
    listeners: Vec<UnixListener>,
24
    watch: Vec<(usize, RegistrationToken)>,
25
    running: bool,
26
    pub wm: Option<X11Wm>,
27
    /// Override-redirect windows such as menus and tooltips, bottom to top.
28
    pub unmanaged: Vec<X11Surface>,
29
}
30
31
impl Drop for Xwayland {
32
    fn drop(&mut self) {
33
        let _ = std::fs::remove_file(socket_path(self.display));
34
        let _ = std::fs::remove_file(lock_path(self.display));
35
    }
36
}
37
38
fn socket_path(n: u32) -> String {
39
    format!("/tmp/.X11-unix/X{n}")
40
}
41
42
fn lock_path(n: u32) -> String {
43
    format!("/tmp/.X{n}-lock")
44
}
45
46
/// Claims the first free X display, taking over stale locks left by dead
47
/// servers.
48
fn claim() -> Option<(u32, Vec<UnixListener>)> {
49
    let _ = std::fs::create_dir_all("/tmp/.X11-unix");
50
    for n in 0..64 {
51
        let lock = lock_path(n);
52
        let created = std::fs::OpenOptions::new()
53
            .write(true)
54
            .create_new(true)
55
            .open(&lock);
56
        let file = match created {
57
            Ok(f) => f,
58
            Err(_) => {
59
                let owner = std::fs::read_to_string(&lock)
60
                    .ok()
61
                    .and_then(|s| s.trim().parse::<u32>().ok());
62
                let stale = owner.is_some_and(|pid| !holds_display(pid));
63
                if !stale || std::fs::remove_file(&lock).is_err() {
64
                    continue;
65
                }
66
                match std::fs::OpenOptions::new()
67
                    .write(true)
68
                    .create_new(true)
69
                    .open(&lock)
70
                {
71
                    Ok(f) => f,
72
                    Err(_) => continue,
73
                }
74
            }
75
        };
76
        use std::io::Write;
77
        let _ = writeln!(&file, "{:>10}", std::process::id());
78
        let path = socket_path(n);
79
        let _ = std::fs::remove_file(&path);
80
        let Ok(fs) = UnixListener::bind(&path) else {
81
            let _ = std::fs::remove_file(&lock);
82
            continue;
83
        };
84
        let mut listeners = vec![fs];
85
        if let Ok(addr) = SocketAddr::from_abstract_name(path.as_bytes())
86
            && let Ok(abs) = UnixListener::bind_addr(&addr)
87
        {
88
            listeners.push(abs);
89
        }
90
        return Some((n, listeners));
91
    }
92
    None
93
}
94
95
pub fn init(state: &mut State) {
96
    let Some((number, listeners)) = claim() else {
97
        tracing::warn!("no free X display, X11 clients will not run");
98
        return;
99
    };
100
    let mut watch = Vec::new();
101
    for (i, l) in listeners.iter().enumerate() {
102
        let Ok(l) = l.try_clone() else { continue };
103
        let token = state.core.handle.insert_source(
104
            Generic::new(l, Interest::READ, Mode::Level),
105
            move |_, _, state| {
106
                start(state, i);
107
                Ok(PostAction::Disable)
108
            },
109
        );
110
        if let Ok(t) = token {
111
            watch.push((i, t));
112
        }
113
    }
114
    tracing::info!("X11 clients will start Xwayland on :{number}");
115
    state.services.xwayland = Some(Xwayland {
116
        display: number,
117
        listeners,
118
        watch,
119
        running: false,
120
        wm: None,
121
        unmanaged: Vec::new(),
122
    });
123
}
124
125
/// Waits for the next X client to knock.
126
fn rearm(state: &mut State) {
127
    let Some(x) = state.services.xwayland.as_mut() else {
128
        return;
129
    };
130
    x.running = false;
131
    for (_, t) in &x.watch {
132
        let _ = state.core.handle.enable(t);
133
    }
134
}
135
136
fn set_inherit(listeners: &[UnixListener], inherit: bool) {
137
    for l in listeners {
138
        let flags = fcntl_getfd(l.as_fd()).unwrap_or(FdFlags::CLOEXEC);
139
        let flags = if inherit {
140
            flags - FdFlags::CLOEXEC
141
        } else {
142
            flags | FdFlags::CLOEXEC
143
        };
144
        let _ = fcntl_setfd(l.as_fd(), flags);
145
    }
146
}
147
148
fn start(state: &mut State, fired: usize) {
149
    let Some(x) = state.services.xwayland.as_mut() else {
150
        return;
151
    };
152
    if x.running {
153
        return;
154
    }
155
    x.running = true;
156
    for (i, t) in &x.watch {
157
        if *i != fired {
158
            state.core.handle.disable(t).ok();
159
        }
160
    }
161
    let mut args = Vec::new();
162
    for l in &x.listeners {
163
        args.push("-listenfd".to_string());
164
        args.push(std::os::fd::AsRawFd::as_raw_fd(l).to_string());
165
    }
166
    // smithay only passes on the sockets it creates itself, so ours are
167
    // made inheritable just for this one spawn.
168
    set_inherit(&x.listeners, true);
169
    let spawned = XWayland::spawn(
170
        &state.core.display,
171
        None,
172
        std::iter::empty::<(String, String)>(),
173
        args,
174
        false,
175
        Stdio::null(),
176
        Stdio::null(),
177
        |_| {},
178
    );
179
    set_inherit(&x.listeners, false);
180
    let (xwayland, client) = match spawned {
181
        Ok(s) => s,
182
        Err(e) => {
183
            tracing::error!("cannot start Xwayland, X11 clients will not run: {e}");
184
            return;
185
        }
186
    };
187
    tracing::info!("starting Xwayland");
188
    let inserted = state
189
        .core
190
        .handle
191
        .insert_source(xwayland, move |event, _, state| match event {
192
            XWaylandEvent::Ready { x11_socket, .. } => {
193
                let dh = state.core.display.clone();
194
                match X11Wm::start_wm(state.core.handle.clone(), &dh, x11_socket, client.clone()) {
195
                    Ok(wm) => {
196
                        tracing::info!("Xwayland ready");
197
                        if let Some(x) = state.services.xwayland.as_mut() {
198
                            x.wm = Some(wm);
199
                        }
200
                    }
201
                    Err(e) => tracing::error!("cannot manage Xwayland windows: {e}"),
202
                }
203
            }
204
            XWaylandEvent::Error => {
205
                tracing::warn!("Xwayland failed to start");
206
                rearm(state);
207
            }
208
        });
209
    if let Err(e) = inserted {
210
        tracing::error!("cannot watch Xwayland: {e}");
211
    }
212
}
213
214
impl XWaylandShellHandler for State {
215
    fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
216
        &mut self.protocols.xwayland_shell_state
217
    }
218
219
    fn surface_associated(&mut self, _xwm: XwmId, surface: WlSurface, window: X11Surface) {
220
        if let Some(id) = self.x11_window_id(&window) {
221
            self.wm.index(id);
222
        }
223
        self.on_buffer(&surface);
224
        self.dirty.redraw = true;
225
    }
226
}
227
228
impl XwmHandler for State {
229
    fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
230
        self.services
231
            .xwayland
232
            .as_mut()
233
            .and_then(|x| x.wm.as_mut())
234
            .expect("X11 events only arrive while the WM runs")
235
    }
236
237
    fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
238
239
    fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
240
241
    fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {
242
        if let Err(e) = window.set_mapped(true) {
243
            tracing::warn!("cannot map X11 window: {e}");
244
            return;
245
        }
246
        let float = floats(&window).then(|| window.geometry());
247
        self.map_window(Window::new_x11_window(window), float);
248
    }
249
250
    fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
251
        if let Some(x) = self.services.xwayland.as_mut() {
252
            x.unmanaged.retain(|w| w != &window);
253
            x.unmanaged.push(window);
254
        }
255
        self.dirty.redraw = true;
256
    }
257
258
    fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
259
        self.forget_x11(&window);
260
        if !window.is_override_redirect() {
261
            logged("unmap", window.set_mapped(false));
262
        }
263
    }
264
265
    fn destroyed_window(&mut self, _xwm: XwmId, window: X11Surface) {
266
        self.forget_x11(&window);
267
    }
268
269
    fn configure_request(
270
        &mut self,
271
        _xwm: XwmId,
272
        window: X11Surface,
273
        x: Option<i32>,
274
        y: Option<i32>,
275
        w: Option<u32>,
276
        h: Option<u32>,
277
        _reorder: Option<Reorder>,
278
    ) {
279
        if self.x11_window_id(&window).is_some() {
280
            logged("configure", window.configure(None));
281
            return;
282
        }
283
        let mut geo = window.geometry();
284
        geo.loc.x = x.unwrap_or(geo.loc.x);
285
        geo.loc.y = y.unwrap_or(geo.loc.y);
286
        geo.size.w = w.map_or(geo.size.w, |w| w as i32);
287
        geo.size.h = h.map_or(geo.size.h, |h| h as i32);
288
        logged("configure", window.configure(geo));
289
    }
290
291
    fn configure_notify(
292
        &mut self,
293
        _xwm: XwmId,
294
        window: X11Surface,
295
        _geometry: Rectangle<i32, Logical>,
296
        _above: Option<smithay::xwayland::xwm::X11Window>,
297
    ) {
298
        if window.is_override_redirect() {
299
            self.dirty.redraw = true;
300
        }
301
    }
302
303
    fn resize_request(
304
        &mut self,
305
        _xwm: XwmId,
306
        _window: X11Surface,
307
        _button: u32,
308
        _edge: ResizeEdge,
309
    ) {
310
    }
311
312
    fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {}
313
314
    fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
315
        self.x11_fullscreen(&window, true);
316
    }
317
318
    fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
319
        self.x11_fullscreen(&window, false);
320
    }
321
322
    fn property_notify(&mut self, _xwm: XwmId, window: X11Surface, property: WmWindowProperty) {
323
        if matches!(property, WmWindowProperty::Title | WmWindowProperty::Class)
324
            && let Some(id) = self.x11_window_id(&window)
325
        {
326
            self.refresh_identity(id);
327
        }
328
    }
329
330
    fn disconnected(&mut self, _xwm: XwmId) {
331
        tracing::info!("Xwayland exited");
332
        let gone: Vec<_> = self
333
            .wm
334
            .iter()
335
            .filter(|(_, w)| w.window.x11_surface().is_some())
336
            .map(|(id, _)| id)
337
            .collect();
338
        for id in gone {
339
            self.remove_window(id);
340
        }
341
        if let Some(x) = self.services.xwayland.as_mut() {
342
            x.wm = None;
343
            x.unmanaged.clear();
344
        }
345
        rearm(self);
346
    }
347
}
348
349
/// Dialogs, splash screens and fixed-size windows float at their own size.
350
fn floats(window: &X11Surface) -> bool {
351
    let kind = matches!(
352
        window.window_type(),
353
        Some(
354
            WmWindowType::Dialog
355
                | WmWindowType::Splash
356
                | WmWindowType::Utility
357
                | WmWindowType::Toolbar
358
                | WmWindowType::Menu
359
        )
360
    );
361
    kind || fixed_size(window).is_some() || window.is_transient_for().is_some()
362
}
363
364
/// The size of a window that asks for exactly one.
365
pub fn fixed_size(window: &X11Surface) -> Option<Size<i32, Logical>> {
366
    match (window.min_size(), window.max_size()) {
367
        (Some(min), Some(max)) if min == max && min.w > 0 && min.h > 0 => Some(min),
368
        _ => None,
369
    }
370
}
371
372
impl State {
373
    pub fn x11_window_id(&self, window: &X11Surface) -> Option<monolith_layout::WindowId> {
374
        self.wm
375
            .iter()
376
            .find(|(_, w)| w.window.x11_surface() == Some(window))
377
            .map(|(id, _)| id)
378
    }
379
380
    fn forget_x11(&mut self, window: &X11Surface) {
381
        if let Some(x) = self.services.xwayland.as_mut() {
382
            x.unmanaged.retain(|w| w != window);
383
        }
384
        if let Some(id) = self.x11_window_id(window) {
385
            self.remove_window(id);
386
        }
387
        self.dirty.redraw = true;
388
    }
389
390
    fn x11_fullscreen(&mut self, window: &X11Surface, on: bool) {
391
        if let Some(id) = self.x11_window_id(window)
392
            && self
393
                .wm
394
                .layout
395
                .window(id)
396
                .is_some_and(|w| w.fullscreen != on)
397
        {
398
            self.wm.layout.toggle_fullscreen(id);
399
            self.wm.layout.focus_if_covering(id);
400
            self.apply_layout();
401
        }
402
    }
403
}
404
405
/// X11 requests fail when the window is already gone, which is routine, but
406
/// a failure on a live window means it never got the geometry we chose.
407
pub fn logged<E: std::fmt::Display>(what: &str, result: Result<(), E>) {
408
    if let Err(e) = result {
409
        tracing::debug!("X11 {what} failed: {e}");
410
    }
411
}
412
413
/// Whether `pid` is alive and could own an X display lock. A SIGKILLed
414
/// monolith leaves its lock behind, and its pid may since belong to anything.
415
fn holds_display(pid: u32) -> bool {
416
    std::fs::read_to_string(format!("/proc/{pid}/comm"))
417
        .is_ok_and(|comm| comm.starts_with('X') || comm.trim() == "monolith")
418
}