diff --git a/.travis.yml b/.travis.yml index 7d93485..cd24a90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,7 @@ env: - FEATURES="backend_session_udev" - FEATURES="backend_session_logind" - FEATURES="renderer_glium" + - FEATURES="xwayland" # test default features - FEATURES="default" # test all features simultaneously 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..2fb4486 --- /dev/null +++ b/src/xwayland/x11_sockets.rs @@ -0,0 +1,137 @@ +use std::io::{Read, Write}; +use std::os::unix::io::FromRawFd; +use std::os::unix::net::UnixStream; + +use nix::{Error as NixError, Result as NixResult}; +use nix::errno::Errno; +use nix::sys::socket; + +/// Find a free X11 display slot and setup +pub(crate) fn prepare_x11_sockets(log: ::slog::Logger) -> Result<(X11Lock, [UnixStream; 2]), ()> { + for d in 0..33 { + // if fails, try the next one + if let Ok(lock) = X11Lock::grab(d, log.clone()) { + // we got a lockfile, try and create the socket + if let Ok(sockets) = open_x11_sockets_for_display(d) { + return Ok((lock, sockets)); + } + } + } + // If we reach here, all values from 0 to 32 failed + // we need to stop trying at some point + return Err(()); +} + +pub(crate) struct X11Lock { + display: u32, + log: ::slog::Logger, +} + +impl X11Lock { + /// Try to grab a lockfile for given X display number + fn grab(display: u32, log: ::slog::Logger) -> Result { + debug!(log, "Attempting to aquire an X11 display lock"; "D" => display); + 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 { + debug!(log, "X11 lock aquired"; "D" => display); + // we got the lockfile and wrote our pid to it, all is good + return Ok(X11Lock { display, log }); + } + } + Err(_) => { + debug!(log, "Failed to acquire lock"; "D" => display); + // 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 + if let Ok(()) = ::std::fs::remove_file(filename) { + debug!(log, "Lock was blocked by a defunct X11 server, trying again"; "D" => display); + return X11Lock::grab(display, log); + } else { + // we could not remove the lockfile, abort + return Err(()); + } + } + // if we reach here, this lockfile exists and is probably in use, give up + return Err(()); + } + } + } + + pub(crate) fn display(&self) -> u32 { + self.display + } +} + +impl Drop for X11Lock { + fn drop(&mut self) { + info!(self.log, "Cleaning up X11 lock."); + // Cleanup all the X11 files + if let Err(e) = ::std::fs::remove_file(format!("/tmp/.X11-unix/X{}", self.display)) { + warn!(self.log, "Failed to remove X11 socket"; "error" => format!("{:?}", e)); + } + if let Err(e) = ::std::fs::remove_file(format!("/tmp/.X{}-lock", self.display)) { + warn!(self.log, "Failed to remove X11 lockfile"; "error" => format!("{:?}", e)); + } + } +} + +/// 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<[UnixStream; 2]> { + let path = format!("/tmp/.X11-unix/X{}", display); + // We know this path is not to long, these unwrap cannot fail + let fs_addr = socket::UnixAddr::new(path.as_bytes()).unwrap(); + let abs_addr = socket::UnixAddr::new_abstract(path.as_bytes()).unwrap(); + let fs_socket = open_socket(fs_addr)?; + let abstract_socket = open_socket(abs_addr)?; + Ok([fs_socket, abstract_socket]) +} + +/// 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(unsafe { FromRawFd::from_raw_fd(fd) }) +} diff --git a/src/xwayland/xserver.rs b/src/xwayland/xserver.rs new file mode 100644 index 0000000..3240ff8 --- /dev/null +++ b/src/xwayland/xserver.rs @@ -0,0 +1,384 @@ +/* + * 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::{AsRawFd, IntoRawFd}; +use std::os::unix::net::UnixStream; + +use nix::{Error as NixError, Result as NixResult}; +use nix::errno::Errno; +use nix::unistd::{fork, ForkResult, Pid}; +use nix::sys::signal; + +use wayland_server::{Client, Display, LoopToken}; +use wayland_server::sources::{SignalEvent, Source}; + +use super::x11_sockets::{X11Lock, 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 `UnixStream` + fn xwayland_ready(&mut self, connection: UnixStream, 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>, + logger: L, + ) -> Result, ()> + where + L: Into>, + { + let log = ::slog_or_stdlog(logger); + let inner = Rc::new(RefCell::new(Inner { + wm, + token, + wayland_display: display, + instance: None, + log: log.new(o!("smithay_module" => "XWayland")), + })); + launch(&inner)?; + Ok(XWayland { inner }) + } +} + +impl Drop for XWayland { + fn drop(&mut self) { + self.inner.borrow_mut().shutdown(); + } +} + +struct XWaylandInstance { + display_lock: X11Lock, + wayland_client: Client, + sigusr1_handler: Option>, + wm_fd: Option, + 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, + log: ::slog::Logger, +} + +// 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(()); + } + + info!(guard.log, "Starting XWayland"); + + let (x_wm_x11, x_wm_me) = UnixStream::pair().map_err(|_| ())?; + let (wl_x11, wl_me) = UnixStream::pair().map_err(|_| ())?; + + let (lock, x_fds) = prepare_x11_sockets(guard.log.clone())?; + + // 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.into_raw_fd()) + }; + 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 = guard + .token + .add_signal_event_source(signal::Signal::SIGUSR1, move |_, ()| { + xwayland_ready(&my_inner) + }) + .map_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(lock.display(), wl_x11, x_wm_x11, &x_fds) { + Ok(x) => match x {}, + Err(e) => { + // well, what can we do ? + error!(guard.log, "exec XWayland failed"; "err" => format!("{:?}", e)); + unsafe { ::nix::libc::exit(1) }; + } + } + } + Err(e) => { + // well, what can we do ? + error!(guard.log, "XWayland second fork failed"; "err" => format!("{:?}", e)); + unsafe { ::nix::libc::exit(1) }; + } + } + } + Err(e) => { + error!(guard.log, "XWayland first fork failed"; "err" => format!("{:?}", e)); + return Err(()); + } + }; + + guard.instance = Some(XWaylandInstance { + display_lock: lock, + wayland_client: client, + sigusr1_handler: Some(sigusr1_handler), + wm_fd: Some(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() { + info!(self.log, "Shutting down XWayland."); + 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(); + } + // All connexions and lockfiles are cleaned by their destructors + + // Remove DISPLAY from the env + ::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 { + warn!(inner.borrow().log, "XWayland crashed, restarting."); + let _ = launch(&inner); + } else { + warn!( + inner.borrow().log, + "XWayland crashed less than 5 seconds after its startup, not restarting." + ); + } +} + +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 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 + info!(inner.log, "XWayland is ready, signaling the WM."); + wm.xwayland_ready( + instance.wm_fd.take().unwrap(), // This is a bug if None + instance.wayland_client.clone(), + ); + + // setup the environemnt + ::std::env::set_var("DISPLAY", format!(":{}", instance.display_lock.display())); + } else { + error!( + inner.log, + "XWayland crashed at startup, will not try to restart it." + ); + } + + // 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: UnixStream, + wm_socket: UnixStream, + listen_sockets: &[UnixStream], +) -> 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.as_raw_fd())).unwrap(), + ]; + for socket in listen_sockets { + args.push(CString::new("-listen").unwrap()); + args.push(CString::new(format!("{}", socket.as_raw_fd())).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.as_raw_fd())); + + // 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: &F) -> NixResult<()> { + use nix::fcntl::{fcntl, FcntlArg, FdFlag}; + fcntl(fd.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::empty()))?; + Ok(()) +}