From d2cbadc670843e366e9ce15ee772ebdf59056752 Mon Sep 17 00:00:00 2001 From: Victor Berger Date: Mon, 30 Apr 2018 21:28:17 +0200 Subject: [PATCH] xwayland: core infrastructure --- Cargo.toml | 12 +- src/lib.rs | 3 + src/xwayland/mod.rs | 4 + src/xwayland/x11_sockets.rs | 136 ++++++++++++ src/xwayland/xserver.rs | 401 ++++++++++++++++++++++++++++++++++++ 5 files changed, 550 insertions(+), 6 deletions(-) create mode 100644 src/xwayland/mod.rs create mode 100644 src/xwayland/x11_sockets.rs create mode 100644 src/xwayland/xserver.rs diff --git a/Cargo.toml b/Cargo.toml index fae0421..93f6b2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,15 +7,15 @@ description = "Smithay is a library for writing wayland compositors." repository = "https://github.com/Smithay/smithay" [dependencies] -wayland-server = "0.20.1" -wayland-sys = "0.20.1" +wayland-server = "0.20.2" +wayland-sys = "0.20.2" nix = "0.10.0" xkbcommon = "0.2.1" tempfile = "2.1.5" slog = { version = "2.1.1" } slog-stdlog = "3.0.2" libloading = "0.4.0" -wayland-client = { version = "0.20.1", optional = true } +wayland-client = { version = "0.20.2", optional = true } winit = { version = "0.10.0", optional = true } drm = { version = "^0.3.1", optional = true } gbm = { version = "^0.4.0", optional = true, default-features = false, features = ["drm-support"] } @@ -24,7 +24,7 @@ input = { version = "0.4.0", optional = true } udev = { version = "0.2.0", optional = true } dbus = { version = "0.6.1", optional = true } systemd = { version = "^0.2.0", optional = true } -wayland-protocols = { version = "0.20.1", features = ["unstable_protocols", "server"] } +wayland-protocols = { version = "0.20.2", features = ["unstable_protocols", "server"] } image = "0.17.0" error-chain = "0.11.0" lazy_static = "1.0.0" @@ -38,7 +38,7 @@ slog-async = "2.2" rand = "0.3" [features] -default = ["backend_winit", "backend_drm", "backend_libinput", "backend_udev", "renderer_glium"] +default = ["backend_winit", "backend_drm", "backend_libinput", "backend_udev", "renderer_glium", "xwayland"] backend_winit = ["winit", "wayland-server/dlopen", "wayland-client/dlopen"] backend_drm = ["drm", "gbm"] backend_libinput = ["input"] @@ -47,4 +47,4 @@ backend_session_udev = ["udev", "backend_session"] backend_session_logind = ["dbus", "systemd", "backend_session"] backend_udev = ["udev", "backend_drm", "backend_session_udev"] renderer_glium = ["glium"] - +xwayland = [] diff --git a/src/lib.rs b/src/lib.rs index ec1d923..9366821 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,9 @@ pub mod backend; pub mod wayland; pub mod utils; +#[cfg(feature = "xwayland")] +pub mod xwayland; + fn slog_or_stdlog(logger: L) -> ::slog::Logger where L: Into>, diff --git a/src/xwayland/mod.rs b/src/xwayland/mod.rs new file mode 100644 index 0000000..5e21dd1 --- /dev/null +++ b/src/xwayland/mod.rs @@ -0,0 +1,4 @@ +mod xserver; +mod x11_sockets; + +pub use self::xserver::{XWayland, XWindowManager}; diff --git a/src/xwayland/x11_sockets.rs b/src/xwayland/x11_sockets.rs new file mode 100644 index 0000000..3b06538 --- /dev/null +++ b/src/xwayland/x11_sockets.rs @@ -0,0 +1,136 @@ +use std::io::{Read, Write}; +use std::os::unix::io::RawFd; + +use nix::{Error as NixError, Result as NixResult}; +use nix::errno::Errno; +use nix::sys::socket; + +pub(crate) fn make_pair() -> Result<(RawFd, RawFd), ()> { + socket::socketpair( + socket::AddressFamily::Unix, + socket::SockType::Stream, + None, + socket::SockFlag::SOCK_CLOEXEC, + ).map_err(|_| ()) +} + +/// Find a free X11 display slot and setup +pub(crate) fn prepare_x11_sockets() -> Result<(u32, [RawFd; 2]), ()> { + for d in 0..33 { + // if fails, try the next one + if let Err(()) = grab_lockfile(d) { + continue; + } + // we got a lockfile, try and create the socket + if let Ok(fds) = open_x11_sockets_for_display(d) { + return Ok((d, fds)); + } + // creating the sockets failed, for some readon ? + // release the lockfile and try with the next + release_lockfile(d); + } + // If we reach here, all values from 0 to 32 failed + // we need to stop trying at some point + return Err(()); +} + +/// Remove the X11 sockets for a given display number +pub(crate) fn cleanup_x11_sockets(display: u32) { + let _ = ::std::fs::remove_file(format!("/tmp/.X11-unix/X{}", display)); + let _ = ::std::fs::remove_file(format!("/tmp/.X{}-lock", display)); +} + +/// Try to grab a lockfile for given X display number +fn grab_lockfile(display: u32) -> Result<(), ()> { + let filename = format!("/tmp/.X{}-lock", display); + let lockfile = ::std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&filename); + match lockfile { + Ok(mut file) => { + // we got it, write our PID in it and we're good + let ret = file.write_fmt(format_args!("{:>10}", ::nix::unistd::Pid::this())); + if let Err(_) = ret { + // write to the file failed ? we abandon + ::std::mem::drop(file); + let _ = ::std::fs::remove_file(&filename); + return Err(()); + } else { + // we got the lockfile and wrote our pid to it, all is good + return Ok(()); + } + } + Err(_) => { + // we could not open the file, now we try to read it + // and if it contains the pid of a process that no longer + // exist (so if a previous x server claimed it and did not + // exit gracefully and remove it), we claim it + // if we can't open it, give up + let mut file = ::std::fs::File::open(&filename).map_err(|_| ())?; + let mut spid = [0u8; 11]; + file.read_exact(&mut spid).map_err(|_| ())?; + ::std::mem::drop(file); + let pid = ::nix::unistd::Pid::from_raw(::std::str::from_utf8(&spid) + .map_err(|_| ())? + .trim() + .parse::() + .map_err(|_| ())?); + if let Err(NixError::Sys(Errno::ESRCH)) = ::nix::sys::signal::kill(pid, None) { + // no process whose pid equals the contents of the lockfile exists + // remove the lockfile and try grabbing it again + let _ = ::std::fs::remove_file(filename); + return grab_lockfile(display); + } + // if we reach here, this lockfile exists and is probably in use, give up + return Err(()); + } + } +} + +/// Release an X11 lockfile +fn release_lockfile(display: u32) { + let filename = format!("/tmp/.X{}-lock", display); + let _ = ::std::fs::remove_file(filename); +} + +/// Open the two unix sockets an X server listens on +/// +/// SHould only be done after the associated lockfile is aquired! +fn open_x11_sockets_for_display(display: u32) -> NixResult<[RawFd; 2]> { + let fs_socket = open_socket( + socket::UnixAddr::new(format!("/tmp/.X11-unix/X{}", display).as_bytes()).unwrap(), // We know this path is not to long, this unwrap cannot fail + )?; + let ret = open_socket( + socket::UnixAddr::new_abstract(format!("/tmp/.X11-unix/X{}", display).as_bytes()).unwrap(), // We know this path is not to long, this unwrap cannot fail + ); + match ret { + Ok(abstract_socket) => Ok([fs_socket, abstract_socket]), + Err(e) => { + // close the first socket and return the error + let _ = ::nix::unistd::close(fs_socket); + Err(e) + } + } +} + +/// Open an unix socket for listening and bind it to given path +fn open_socket(addr: socket::UnixAddr) -> NixResult { + // create an unix stream socket + let fd = socket::socket( + socket::AddressFamily::Unix, + socket::SockType::Stream, + socket::SockFlag::SOCK_CLOEXEC, + None, + )?; + // bind it to requested address + if let Err(e) = socket::bind(fd, &socket::SockAddr::Unix(addr)) { + let _ = ::nix::unistd::close(fd); + return Err(e); + } + if let Err(e) = socket::listen(fd, 1) { + let _ = ::nix::unistd::close(fd); + return Err(e); + } + Ok(fd) +} diff --git a/src/xwayland/xserver.rs b/src/xwayland/xserver.rs new file mode 100644 index 0000000..9a788b5 --- /dev/null +++ b/src/xwayland/xserver.rs @@ -0,0 +1,401 @@ +/* + * Steps of Xwayland server creation + * + * Sockets to create: + * - a pair for Xwayland to connect to smithay as a wayland client, we use our + * end to insert the Xwayland client in the display + * - a pair for smithay to connect to Xwayland as a WM, we give our end to the + * WM and it deals with it + * - 2 listening sockets on which the Xwayland server will listen. We need to + * bind them ouserlves so we know what value put in the $DISPLAY env variable. + * This involves some dance with a lockfile to ensure there is no collision with + * an other starting xserver + * if we listen on display $D, their paths are respectly: + * - /tmp/.X11-unix/X$D + * - @/tmp/.X11-unix/X$D (abstract socket) + * + * The XWayland server is spawned via fork+exec. + * -> wlroot does a double-fork while weston a single one, why ?? + * -> https://stackoverflow.com/questions/881388/ + * -> once it is started, it sends us a SIGUSR1, we need to setup a listener + * for it and when we receive it we can launch the WM + * -> we need to track if the Xwayland crashes, to restart it + * + * cf https://github.com/swaywm/wlroots/blob/master/xwayland/xwayland.c + * + */ +use std::cell::RefCell; +use std::rc::Rc; +use std::env; +use std::ffi::CString; +use std::os::unix::io::RawFd; + +use nix::{Error as NixError, Result as NixResult}; +use nix::errno::Errno; +use nix::unistd::{close, fork, ForkResult, Pid}; +use nix::sys::signal; + +use wayland_server::{Client, Display, LoopToken}; +use wayland_server::sources::{SignalEvent, Source}; + +use super::x11_sockets::{make_pair, cleanup_x11_sockets, prepare_x11_sockets}; + +/// The XWayland handle +pub struct XWayland { + inner: Rc>>, +} + +/// Trait to be implemented by you WM for Xwayland +/// +/// This is a very low-level trait, only notifying you +/// when the connection with XWayland is up, or when +/// it terminates. +/// +/// You WM must be able handle the XWayland server connecting +/// then disconnecting several time in a row, but only a single +/// connection will be active at any given time. +pub trait XWindowManager { + /// The XWayland server is ready + /// + /// Your previlegied connection to it is this `RawFd` + fn xwayland_ready(&mut self, fd: RawFd, client: Client); + /// The XWayland server has exited + fn xwayland_exited(&mut self); +} + +impl XWayland { + /// Start the XWayland server + pub fn init(wm: WM, token: LoopToken, display: Rc>) -> Result, ()> { + let inner = Rc::new(RefCell::new(Inner { + wm, + token, + wayland_display: display, + instance: None, + })); + launch(&inner)?; + Ok(XWayland { inner }) + } +} + +impl Drop for XWayland { + fn drop(&mut self) { + self.inner.borrow_mut().shutdown(); + } +} + +struct XWaylandInstance { + display: u32, + wayland_client: Client, + sigusr1_handler: Option>, + wm_fd: RawFd, + started_at: ::std::time::Instant, + child_pid: Option, +} + +// Inner implementation of the XWayland manager +struct Inner { + wm: WM, + token: LoopToken, + wayland_display: Rc>, + instance: Option, +} + +// Launch an XWayland server +// +// Does nothing if there is already a launched instance +fn launch(inner: &Rc>>) -> Result<(), ()> { + let mut guard = inner.borrow_mut(); + if guard.instance.is_some() { + return Ok(()); + } + + let (display, x_fds) = prepare_x11_sockets()?; + + let (x_wm_x11, x_wm_me) = match make_pair() { + Ok(v) => v, + Err(()) => { + // cleanup + for &f in &x_fds { + let _ = close(f); + } + cleanup_x11_sockets(display); + return Err(()); + } + }; + let (wl_x11, wl_me) = match make_pair() { + Ok(v) => v, + Err(()) => { + // cleanup + for &f in &x_fds { + let _ = close(f); + } + let _ = close(x_wm_x11); + let _ = close(x_wm_me); + cleanup_x11_sockets(display); + return Err(()); + } + }; + + // we have now created all the required sockets + + // record launch time + let creation_time = ::std::time::Instant::now(); + + // create the wayland client for XWayland + let client = unsafe { guard.wayland_display.borrow_mut().create_client(wl_me) }; + client.set_user_data(Rc::into_raw(inner.clone()) as *const () as *mut ()); + client.set_destructor(client_destroy::); + + // setup the SIGUSR1 handler + let my_inner = inner.clone(); + let sigusr1_handler = match guard + .token + .add_signal_event_source(signal::Signal::SIGUSR1, move |_, ()| { + xwayland_ready(&my_inner) + }) { + Ok(v) => v, + Err(_) => { + // If that fails (can it even fail ?), cleanup + for &f in &x_fds { + let _ = close(f); + } + let _ = close(x_wm_x11); + let _ = close(x_wm_me); + let _ = close(wl_x11); + let _ = close(wl_me); + cleanup_x11_sockets(display); + return Err(()); + } + }; + + // all is ready, we can do the fork dance + let child_pid = match fork() { + Ok(ForkResult::Parent { child }) => { + // we are the main smithay process + child + } + Ok(ForkResult::Child) => { + // we are the first child + let ppid = Pid::parent(); + let mut set = signal::SigSet::empty(); + set.add(signal::Signal::SIGUSR1); + set.add(signal::Signal::SIGCHLD); + // we can't handle errors here anyway + let _ = signal::sigprocmask(signal::SigmaskHow::SIG_BLOCK, Some(&set), None); + match fork() { + Ok(ForkResult::Parent { child }) => { + // we are still the first child + let sig = set.wait(); + // send USR1 to parent + let _ = signal::kill(ppid, signal::Signal::SIGUSR1); + // Parent will wait for us and know from out + // exit status if XWayland launch was a success or not =) + if let Ok(signal::Signal::SIGCHLD) = sig { + // Xwayland has exited before being ready + let _ = ::nix::sys::wait::waitpid(child, None); + unsafe { ::nix::libc::exit(1) }; + } + unsafe { ::nix::libc::exit(0) }; + } + Ok(ForkResult::Child) => { + // we are the second child, we exec xwayland + match exec_xwayland(display, wl_x11, x_wm_x11, &x_fds) { + Ok(x) => match x {}, + Err(e) => { + // well, what can we do ? + eprintln!("[smithay] exec XWayland failed: {:?}", e); + unsafe { ::nix::libc::exit(1) }; + } + } + } + Err(e) => { + // well, what can we do ? + eprintln!("[smithay] XWayland second fork failed: {:?}", e); + unsafe { ::nix::libc::exit(1) }; + } + } + } + Err(e) => { + eprintln!("[smithay] XWayland first fork failed: {:?}", e); + // fork failed ? cleanup + for &f in &x_fds { + let _ = close(f); + } + let _ = close(x_wm_x11); + let _ = close(x_wm_me); + let _ = close(wl_x11); + let _ = close(wl_me); + cleanup_x11_sockets(display); + return Err(()); + } + }; + + // now, close the FDs that were given to XWayland + for &f in &x_fds { + let _ = close(f); + } + let _ = close(x_wm_x11); + let _ = close(wl_x11); + + guard.instance = Some(XWaylandInstance { + display, + wayland_client: client, + sigusr1_handler: Some(sigusr1_handler), + wm_fd: x_wm_me, + started_at: creation_time, + child_pid: Some(child_pid), + }); + + Ok(()) +} + +impl Inner { + // Shutdown the XWayland server and cleanup everything + fn shutdown(&mut self) { + // don't do anything if not running + if let Some(mut instance) = self.instance.take() { + self.wm.xwayland_exited(); + // kill the client + instance.wayland_client.kill(); + // remove the event source + if let Some(s) = instance.sigusr1_handler.take() { + s.remove(); + } + // close the WM fd + let _ = close(instance.wm_fd); + // cleanup the X11 sockets + cleanup_x11_sockets(instance.display); + ::std::env::remove_var("DISPLAY"); + // We do like wlroots: + // > We do not kill the Xwayland process, it dies to broken pipe + // > after we close our side of the wm/wl fds. This is more reliable + // > than trying to kill something that might no longer be Xwayland. + } + } +} + +fn client_destroy(data: *mut ()) { + let inner = unsafe { Rc::from_raw(data as *const () as *const RefCell>) }; + + // shutdown the server + let started_at = inner.borrow().instance.as_ref().map(|i| i.started_at); + inner.borrow_mut().shutdown(); + + // restart it, unless we really just started it, if it crashes right + // at startup there is no point + if started_at.map(|t| t.elapsed().as_secs()).unwrap_or(10) > 5 { + let _ = launch(&inner); + } +} + +fn xwayland_ready(inner: &Rc>>) { + use nix::sys::wait; + let mut guard = inner.borrow_mut(); + let inner = &mut *guard; + // instance should never be None at this point + let instance = inner.instance.as_mut().unwrap(); + let wm = &mut inner.wm; + // neither the pid + let pid = instance.child_pid.unwrap(); + + // find out if the launch was a success by waiting on the intermediate child + let mut success: bool; + loop { + match wait::waitpid(pid, None) { + Ok(wait::WaitStatus::Exited(_, 0)) => { + // XWayland was correctly started :) + success = true; + break; + } + Err(NixError::Sys(Errno::EINTR)) => { + // interupted, retry + continue; + } + _ => { + // something went wrong :( + success = false; + break; + } + } + } + + if success { + // signal the WM + wm.xwayland_ready(instance.wm_fd, instance.wayland_client.clone()); + + // setup the environemnt + ::std::env::set_var("DISPLAY", format!(":{}", instance.display)); + } + + // in all cases, cleanup + if let Some(s) = instance.sigusr1_handler.take() { + s.remove(); + } +} + +enum Void {} + +/// Exec xwayland with given sockets on given display +/// +/// If this returns, that means that something failed +fn exec_xwayland( + display: u32, + wayland_socket: RawFd, + wm_socket: RawFd, + listen_sockets: &[RawFd], +) -> NixResult { + // uset the CLOEXEC flag from the sockets we need to pass + // to xwayland + unset_cloexec(wayland_socket)?; + unset_cloexec(wm_socket)?; + for &socket in listen_sockets { + unset_cloexec(socket)?; + } + // prepare the arguments to Xwayland + let mut args = vec![ + CString::new("Xwayland").unwrap(), + CString::new(format!(":{}", display)).unwrap(), + CString::new("-rootless").unwrap(), + CString::new("-terminate").unwrap(), + CString::new("-wm").unwrap(), + CString::new(format!("{}", wm_socket)).unwrap(), + ]; + for &socket in listen_sockets { + args.push(CString::new("-listen").unwrap()); + args.push(CString::new(format!("{}", socket)).unwrap()); + } + // setup the environment: clear everything except PATH and XDG_RUNTIME_DIR + for (key, _) in env::vars_os() { + if key.to_str() == Some("PATH") || key.to_str() == Some("XDG_RUNTIME_DIR") { + continue; + } + env::remove_var(key); + } + // the WAYLAND_SOCKET var tells Xwayland where to connect as a wayland client + env::set_var("WAYLAND_SOCKET", format!("{}", wayland_socket)); + + // ignore SIGUSR1, this will make the Xwayland server send us this + // signal when it is ready apparently + unsafe { + use nix::sys::signal::*; + sigaction( + Signal::SIGUSR1, + &SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty()), + )?; + } + + // run it + let ret = ::nix::unistd::execvp(&CString::new("Xwayland").unwrap(), &args)?; + // small dance to actually return Void + match ret {} +} + +/// Remove the O_CLOEXEC flag from this Fd +/// +/// This means that the Fd will *not* be automatically +/// closed when we exec() into Xwayland +fn unset_cloexec(fd: RawFd) -> NixResult<()> { + use nix::fcntl::{fcntl, FcntlArg, FdFlag}; + fcntl(fd, FcntlArg::F_SETFD(FdFlag::empty()))?; + Ok(()) +}