254 lines
10 KiB
Rust
254 lines
10 KiB
Rust
use std::io::{Read, Write};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::os::unix::process::ExitStatusExt;
|
|
use std::process::{Command, Stdio};
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use calloop::EventLoop;
|
|
use calloop::signals::{Signal, Signals};
|
|
use smithay::input::SeatHandler;
|
|
use smithay::input::pointer::{CursorImageStatus, MotionEvent};
|
|
use smithay::reexports::wayland_server::protocol::{wl_compositor::WlCompositor, wl_surface::WlSurface};
|
|
use smithay::reexports::wayland_server::{Display, Resource};
|
|
use smithay::utils::SERIAL_COUNTER;
|
|
|
|
use crate::{Ferese, RedrawRequest, after_dispatch, after_dispatch_with_redraw};
|
|
|
|
// Each state needs its own sockets and environment. A subprocess also bounds
|
|
// the cursor deadlock test without changing environment variables across tests.
|
|
pub(crate) fn private_runtime(test: &str) -> bool {
|
|
const CHILD: &str = "FERESE_STARTUP_TEST_CHILD";
|
|
if std::env::var_os(CHILD).is_some_and(|value| value == test) {
|
|
return true;
|
|
}
|
|
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let log_path = directory.path().join("test.log");
|
|
let log = std::fs::File::create(&log_path).unwrap();
|
|
let mut child = Command::new(std::env::current_exe().unwrap())
|
|
.args(["--exact", test, "--include-ignored", "--nocapture"])
|
|
.env(CHILD, test)
|
|
.env("XDG_RUNTIME_DIR", directory.path())
|
|
.env("XDG_CONFIG_HOME", directory.path())
|
|
.env("XDG_STATE_HOME", directory.path())
|
|
.env("XDG_CACHE_HOME", directory.path())
|
|
.env_remove("FERESE_SOCKET")
|
|
.stdout(Stdio::from(log.try_clone().unwrap()))
|
|
.stderr(Stdio::from(log))
|
|
.spawn()
|
|
.unwrap();
|
|
let deadline = Instant::now() + Duration::from_secs(20);
|
|
let status = loop {
|
|
if let Some(status) = child.try_wait().unwrap() {
|
|
break status;
|
|
}
|
|
|
|
if Instant::now() >= deadline {
|
|
child.kill().unwrap();
|
|
child.wait().unwrap();
|
|
panic!("{test} did not complete; possible startup deadlock");
|
|
}
|
|
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
};
|
|
|
|
assert!(status.success(), "{}", std::fs::read_to_string(log_path).unwrap());
|
|
false
|
|
}
|
|
|
|
pub(crate) fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese {
|
|
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
|
config.wallpaper.path = None;
|
|
|
|
Ferese::new(events, Display::new().unwrap(), config).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn launched_clients_unblock_termination_signals_and_keep_private_sockets() {
|
|
if !private_runtime("startup_tests::launched_clients_unblock_termination_signals_and_keep_private_sockets") {
|
|
return;
|
|
}
|
|
|
|
let mut events = EventLoop::try_new().unwrap();
|
|
let mut state = state(&mut events);
|
|
let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM, Signal::SIGUSR1]).unwrap();
|
|
let mut private = crate::private_client::ClientCapabilities::EFFECTS;
|
|
private.insert(crate::private_client::ClientCapabilities::SHELL_CONTROL);
|
|
for capabilities in [crate::private_client::ClientCapabilities::default(), private] {
|
|
let mut child = crate::process::spawn_client(&mut state, ["/bin/sleep", "30"], capabilities).unwrap();
|
|
let status = std::fs::read_to_string(format!("/proc/{}/status", child.id())).unwrap();
|
|
let blocked = status.lines().find(|line| line.starts_with("SigBlk:")).unwrap();
|
|
let blocked = u64::from_str_radix(blocked.split_whitespace().nth(1).unwrap(), 16).unwrap();
|
|
assert_eq!(blocked & ((1 << (libc::SIGINT - 1)) | (1 << (libc::SIGTERM - 1))), 0);
|
|
assert_ne!(blocked & (1 << (libc::SIGUSR1 - 1)), 0, "unrelated mask was lost");
|
|
if !capabilities.is_empty() {
|
|
let environment = std::fs::read(format!("/proc/{}/environ", child.id())).unwrap();
|
|
for name in ["WAYLAND_SOCKET=", "FERESE_SHELL_CONTROL_SOCKET="] {
|
|
let variable = environment
|
|
.split(|byte| *byte == 0)
|
|
.filter_map(|value| std::str::from_utf8(value).ok())
|
|
.find_map(|value| value.strip_prefix(name))
|
|
.unwrap();
|
|
let fd = std::fs::read_link(format!("/proc/{}/fd/{variable}", child.id())).unwrap();
|
|
assert!(fd.to_string_lossy().starts_with("socket:["));
|
|
}
|
|
}
|
|
|
|
crate::process::terminate_child(&mut child);
|
|
assert_eq!(
|
|
child.wait().unwrap().signal(),
|
|
Some(libc::SIGTERM),
|
|
"child needed SIGKILL"
|
|
);
|
|
}
|
|
|
|
// Child setup must not unblock the compositor's own signalfd mask.
|
|
let mut mask = unsafe { std::mem::zeroed() };
|
|
assert_eq!(
|
|
unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, std::ptr::null(), &mut mask) },
|
|
0
|
|
);
|
|
assert_eq!(unsafe { libc::sigismember(&mask, libc::SIGINT) }, 1);
|
|
assert_eq!(unsafe { libc::sigismember(&mask, libc::SIGTERM) }, 1);
|
|
drop(signals);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_client_exit_is_reaped_without_stopping_the_compositor() {
|
|
if !private_runtime("startup_tests::explicit_client_exit_is_reaped_without_stopping_the_compositor") {
|
|
return;
|
|
}
|
|
|
|
let mut events = EventLoop::try_new().unwrap();
|
|
let mut state = state(&mut events);
|
|
let child = crate::process::spawn_client(
|
|
&mut state,
|
|
["/bin/sh", "-c", "exit 7"],
|
|
crate::private_client::ClientCapabilities::default(),
|
|
)
|
|
.unwrap();
|
|
let pid = child.id();
|
|
let watched = crate::process::watch_client_exit(&events.handle(), child);
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
while watched.borrow().is_some() {
|
|
assert!(Instant::now() < deadline, "client was not reaped");
|
|
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
|
|
after_dispatch(&mut state);
|
|
}
|
|
|
|
let mut status = 0;
|
|
assert_eq!(unsafe { libc::waitpid(pid as i32, &mut status, libc::WNOHANG) }, -1);
|
|
assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD));
|
|
// Exit watching removes itself; subsequent event-loop dispatch remains usable.
|
|
events.dispatch(Duration::ZERO, &mut state).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn check_config_rejects_an_otherwise_valid_oversized_file() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let path = directory.path().join("config.kdl");
|
|
let valid = format!("//{}\n", "x".repeat(60 * 1024 - 3));
|
|
std::fs::write(&path, &valid).unwrap();
|
|
crate::check_config(&path).unwrap();
|
|
assert!(crate::theme::prepare(&(valid.clone() + "\n"), directory.path()).is_ok());
|
|
std::fs::write(&path, valid + "\n").unwrap();
|
|
let error = crate::check_config(&path).unwrap_err();
|
|
assert!(error.contains("exceeds 60 KiB"), "{error}");
|
|
assert_eq!(crate::theme::read_source(&path).unwrap_err(), error);
|
|
}
|
|
|
|
#[test]
|
|
fn deferred_cursor_redraw_runs_after_input_lock_is_released() {
|
|
if !private_runtime("startup_tests::deferred_cursor_redraw_runs_after_input_lock_is_released") {
|
|
return;
|
|
}
|
|
|
|
let mut events = EventLoop::try_new().unwrap();
|
|
let mut state = state(&mut events);
|
|
let (server, mut client_wire) = UnixStream::pair().unwrap();
|
|
let client = state
|
|
.display_handle
|
|
.insert_client(server, Arc::new(crate::state::ClientState::default()))
|
|
.unwrap();
|
|
let compositor = client
|
|
.create_resource::<WlCompositor, (), Ferese>(&state.display_handle, 6, ())
|
|
.unwrap();
|
|
// wl_compositor.create_surface(new_id=2).
|
|
for word in [compositor.id().protocol_id(), 12u32 << 16, 2] {
|
|
client_wire.write_all(&word.to_ne_bytes()).unwrap();
|
|
}
|
|
|
|
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
|
|
let surface = client
|
|
.object_from_protocol_id::<WlSurface>(&state.display_handle, 2)
|
|
.unwrap();
|
|
let pointer = state.seat.get_pointer().unwrap();
|
|
let seat = state.seat.clone();
|
|
state.cursor_image(&seat, CursorImageStatus::Hidden);
|
|
after_dispatch_with_redraw(&mut state, |_, _| {});
|
|
events.handle().insert_idle(move |state| {
|
|
for _ in 0..2 {
|
|
let event = MotionEvent {
|
|
location: (10.0, 20.0).into(),
|
|
serial: SERIAL_COUNTER.next_serial(),
|
|
time: 0,
|
|
};
|
|
pointer.motion(state, Some((surface.clone(), (0.0, 0.0).into())), &event);
|
|
// Smithay invokes Ferese::cursor_image while holding its real pointer lock.
|
|
pointer.motion(state, None, &event);
|
|
assert!(state.cursor_redraw_pending);
|
|
}
|
|
});
|
|
let signal = events.get_signal();
|
|
let mut redraws = 0;
|
|
events
|
|
.run(Some(Duration::ZERO), &mut state, |state| {
|
|
after_dispatch_with_redraw(state, |state, request| {
|
|
assert!(matches!(request, RedrawRequest::Cursor));
|
|
assert_eq!(
|
|
state.seat.get_pointer().unwrap().current_location(),
|
|
(10.0, 20.0).into()
|
|
);
|
|
redraws += 1;
|
|
});
|
|
assert!(!state.cursor_redraw_pending);
|
|
signal.stop();
|
|
})
|
|
.unwrap();
|
|
assert_eq!(redraws, 1);
|
|
after_dispatch_with_redraw(&mut state, |_, _| panic!("redraw was not consumed"));
|
|
}
|
|
|
|
#[test]
|
|
fn wayland_roundtrip_completes_without_a_rendered_frame() {
|
|
if !private_runtime("startup_tests::wayland_roundtrip_completes_without_a_rendered_frame") {
|
|
return;
|
|
}
|
|
|
|
let mut events = EventLoop::try_new().unwrap();
|
|
let mut state = state(&mut events);
|
|
let (server, mut client) = UnixStream::pair().unwrap();
|
|
state
|
|
.display_handle
|
|
.insert_client(server, Arc::new(crate::state::ClientState::default()))
|
|
.unwrap();
|
|
// wl_display.sync(new_id=2), using native-endian Wayland wire format.
|
|
for word in [1u32, 12u32 << 16, 2u32] {
|
|
client.write_all(&word.to_ne_bytes()).unwrap();
|
|
}
|
|
|
|
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
|
|
client.set_read_timeout(Some(Duration::from_millis(20))).unwrap();
|
|
let mut reply = [0u8; 12];
|
|
assert!(
|
|
client.read_exact(&mut reply).is_err(),
|
|
"dispatch flushed replies before after_dispatch"
|
|
);
|
|
after_dispatch(&mut state);
|
|
client.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
|
|
client.read_exact(&mut reply).unwrap();
|
|
assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2);
|
|
assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0);
|
|
}
|