Format Rust sources at 100 columns

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 4661f595f8
commit 610478f8c7
336 files changed
+23497 -7892

No files matched your search

+27 -7
View File
@@ -17,7 +17,13 @@ fn random_cookie() -> io::Result<[u8; 16]> {
let mut cookie = [0_u8; 16];
let mut filled = 0;
while filled < cookie.len() {
let count = unsafe { libc::getrandom(cookie[filled..].as_mut_ptr().cast(), cookie.len() - filled, 0) };
let count = unsafe {
libc::getrandom(
cookie[filled..].as_mut_ptr().cast(),
cookie.len() - filled,
0,
)
};
if count < 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
@@ -26,7 +32,10 @@ fn random_cookie() -> io::Result<[u8; 16]> {
return Err(error);
}
if count == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "getrandom returned zero"));
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"getrandom returned zero",
));
}
filled += count as usize;
}
@@ -72,7 +81,8 @@ impl AuthorityFile {
let mut file = tempfile::Builder::new()
.prefix("ferese-xauth-")
.tempfile_in(runtime_directory)?;
file.as_file().set_permissions(fs::Permissions::from_mode(0o600))?;
file.as_file()
.set_permissions(fs::Permissions::from_mode(0o600))?;
let cookie = random_cookie()?;
let host = hostname()?;
@@ -159,7 +169,10 @@ mod tests {
let file = AuthorityFile::create(&runtime, 3).unwrap();
let path = file.path().to_owned();
let mode = fs::metadata(&path).unwrap().mode() & 0o777;
assert_eq!(mode, 0o600, "authority file must not be group/world readable");
assert_eq!(
mode, 0o600,
"authority file must not be group/world readable"
);
path
};
@@ -168,7 +181,8 @@ mod tests {
#[test]
fn a_group_readable_runtime_directory_is_rejected() {
let runtime = std::env::temp_dir().join(format!("ferese-xauth-loose-{}", std::process::id()));
let runtime =
std::env::temp_dir().join(format!("ferese-xauth-loose-{}", std::process::id()));
let _ = fs::remove_dir_all(&runtime);
fs::create_dir_all(&runtime).unwrap();
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o755)).unwrap();
@@ -204,11 +218,17 @@ mod tests {
.lines()
.find(|line| line.contains("MIT-MAGIC-COOKIE-1"))
.unwrap_or_else(|| panic!("xauth did not list the cookie record: {listing:?}"));
assert!(entry.contains(":91"), "entry must reference the display: {entry:?}");
assert!(
entry.contains(":91"),
"entry must reference the display: {entry:?}"
);
let cookie_hex = entry.split_whitespace().last().expect("cookie field");
assert_eq!(cookie_hex.len(), 32, "16-byte cookie as hex: {cookie_hex}");
assert!(cookie_hex.chars().any(|c| c != '0'), "cookie must not be zero");
assert!(
cookie_hex.chars().any(|c| c != '0'),
"cookie must not be zero"
);
}
#[test]
+46 -10
View File
@@ -60,7 +60,14 @@ pub(crate) fn spawn_satellite(
));
}
satellite_command(executable, environment, reservation, authority, notify_socket).spawn()
satellite_command(
executable,
environment,
reservation,
authority,
notify_socket,
)
.spawn()
}
fn satellite_command(
@@ -70,7 +77,11 @@ fn satellite_command(
authority: &Path,
notify_socket: Option<&Path>,
) -> std::process::Command {
let inherited: Vec<_> = reservation.listeners().iter().map(AsRawFd::as_raw_fd).collect();
let inherited: Vec<_> = reservation
.listeners()
.iter()
.map(AsRawFd::as_raw_fd)
.collect();
if inherited.is_empty() {
panic!("cannot build a Satellite command without reserved listening sockets");
}
@@ -260,7 +271,10 @@ impl OwnedProcessGroup {
self.reaped
}
pub fn terminate(&mut self, grace: std::time::Duration) -> Result<Option<std::process::ExitStatus>, String> {
pub fn terminate(
&mut self,
grace: std::time::Duration,
) -> Result<Option<std::process::ExitStatus>, String> {
self.signal_group(libc::SIGTERM);
let deadline = std::time::Instant::now() + grace;
@@ -371,7 +385,11 @@ mod tests {
.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("without reserved listening sockets"));
assert!(
error
.to_string()
.contains("without reserved listening sockets")
);
}
#[test]
@@ -397,10 +415,15 @@ mod tests {
assert!(!process.group_is_empty());
process.signal_group(libc::SIGTERM);
let status = process.terminate(Duration::from_secs(5)).expect("terminate");
let status = process
.terminate(Duration::from_secs(5))
.expect("terminate");
assert!(status.is_some());
assert!(process.group_is_empty(), "a reaped group has no members left");
assert!(
process.group_is_empty(),
"a reaped group has no members left"
);
assert_eq!(process.cleanup_error(), None);
}
@@ -423,7 +446,9 @@ mod tests {
"the surviving descendant keeps the group non-empty"
);
process.terminate(Duration::from_secs(5)).expect("terminate");
process
.terminate(Duration::from_secs(5))
.expect("terminate");
assert!(
process.group_is_empty(),
@@ -463,7 +488,10 @@ mod tests {
.terminate(Duration::from_secs(5))
.expect("terminating a reaped owner must not fail");
assert_eq!(status, None, "there is no leader left to report a status for");
assert_eq!(
status, None,
"there is no leader left to report a status for"
);
assert_eq!(
test_hook::group_signals(),
before,
@@ -537,9 +565,17 @@ mod tests {
let key = key.to_string_lossy();
if matches!(
key.as_ref(),
"LISTEN_PID" | "LISTEN_FDS" | "LISTEN_FDNAMES" | "WATCHDOG_PID" | "WATCHDOG_USEC" | "NOTIFY_SOCKET"
"LISTEN_PID"
| "LISTEN_FDS"
| "LISTEN_FDNAMES"
| "WATCHDOG_PID"
| "WATCHDOG_USEC"
| "NOTIFY_SOCKET"
) {
assert_eq!(value, None, "{key} must not be inherited from the launching service");
assert_eq!(
value, None,
"{key} must not be inherited from the launching service"
);
}
if key.as_ref() == "WAYLAND_SOCKET" {
assert_eq!(
+287 -105
View File
@@ -46,18 +46,22 @@ impl Fixture {
.expect("make the runtime directory private");
if production_inputs {
crate::xwayland::test_hooks::allocator_root_in(PathBuf::from("/tmp/.X11-unix"), PathBuf::from("/tmp"));
crate::xwayland::test_hooks::allocator_root_in(
PathBuf::from("/tmp/.X11-unix"),
PathBuf::from("/tmp"),
);
} else {
// Owned inputs, so this fixture never allocates a real slot in `/tmp/.X11-unix`.
let allocator = directory.join("x11");
fs::create_dir_all(&allocator).expect("create the allocator directory");
fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700)).expect("make the allocator private");
fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700))
.expect("make the allocator private");
crate::xwayland::test_hooks::allocator_root(allocator);
}
let reservation = Reservation::allocate().expect("reserve a display");
let authority =
AuthorityFile::create(&runtime_directory, reservation.display_number()).expect("create an authority file");
let authority = AuthorityFile::create(&runtime_directory, reservation.display_number())
.expect("create an authority file");
Self {
record: directory.join("starts.jsonl"),
@@ -70,8 +74,13 @@ impl Fixture {
}
fn script(&self, assignments: &[(&str, &str)]) -> &Self {
let fake = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/tests/fake-xwayland-satellite.py");
assert!(fake.exists(), "the fake Satellite must exist at {}", fake.display());
let fake = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../scripts/tests/fake-xwayland-satellite.py");
assert!(
fake.exists(),
"the fake Satellite must exist at {}",
fake.display()
);
let record = self.record.display();
let fake = fake.display();
@@ -84,7 +93,8 @@ impl Fixture {
}
body.push_str(&format!("exec '{fake}' \"$@\"\n"));
fs::write(&self.wrapper, body).expect("write the wrapper script");
fs::set_permissions(&self.wrapper, fs::Permissions::from_mode(0o755)).expect("make the wrapper executable");
fs::set_permissions(&self.wrapper, fs::Permissions::from_mode(0o755))
.expect("make the wrapper executable");
self
}
@@ -113,7 +123,8 @@ impl Fixture {
}
fn spawn(&self, generation: u64) -> (Child, ReadinessSocket) {
let socket = ReadinessSocket::create(&self.runtime_directory, generation).expect("create a readiness socket");
let socket = ReadinessSocket::create(&self.runtime_directory, generation)
.expect("create a readiness socket");
let notify = socket.path().to_owned();
let spawned = child::spawn_satellite(
&self.wrapper,
@@ -127,11 +138,13 @@ impl Fixture {
}
fn spawn_real(&self, generation: u64, executable: &Path) -> (Child, ReadinessSocket) {
let socket = ReadinessSocket::create(&self.runtime_directory, generation).expect("create a readiness socket");
let socket = ReadinessSocket::create(&self.runtime_directory, generation)
.expect("create a readiness socket");
let notify = socket.path().to_owned();
let mut environment = self.environment();
environment.wayland_display = std::env::var_os("WAYLAND_DISPLAY").expect("a live Wayland session is required");
environment.wayland_display =
std::env::var_os("WAYLAND_DISPLAY").expect("a live Wayland session is required");
let spawned = child::spawn_satellite(
executable,
&environment,
@@ -167,7 +180,8 @@ fn run_isolated(test_path: &str) -> bool {
));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the isolated session directory");
fs::set_permissions(&directory, fs::Permissions::from_mode(0o700)).expect("make the session private");
fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))
.expect("make the session private");
let status = Command::new(std::env::current_exe().expect("the test binary"))
.args(["--exact", test_path, "--nocapture"])
@@ -177,7 +191,11 @@ fn run_isolated(test_path: &str) -> bool {
.env_remove("FERESE_SOCKET")
.output()
.expect("re-exec the test");
assert!(status.status.success(), "{}", String::from_utf8_lossy(&status.stderr));
assert!(
status.status.success(),
"{}",
String::from_utf8_lossy(&status.stderr)
);
false
}
@@ -239,7 +257,10 @@ fn satellite_receives_exactly_the_owned_listeners_and_no_extra_descriptors() {
let record = fixture.starts().remove(0);
assert_eq!(record["display"], fixture.reservation.display_name());
assert_eq!(record["authority"], fixture.authority.path().to_str().unwrap());
assert_eq!(
record["authority"],
fixture.authority.path().to_str().unwrap()
);
let listenfds = record["listenfds"].as_array().expect("listenfds array");
assert_eq!(listenfds.len(), fixture.reservation.listeners().len());
@@ -304,11 +325,17 @@ fn satellite_gets_the_session_environment_and_never_a_private_socket() {
let environment = &record["env"];
assert_eq!(environment["DISPLAY"], fixture.reservation.display_name());
assert_eq!(environment["XAUTHORITY"], fixture.authority.path().to_str().unwrap());
assert_eq!(
environment["XAUTHORITY"],
fixture.authority.path().to_str().unwrap()
);
assert_eq!(environment["WAYLAND_DISPLAY"], "wayland-it");
assert_eq!(environment["WAYLAND_SOCKET"], serde_json::Value::Null);
assert_eq!(environment["FERESE_SHELL_CONTROL_SOCKET"], serde_json::Value::Null);
assert_eq!(
environment["FERESE_SHELL_CONTROL_SOCKET"],
serde_json::Value::Null
);
reap(satellite);
}
@@ -329,10 +356,16 @@ fn a_valid_notification_from_the_service_pid_satisfies_readiness() {
#[test]
fn a_delayed_notification_is_still_accepted() {
let fixture = Fixture::new("ready-delay");
fixture.script(&[("FAKE_SATELLITE_READY", "1"), ("FAKE_SATELLITE_READY_DELAY", "0.3")]);
fixture.script(&[
("FAKE_SATELLITE_READY", "1"),
("FAKE_SATELLITE_READY_DELAY", "0.3"),
]);
let (satellite, socket) = fixture.spawn(1);
let pid = satellite.id();
assert!(read_ready(&socket, pid), "a delayed READY=1 must be accepted");
assert!(
read_ready(&socket, pid),
"a delayed READY=1 must be accepted"
);
reap(satellite);
}
@@ -343,7 +376,10 @@ fn a_notification_from_a_child_pid_is_rejected() {
let (satellite, socket) = fixture.spawn(1);
let pid = satellite.id();
assert!(!read_ready(&socket, pid), "a foreign PID's READY=1 must be rejected");
assert!(
!read_ready(&socket, pid),
"a foreign PID's READY=1 must be rejected"
);
reap(satellite);
}
@@ -354,7 +390,10 @@ fn malformed_and_truncated_notifications_are_rejected() {
fixture.script(&[("FAKE_SATELLITE_READY", payload)]);
let (satellite, socket) = fixture.spawn(1);
let pid = satellite.id();
assert!(!read_ready(&socket, pid), "{payload} must not satisfy readiness");
assert!(
!read_ready(&socket, pid),
"{payload} must not satisfy readiness"
);
reap(satellite);
}
}
@@ -368,7 +407,10 @@ fn a_service_that_exits_before_readiness_leaves_no_running_process() {
]);
let (mut satellite, socket) = fixture.spawn(1);
let pid = satellite.id();
assert!(!read_ready(&socket, pid), "an early exit must not satisfy readiness");
assert!(
!read_ready(&socket, pid),
"an early exit must not satisfy readiness"
);
let status = wait_until_child(&mut satellite);
assert_eq!(status.code(), Some(17));
assert!(
@@ -404,7 +446,10 @@ fn shutdown_cleans_the_whole_owned_process_group() {
]);
let (satellite, socket) = fixture.spawn(1);
let pid = satellite.id();
assert!(read_ready(&socket, pid), "the group fixture must reach readiness first");
assert!(
read_ready(&socket, pid),
"the group fixture must reach readiness first"
);
let group_children = process::group_members(pid);
assert!(
@@ -414,10 +459,15 @@ fn shutdown_cleans_the_whole_owned_process_group() {
// Ownership, not liveness: the group must hold the endpoints it was handed.
let inodes = listener_inodes(&fixture.reservation);
assert!(!inodes.is_empty(), "the reserved listeners must be identifiable");
assert!(
!inodes.is_empty(),
"the reserved listeners must be identifiable"
);
let holders_before = process::socket_holders(&inodes);
assert!(
holders_before.iter().any(|holder| *holder != std::process::id()),
holders_before
.iter()
.any(|holder| *holder != std::process::id()),
"a service descendant must hold the X11 listeners before shutdown: {holders_before:?}"
);
@@ -447,7 +497,8 @@ fn listener_inodes(reservation: &Reservation) -> Vec<String> {
.listeners()
.iter()
.filter_map(|listener| {
let target = std::fs::read_link(format!("/proc/self/fd/{}", listener.as_raw_fd())).ok()?;
let target =
std::fs::read_link(format!("/proc/self/fd/{}", listener.as_raw_fd())).ok()?;
let text = target.to_string_lossy().into_owned();
text.strip_prefix("socket:[")
.and_then(|rest| rest.strip_suffix(']'))
@@ -478,7 +529,10 @@ mod process {
continue;
};
let text = target.to_string_lossy();
let Some(number) = text.strip_prefix("socket:[").and_then(|rest| rest.strip_suffix(']')) else {
let Some(number) = text
.strip_prefix("socket:[")
.and_then(|rest| rest.strip_suffix(']'))
else {
continue;
};
if inodes.iter().any(|inode| inode == number) {
@@ -541,7 +595,10 @@ fn real_satellite_reports_verified_readiness_and_serves_authorized_x_clients() {
}
}
let elapsed = started.elapsed();
assert!(accepted, "the installed Satellite never sent a usable READY=1");
assert!(
accepted,
"the installed Satellite never sent a usable READY=1"
);
println!("real Satellite reported readiness in {elapsed:?} (production budget 10s)");
assert!(
elapsed <= Duration::from_secs(10),
@@ -578,7 +635,10 @@ fn real_satellite_reports_verified_readiness_and_serves_authorized_x_clients() {
group.group_is_empty(),
"the real service group must be empty after shutdown"
);
assert!(wait_until(|| !process_exists(pid)), "the real Satellite must be gone");
assert!(
wait_until(|| !process_exists(pid)),
"the real Satellite must be gone"
);
}
/// Everything the live smoke test cannot supply for itself, declared up front.
@@ -598,17 +658,22 @@ fn live_dependencies() -> PathBuf {
}
let on_path = |program: &str| {
std::env::var_os("PATH")
.is_some_and(|paths| std::env::split_paths(&paths).any(|directory| directory.join(program).is_file()))
std::env::var_os("PATH").is_some_and(|paths| {
std::env::split_paths(&paths).any(|directory| directory.join(program).is_file())
})
};
if !on_path("xdpyinfo") {
panic!("dependency missing: xdpyinfo (x11-utils) is required to prove an X client can connect");
panic!(
"dependency missing: xdpyinfo (x11-utils) is required to prove an X client can connect"
);
}
match fs::symlink_metadata("/tmp/.X11-unix") {
Ok(metadata) if metadata.is_dir() => {}
Ok(_) => panic!("dependency missing: /tmp/.X11-unix exists but is not a directory"),
Err(error) => panic!("dependency missing: the shared X11 socket directory /tmp/.X11-unix: {error}"),
Err(error) => {
panic!("dependency missing: the shared X11 socket directory /tmp/.X11-unix: {error}")
}
}
executable
@@ -626,7 +691,8 @@ mod manager_loop {
use smithay::reexports::wayland_server::Display;
fn wrapper(directory: &Path, assignments: &[(&str, &str)]) -> PathBuf {
let fake = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/tests/fake-xwayland-satellite.py");
let fake = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../scripts/tests/fake-xwayland-satellite.py");
let script = directory.join("fake-satellite");
let mut body = String::from("#!/bin/sh\n");
for (key, value) in assignments {
@@ -634,7 +700,8 @@ mod manager_loop {
}
body.push_str(&format!("exec '{}' \"$@\"\n", fake.display()));
fs::write(&script, body).expect("write the wrapper");
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).expect("make the wrapper executable");
fs::set_permissions(&script, fs::Permissions::from_mode(0o755))
.expect("make the wrapper executable");
script
}
@@ -646,11 +713,13 @@ mod manager_loop {
/// A `Ferese` wired to a real event loop, ready to run the manager.
fn session(directory: &Path, path: PathBuf) -> (EventLoop<'static, Ferese>, Ferese) {
let (_, runtime, _) = crate::config::prepare("xwayland { }", directory).expect("prepare configuration");
let (_, runtime, _) =
crate::config::prepare("xwayland { }", directory).expect("prepare configuration");
// Owned inputs, so this test never competes for a real display slot.
let allocator = directory.join("x11");
fs::create_dir_all(&allocator).expect("create the allocator directory");
fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700)).expect("make the allocator private");
fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700))
.expect("make the allocator private");
test_hooks::allocator_root(allocator);
let mut event_loop = EventLoop::try_new().expect("create the event loop");
let display = Display::new().expect("create the Wayland display");
@@ -690,14 +759,19 @@ mod manager_loop {
) {
return;
}
let directory = std::env::temp_dir().join(format!("ferese-manager-run-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-run-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
let (mut event_loop, mut state) = session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]));
let (mut event_loop, mut state) = session(
&directory,
wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]),
);
xwayland::initialize(&mut state);
let generation = xwayland::request_start(&mut state).expect("an on-demand service accepts a start request");
let generation = xwayland::request_start(&mut state)
.expect("an on-demand service accepts a start request");
assert!(generation > 0);
dispatch_until(&mut event_loop, &mut state, |state| {
@@ -711,12 +785,20 @@ mod manager_loop {
"readiness is earned, not assumed"
);
assert_eq!(status.state, State::Running);
assert!(!status.restart_required, "the live service matches the configured one");
let pid = status.satellite_pid.expect("a running service reports its pid");
assert!(
!status.restart_required,
"the live service matches the configured one"
);
let pid = status
.satellite_pid
.expect("a running service reports its pid");
assert!(process_exists(pid), "the service must be alive");
xwayland::shutdown_after_loop(&mut state);
assert!(!process_exists(pid), "shutdown must stop and reap the service");
assert!(
!process_exists(pid),
"shutdown must stop and reap the service"
);
let _ = fs::remove_dir_all(&directory);
}
@@ -727,7 +809,8 @@ mod manager_loop {
) {
return;
}
let directory = std::env::temp_dir().join(format!("ferese-manager-crash-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-crash-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
@@ -735,7 +818,10 @@ mod manager_loop {
&directory,
wrapper(
&directory,
&[("FAKE_SATELLITE_EXIT_AFTER_READY", "1"), ("FAKE_SATELLITE_LINGER", "0")],
&[
("FAKE_SATELLITE_EXIT_AFTER_READY", "1"),
("FAKE_SATELLITE_LINGER", "0"),
],
),
);
xwayland::initialize(&mut state);
@@ -768,7 +854,10 @@ mod manager_loop {
let status = xwayland::status_snapshot(&state);
assert!(
matches!(status.state, State::Backoff | State::Stopped | State::Failed),
matches!(
status.state,
State::Backoff | State::Stopped | State::Failed
),
"a crash must land in a stopped phase, not {:?}",
status.state
);
@@ -796,7 +885,8 @@ mod manager_loop {
test_hooks::reset();
test_hooks::STARTUP_DEADLINE_MS.store(600, std::sync::atomic::Ordering::SeqCst);
let directory = std::env::temp_dir().join(format!("ferese-manager-timeout-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-timeout-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
let record = directory.join("starts.jsonl");
@@ -816,15 +906,16 @@ mod manager_loop {
),
);
xwayland::initialize(&mut state);
let first_generation =
xwayland::request_start(&mut state).expect("an on-demand service accepts a start request");
let first_generation = xwayland::request_start(&mut state)
.expect("an on-demand service accepts a start request");
let display = xwayland::status_snapshot(&state)
.display
.expect("a display is reserved");
let socket_path = x11_socket_path(&directory, &display);
// The kernel queues the connection, so a re-enabled listener could claim it.
std::os::unix::net::UnixStream::connect(&socket_path).expect("the reserved socket accepts a queued connection");
std::os::unix::net::UnixStream::connect(&socket_path)
.expect("the reserved socket accepts a queued connection");
let mut first_pid = None;
let mut observed_pids = Vec::new();
@@ -856,7 +947,11 @@ mod manager_loop {
}
let status = xwayland::status_snapshot(&state);
assert_eq!(status.state, State::Failed, "three silent starts must open the circuit");
assert_eq!(
status.state,
State::Failed,
"three silent starts must open the circuit"
);
assert!(
status
.last_error
@@ -871,7 +966,9 @@ mod manager_loop {
"the timed-out service leader must be reaped, pid {first_pid} still exists"
);
assert!(
observed_pids.iter().all(|pid| wait_until(|| !process_exists(*pid))),
observed_pids
.iter()
.all(|pid| wait_until(|| !process_exists(*pid))),
"every generation started by the queued connection must be reaped"
);
@@ -879,7 +976,10 @@ mod manager_loop {
.expect("the fake recorded its start")
.lines()
.next()
.map(|line| serde_json::from_str::<serde_json::Value>(line).expect("valid JSON")["holder_pid"].clone())
.map(|line| {
serde_json::from_str::<serde_json::Value>(line).expect("valid JSON")["holder_pid"]
.clone()
})
.and_then(|value| value.as_u64())
.map(|pid| pid as u32)
.expect("the fake reports the descriptor holder");
@@ -915,7 +1015,8 @@ mod manager_loop {
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-cleanup-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-cleanup-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
let record = directory.join("starts.jsonl");
@@ -960,7 +1061,9 @@ mod manager_loop {
let parked = xwayland::status_snapshot(&state);
let retired_pid = retired_pid.expect("a service was spawned");
let generation = parked.generation.expect("the parked generation is reported");
let generation = parked
.generation
.expect("the parked generation is reported");
let display = parked.display.clone().expect("the display stays reserved");
assert_eq!(
parked.satellite_pid,
@@ -968,7 +1071,10 @@ mod manager_loop {
"the cleanup record must stay owned while its group may still exist"
);
{
let manager = state.xwayland.as_ref().expect("the manager stays published");
let manager = state
.xwayland
.as_ref()
.expect("the manager stays published");
assert!(
matches!(manager.lifecycle.phase, Phase::CleanupFailed { .. }),
"the expired deadline must park the stop, found {:?}",
@@ -984,7 +1090,8 @@ mod manager_loop {
xwayland::request_start(&mut state).is_none(),
"a parked cleanup must refuse a replacement generation"
);
let error = xwayland::retry(&mut state).expect_err("a retry must be refused while cleanup is unconfirmed");
let error = xwayland::retry(&mut state)
.expect_err("a retry must be refused while cleanup is unconfirmed");
assert!(
error.contains("still holds the X11 display"),
"the refusal must name what is still holding the display: {error}"
@@ -1019,7 +1126,8 @@ mod manager_loop {
xwayland::retry(&mut state).expect("a confirmed cleanup makes a retry legal again");
dispatch_until(&mut event_loop, &mut state, |state| {
let status = xwayland::status_snapshot(state);
status.state == State::Running && status.generation.is_some_and(|value| value > generation)
status.state == State::Running
&& status.generation.is_some_and(|value| value > generation)
});
let recovered = xwayland::status_snapshot(&state);
@@ -1052,7 +1160,10 @@ mod manager_loop {
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-cleanup-stop-{}", std::process::id()));
let directory = std::env::temp_dir().join(format!(
"ferese-manager-cleanup-stop-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
@@ -1060,7 +1171,10 @@ mod manager_loop {
&directory,
wrapper(
&directory,
&[("FAKE_SATELLITE_EXIT_AFTER_READY", "1"), ("FAKE_SATELLITE_LINGER", "0")],
&[
("FAKE_SATELLITE_EXIT_AFTER_READY", "1"),
("FAKE_SATELLITE_LINGER", "0"),
],
),
);
xwayland::initialize(&mut state);
@@ -1091,7 +1205,10 @@ mod manager_loop {
.display
.expect("the display stays reserved");
{
let manager = state.xwayland.as_ref().expect("the manager stays published");
let manager = state
.xwayland
.as_ref()
.expect("the manager stays published");
assert!(
matches!(manager.lifecycle.phase, Phase::CleanupFailed { .. }),
"the stop must be parked as unconfirmed, found {:?}",
@@ -1128,15 +1245,15 @@ mod manager_loop {
#[test]
fn an_unavailable_pidfd_falls_back_to_the_bounded_exit_poll() {
const PATH: &str =
"xwayland::integration_tests::manager_loop::an_unavailable_pidfd_falls_back_to_the_bounded_exit_poll";
const PATH: &str = "xwayland::integration_tests::manager_loop::an_unavailable_pidfd_falls_back_to_the_bounded_exit_poll";
if !run_isolated(PATH) {
return;
}
test_hooks::reset();
test_hooks::PIDFD_UNAVAILABLE.store(true, std::sync::atomic::Ordering::SeqCst);
let directory = std::env::temp_dir().join(format!("ferese-manager-poll-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-poll-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
@@ -1181,23 +1298,29 @@ mod manager_loop {
wait_until(|| !process_exists(pid)),
"the observed service must be reaped without a pidfd"
);
assert!(verified_readiness, "the service reported readiness before it exited");
assert!(
verified_readiness,
"the service reported readiness before it exited"
);
let status = xwayland::status_snapshot(&state);
assert_eq!(status.satellite_pid, None, "cleanup released the process owner");
assert_eq!(
status.satellite_pid, None,
"cleanup released the process owner"
);
test_hooks::reset();
let _ = fs::remove_dir_all(&directory);
}
#[test]
fn a_stale_exit_callback_cannot_stop_a_replacement_service() {
const PATH: &str =
"xwayland::integration_tests::manager_loop::a_stale_exit_callback_cannot_stop_a_replacement_service";
const PATH: &str = "xwayland::integration_tests::manager_loop::a_stale_exit_callback_cannot_stop_a_replacement_service";
if !run_isolated(PATH) {
return;
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-stale-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-stale-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
@@ -1206,8 +1329,8 @@ mod manager_loop {
wrapper(&directory, &[("FAKE_SATELLITE_EXIT_BEFORE_READY", "1")]),
);
xwayland::initialize(&mut state);
let first_generation =
xwayland::request_start(&mut state).expect("an on-demand service accepts a start request");
let first_generation = xwayland::request_start(&mut state)
.expect("an on-demand service accepts a start request");
let mut retired_pid = None;
let deadline = Instant::now() + Duration::from_secs(30);
@@ -1222,7 +1345,10 @@ mod manager_loop {
if status.state == State::Failed {
break;
}
assert!(Instant::now() < deadline, "the early failure never settled: {status:?}");
assert!(
Instant::now() < deadline,
"the early failure never settled: {status:?}"
);
}
let retired_pid = retired_pid.expect("the first generation was spawned");
let retired_status = xwayland::status_snapshot(&state);
@@ -1238,8 +1364,13 @@ mod manager_loop {
});
let live = xwayland::status_snapshot(&state);
let live_pid = live.satellite_pid.expect("the replacement reports its pid");
let live_generation = live.generation.expect("the replacement reports its generation");
assert_ne!(live_pid, retired_pid, "the replacement must be a different process");
let live_generation = live
.generation
.expect("the replacement reports its generation");
assert_ne!(
live_pid, retired_pid,
"the replacement must be a different process"
);
let handle = state.loop_handle.clone();
let stale_generation = first_generation;
@@ -1259,7 +1390,11 @@ mod manager_loop {
State::Running,
"a stale callback must not move the live service"
);
assert_eq!(after.satellite_pid, Some(live_pid), "the live process must stay owned");
assert_eq!(
after.satellite_pid,
Some(live_pid),
"the live process must stay owned"
);
assert_eq!(
after.generation,
Some(live_generation),
@@ -1274,18 +1409,20 @@ mod manager_loop {
#[test]
fn shutdown_while_starting_stops_the_service_it_owns() {
const PATH: &str =
"xwayland::integration_tests::manager_loop::shutdown_while_starting_stops_the_service_it_owns";
const PATH: &str = "xwayland::integration_tests::manager_loop::shutdown_while_starting_stops_the_service_it_owns";
if !run_isolated(PATH) {
return;
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-shutdown-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-shutdown-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
let (mut event_loop, mut state) =
session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_READY_DELAY", "30")]));
let (mut event_loop, mut state) = session(
&directory,
wrapper(&directory, &[("FAKE_SATELLITE_READY_DELAY", "30")]),
);
xwayland::initialize(&mut state);
xwayland::request_start(&mut state).expect("an on-demand service accepts a start request");
@@ -1332,7 +1469,8 @@ mod manager_loop {
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-rebind-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-rebind-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
@@ -1341,8 +1479,8 @@ mod manager_loop {
wrapper(&directory, &[("FAKE_SATELLITE_EXIT_BEFORE_READY", "1")]),
);
xwayland::initialize(&mut state);
let first_generation =
xwayland::request_start(&mut state).expect("an on-demand service accepts a start request");
let first_generation = xwayland::request_start(&mut state)
.expect("an on-demand service accepts a start request");
let deadline = Instant::now() + Duration::from_secs(30);
loop {
@@ -1356,7 +1494,8 @@ mod manager_loop {
}
test_hooks::FAIL_REBIND_AFTER.store(0, std::sync::atomic::Ordering::SeqCst);
let error = xwayland::retry(&mut state).expect_err("an injected rebind failure must be reported");
let error =
xwayland::retry(&mut state).expect_err("an injected rebind failure must be reported");
assert!(
error.contains("rebind X11 display"),
"the reported error must name the rebind step: {error}"
@@ -1393,14 +1532,14 @@ mod manager_loop {
#[test]
fn a_listener_registration_failure_leaves_a_retryable_endpoint() {
const PATH: &str =
"xwayland::integration_tests::manager_loop::a_listener_registration_failure_leaves_a_retryable_endpoint";
const PATH: &str = "xwayland::integration_tests::manager_loop::a_listener_registration_failure_leaves_a_retryable_endpoint";
if !run_isolated(PATH) {
return;
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-register-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-register-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
@@ -1411,7 +1550,10 @@ mod manager_loop {
// Fail the second listener source: the first is already registered.
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(1, std::sync::atomic::Ordering::SeqCst);
xwayland::initialize(&mut state);
let diagnostic = state.x11_diagnostic.clone().expect("the failure must be visible");
let diagnostic = state
.x11_diagnostic
.clone()
.expect("the failure must be visible");
assert!(
diagnostic.contains("injected X11 listener registration failure"),
"the diagnostic must name the failure: {diagnostic}"
@@ -1421,7 +1563,8 @@ mod manager_loop {
"a half-registered endpoint must not be published"
);
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(usize::MAX, std::sync::atomic::Ordering::SeqCst);
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER
.store(usize::MAX, std::sync::atomic::Ordering::SeqCst);
xwayland::retry(&mut state).expect("the service becomes available once registration works");
let mut first_generation = None;
@@ -1437,7 +1580,10 @@ mod manager_loop {
if status.state == State::Failed {
break;
}
assert!(Instant::now() < deadline, "the service never became failed: {status:?}");
assert!(
Instant::now() < deadline,
"the service never became failed: {status:?}"
);
}
let first_generation = first_generation.expect("a generation was started");
@@ -1463,7 +1609,8 @@ mod manager_loop {
"the half-registered listeners must be closed"
);
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(usize::MAX, std::sync::atomic::Ordering::SeqCst);
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER
.store(usize::MAX, std::sync::atomic::Ordering::SeqCst);
wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]);
xwayland::retry(&mut state).expect("the endpoint recovers once registration works");
dispatch_until(&mut event_loop, &mut state, |state| {
@@ -1477,28 +1624,35 @@ mod manager_loop {
#[test]
fn an_eager_service_that_could_not_start_retries_exactly_once() {
const PATH: &str =
"xwayland::integration_tests::manager_loop::an_eager_service_that_could_not_start_retries_exactly_once";
const PATH: &str = "xwayland::integration_tests::manager_loop::an_eager_service_that_could_not_start_retries_exactly_once";
if !run_isolated(PATH) {
return;
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-eager-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-eager-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
let (mut event_loop, mut state) = session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]));
let (mut event_loop, mut state) = session(
&directory,
wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]),
);
state.xwayland_config.startup = XwaylandStartup::Eager;
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(0, std::sync::atomic::Ordering::SeqCst);
xwayland::initialize(&mut state);
assert!(state.xwayland.is_none(), "an unavailable service publishes no endpoint");
assert!(
state.xwayland.is_none(),
"an unavailable service publishes no endpoint"
);
assert!(
state.x11_diagnostic.is_some(),
"the unavailable start must leave a diagnostic"
);
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(usize::MAX, std::sync::atomic::Ordering::SeqCst);
test_hooks::FAIL_LISTENER_REGISTRATION_AFTER
.store(usize::MAX, std::sync::atomic::Ordering::SeqCst);
// Eager startup must still issue exactly one start request.
xwayland::retry(&mut state).expect("an eager retry of an unavailable service must succeed");
@@ -1516,24 +1670,31 @@ mod manager_loop {
Some(1),
"exactly one generation was requested: {status:?}"
);
assert_eq!(status.readiness, Readiness::Verified, "readiness is earned: {status:?}");
assert_eq!(
status.readiness,
Readiness::Verified,
"readiness is earned: {status:?}"
);
let _ = fs::remove_dir_all(&directory);
}
#[test]
fn restart_required_tracks_desired_configuration_only() {
const PATH: &str =
"xwayland::integration_tests::manager_loop::restart_required_tracks_desired_configuration_only";
const PATH: &str = "xwayland::integration_tests::manager_loop::restart_required_tracks_desired_configuration_only";
if !run_isolated(PATH) {
return;
}
test_hooks::reset();
let directory = std::env::temp_dir().join(format!("ferese-manager-reload-{}", std::process::id()));
let directory =
std::env::temp_dir().join(format!("ferese-manager-reload-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
fs::create_dir_all(&directory).expect("create the test directory");
let (mut event_loop, mut state) = session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]));
let (mut event_loop, mut state) = session(
&directory,
wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]),
);
xwayland::initialize(&mut state);
xwayland::request_start(&mut state).expect("an on-demand service accepts a start request");
dispatch_until(&mut event_loop, &mut state, |state| {
@@ -1542,11 +1703,19 @@ mod manager_loop {
let effective = xwayland::status_snapshot(&state);
let display = effective.display.clone().expect("a display is reserved");
let pid = effective.satellite_pid.expect("a running service reports its pid");
let pid = effective
.satellite_pid
.expect("a running service reports its pid");
let authority = state
.xwayland
.as_ref()
.and_then(|manager| manager.x11_environment().authority.to_str().map(str::to_owned))
.and_then(|manager| {
manager
.x11_environment()
.authority
.to_str()
.map(str::to_owned)
})
.expect("an authority path is published");
assert!(
!effective.restart_required,
@@ -1578,13 +1747,26 @@ mod manager_loop {
Some(display.as_str()),
"the display must not move"
);
assert_eq!(status.satellite_pid, Some(pid), "the service must not be restarted");
assert_eq!(
status.satellite_pid,
Some(pid),
"the service must not be restarted"
);
let authority_now = state
.xwayland
.as_ref()
.and_then(|manager| manager.x11_environment().authority.to_str().map(str::to_owned))
.and_then(|manager| {
manager
.x11_environment()
.authority
.to_str()
.map(str::to_owned)
})
.expect("an authority path is published");
assert_eq!(authority_now, authority, "the authority file must not be replaced");
assert_eq!(
authority_now, authority,
"the authority file must not be replaced"
);
}
// Reverting to the session configuration clears the indication.
+41 -10
View File
@@ -118,7 +118,12 @@ impl Lifecycle {
true
}
pub fn begin_stop_after_failure(&mut self, generation: u64, now: Instant, retryable: bool) -> Option<AfterStop> {
pub fn begin_stop_after_failure(
&mut self,
generation: u64,
now: Instant,
retryable: bool,
) -> Option<AfterStop> {
let current = matches!(
self.phase,
Phase::Starting(current)
@@ -144,7 +149,10 @@ impl Lifecycle {
} else {
AfterStop::Backoff
};
self.phase = Phase::Stopping { generation, after_stop };
self.phase = Phase::Stopping {
generation,
after_stop,
};
Some(after_stop)
}
@@ -183,7 +191,8 @@ impl Lifecycle {
pub fn park_cleanup_failure(&mut self, generation: u64) -> bool {
let Phase::Stopping {
generation: current, ..
generation: current,
..
} = self.phase
else {
return false;
@@ -255,14 +264,21 @@ mod tests {
use super::*;
fn start(lifecycle: &mut Lifecycle) -> u64 {
lifecycle.request_start().expect("Idle must yield a generation ticket")
lifecycle
.request_start()
.expect("Idle must yield a generation ticket")
}
/// Fail a generation the way the manager does, then complete the stop.
///
/// Cleanup completing is what publishes the next phase, so a test that
/// needs the next generation must say so explicitly.
fn fail_and_stop(lifecycle: &mut Lifecycle, generation: u64, now: Instant, retryable: bool) -> AfterStop {
fn fail_and_stop(
lifecycle: &mut Lifecycle,
generation: u64,
now: Instant,
retryable: bool,
) -> AfterStop {
let after_stop = lifecycle
.begin_stop_after_failure(generation, now, retryable)
.expect("the current generation must accept a failure");
@@ -328,7 +344,10 @@ mod tests {
assert!(!lifecycle.spawned(stale));
assert!(!lifecycle.ready(stale));
assert_eq!(lifecycle.begin_stop_after_failure(stale, Instant::now(), true), None);
assert_eq!(
lifecycle.begin_stop_after_failure(stale, Instant::now(), true),
None
);
assert_eq!(lifecycle.phase, Phase::Starting(current));
}
@@ -361,7 +380,10 @@ mod tests {
let now = Instant::now();
let first = start(&mut lifecycle);
assert_eq!(fail_and_stop(&mut lifecycle, first, now, true), AfterStop::Backoff);
assert_eq!(
fail_and_stop(&mut lifecycle, first, now, true),
AfterStop::Backoff
);
let Phase::Backoff { until, .. } = lifecycle.phase else {
panic!("first failure must back off");
};
@@ -369,7 +391,10 @@ mod tests {
let second = start_after_backoff(&mut lifecycle);
let now = Instant::now();
assert_eq!(fail_and_stop(&mut lifecycle, second, now, true), AfterStop::Backoff);
assert_eq!(
fail_and_stop(&mut lifecycle, second, now, true),
AfterStop::Backoff
);
let Phase::Backoff { until, .. } = lifecycle.phase else {
panic!("second failure must back off");
};
@@ -456,7 +481,10 @@ mod tests {
.begin_stop_after_failure(generation, now, true)
.expect("the current generation accepts a failure");
assert!(lifecycle.park_cleanup_failure(generation), "an in-flight stop parks");
assert!(
lifecycle.park_cleanup_failure(generation),
"an in-flight stop parks"
);
assert_eq!(lifecycle.phase, Phase::CleanupFailed { generation });
// The deadline proved nothing, so the service must refuse to act on it.
@@ -569,7 +597,10 @@ mod tests {
lifecycle
.begin_stop_after_failure(generation, Instant::now(), false)
.expect("the current generation accepts a failure");
assert_eq!(lifecycle.finish_stop(generation.wrapping_add(1), Instant::now()), None);
assert_eq!(
lifecycle.finish_stop(generation.wrapping_add(1), Instant::now()),
None
);
// A stale completion leaves the real transition in flight.
assert!(lifecycle.phase.is_stopping());
+172 -95
View File
@@ -98,7 +98,8 @@ pub(crate) mod test_hooks {
ALLOCATOR_ROOT.with(|slot| *slot.borrow_mut() = Some((sockets, locks)));
}
pub(crate) fn allocator_root_for_reservation() -> Option<(std::path::PathBuf, std::path::PathBuf)> {
pub(crate) fn allocator_root_for_reservation()
-> Option<(std::path::PathBuf, std::path::PathBuf)> {
ALLOCATOR_ROOT.with(|slot| slot.borrow().clone())
}
@@ -111,7 +112,8 @@ pub(crate) mod test_hooks {
std::thread::current().id()
));
let _ = std::fs::create_dir_all(&root);
let _ = std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700));
let _ =
std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700));
root
}
@@ -299,15 +301,23 @@ impl XwaylandManager {
self.last_error = None;
}
fn install_exit_watch(&mut self, loop_handle: &Loop, generation: u64, pid: u32) -> Result<(), String> {
fn install_exit_watch(
&mut self,
loop_handle: &Loop,
generation: u64,
pid: u32,
) -> Result<(), String> {
if let Ok(fd) = crate::process::pidfd(pid) {
match loop_handle.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, state| {
let handle = state.loop_handle.clone();
if let Some(manager) = state.xwayland.as_mut() {
manager.handle_exit(&handle, generation, pid);
}
Ok(PostAction::Remove)
}) {
match loop_handle.insert_source(
Generic::new(fd, Interest::READ, Mode::Level),
move |_, _, state| {
let handle = state.loop_handle.clone();
if let Some(manager) = state.xwayland.as_mut() {
manager.handle_exit(&handle, generation, pid);
}
Ok(PostAction::Remove)
},
) {
Ok(token) => {
self.sources.exit_watch = Some(token);
return Ok(());
@@ -315,13 +325,21 @@ impl XwaylandManager {
Err(error) => warn!(%error, pid, "cannot register the X11 service exit source"),
}
} else {
warn!(pid, "pidfd unavailable; using a bounded poll to watch the X11 service");
warn!(
pid,
"pidfd unavailable; using a bounded poll to watch the X11 service"
);
}
self.install_exit_poll(loop_handle, generation, pid)
}
fn install_exit_poll(&mut self, loop_handle: &Loop, generation: u64, pid: u32) -> Result<(), String> {
fn install_exit_poll(
&mut self,
loop_handle: &Loop,
generation: u64,
pid: u32,
) -> Result<(), String> {
let timer = Timer::from_duration(CHILD_POLL_INTERVAL);
let token = loop_handle
.insert_source(timer, move |_, _, state| {
@@ -333,7 +351,9 @@ impl XwaylandManager {
.and_then(|manager| manager.process.as_mut())
.map(|process| process.observe());
match observed {
Some(Observation::Running) => return TimeoutAction::ToDuration(CHILD_POLL_INTERVAL),
Some(Observation::Running) => {
return TimeoutAction::ToDuration(CHILD_POLL_INTERVAL);
}
Some(Observation::Exited) | Some(Observation::Failed(_)) | None => {}
}
if let Some(manager) = state.xwayland.as_mut() {
@@ -349,7 +369,10 @@ impl XwaylandManager {
fn owns(&self, generation: u64, pid: u32) -> bool {
self.lifecycle.phase.generation() == Some(generation)
&& self.process.as_ref().is_some_and(|process| process.pid() == pid)
&& self
.process
.as_ref()
.is_some_and(|process| process.pid() == pid)
}
fn handle_exit(&mut self, loop_handle: &Loop, generation: u64, pid: u32) {
@@ -366,10 +389,11 @@ impl XwaylandManager {
self.last_error = Some(error.to_owned());
// The verdict describes the generation that just ended.
self.readiness_verdict = Readiness::NotAttempted;
let Some(after_stop) =
self.lifecycle
.begin_stop_after_failure(generation, std::time::Instant::now(), retryable)
else {
let Some(after_stop) = self.lifecycle.begin_stop_after_failure(
generation,
std::time::Instant::now(),
retryable,
) else {
return;
};
@@ -392,7 +416,10 @@ impl XwaylandManager {
self.sources.readiness_socket = None;
self.disable_listeners(loop_handle);
debug_assert!(self.sources.is_empty(), "a stop must release every source it owns");
debug_assert!(
self.sources.is_empty(),
"a stop must release every source it owns"
);
let Some(process) = self.process.as_mut() else {
// Nothing survived, so cleanup is already complete.
@@ -410,7 +437,8 @@ impl XwaylandManager {
// The group, not just the leader, has to be gone.
Some(Observation::Exited) | None => {
self.wait_for_empty_group(loop_handle, generation, after_stop);
if self.process.is_none() || !matches!(self.lifecycle.phase, Phase::Stopping { .. }) {
if self.process.is_none() || !matches!(self.lifecycle.phase, Phase::Stopping { .. })
{
// Cleanup finished, or was parked as unconfirmed, inside
// the helper: either way no poll belongs to this stop.
return;
@@ -434,7 +462,10 @@ impl XwaylandManager {
return TimeoutAction::Drop;
};
if !manager.owns(generation, manager.process.as_ref().map_or(0, OwnedProcessGroup::pid)) {
if !manager.owns(
generation,
manager.process.as_ref().map_or(0, OwnedProcessGroup::pid),
) {
return TimeoutAction::Drop;
}
@@ -493,7 +524,11 @@ impl XwaylandManager {
}
}
if self.process.as_ref().is_some_and(OwnedProcessGroup::group_is_empty) {
if self
.process
.as_ref()
.is_some_and(OwnedProcessGroup::group_is_empty)
{
self.complete_stop(loop_handle, generation, after_stop);
return;
}
@@ -503,7 +538,10 @@ impl XwaylandManager {
let now = std::time::Instant::now();
match self.stop_kill_grace {
None => {
warn!(generation, "the X11 service left survivors after SIGKILL; waiting");
warn!(
generation,
"the X11 service left survivors after SIGKILL; waiting"
);
self.stop_kill_grace = Some(now + STOP_TIMEOUT);
}
Some(grace) if now >= grace => {
@@ -605,7 +643,10 @@ impl XwaylandManager {
self.process = None;
if !self.lifecycle.confirm_cleanup(generation) {
return Err("the X11 service left the unconfirmed cleanup state while it was checked".to_owned());
return Err(
"the X11 service left the unconfirmed cleanup state while it was checked"
.to_owned(),
);
}
info!(
generation,
@@ -643,7 +684,10 @@ impl XwaylandManager {
// Safe now: the leader has been reaped, so Drop has nothing to signal.
self.process = None;
let Some(published) = self.lifecycle.finish_stop(generation, std::time::Instant::now()) else {
let Some(published) = self
.lifecycle
.finish_stop(generation, std::time::Instant::now())
else {
return;
};
debug_assert!(
@@ -659,7 +703,10 @@ impl XwaylandManager {
let handle = state.loop_handle.clone();
if let Some(manager) = state.xwayland.as_mut() {
// Idle again: a still-queued connection can wake us.
if manager.lifecycle.finish_backoff(generation, std::time::Instant::now()) {
if manager
.lifecycle
.finish_backoff(generation, std::time::Instant::now())
{
manager.enable_listeners(&handle);
}
}
@@ -672,7 +719,9 @@ impl XwaylandManager {
AfterStop::Failed => {
// The circuit is open. An open listening socket with no consumer
// would strand clients, so fail fast instead.
warn!("X11 endpoint disabled after repeated failures; waiting for an explicit retry");
warn!(
"X11 endpoint disabled after repeated failures; waiting for an explicit retry"
);
self.remove_listener_sources(loop_handle);
self.reservation.close_listeners();
}
@@ -695,7 +744,9 @@ impl XwaylandManager {
return Err("X11 is not in a failed state, so there is nothing to retry".to_owned());
}
if self.process.is_some() {
return Err("X11 is still stopping its previous service; retry once it has finished".to_owned());
return Err(
"X11 is still stopping its previous service; retry once it has finished".to_owned(),
);
}
// Rebind the **same** display so launchers that already learned the
@@ -756,7 +807,10 @@ fn validate_runtime_directory(path: &std::path::Path) -> Result<(), String> {
let metadata = std::fs::symlink_metadata(path)
.map_err(|error| format!("XDG_RUNTIME_DIR is unusable ({}): {error}", path.display()))?;
let uid = unsafe { libc::geteuid() };
if !metadata.is_dir() || metadata.file_type().is_symlink() || metadata.uid() != uid || metadata.mode() & 0o077 != 0
if !metadata.is_dir()
|| metadata.file_type().is_symlink()
|| metadata.uid() != uid
|| metadata.mode() & 0o077 != 0
{
return Err(format!(
"XDG_RUNTIME_DIR is not a private user-owned directory: {}",
@@ -810,8 +864,9 @@ fn start_service(state: &mut Ferese) -> Result<(), String> {
}
fn request_first_generation(state: &mut Ferese) -> Result<u64, String> {
request_start(state)
.ok_or_else(|| "X11 could not be started yet; a previous attempt is still in progress".to_owned())
request_start(state).ok_or_else(|| {
"X11 could not be started yet; a previous attempt is still in progress".to_owned()
})
}
/// Create the private directory that holds one service instance's readiness
@@ -821,7 +876,9 @@ fn request_first_generation(state: &mut Ferese) -> Result<u64, String> {
/// against the same code that ships: the directory is `0700` whatever the
/// session umask is, because another user must not be able to place a socket
/// where Ferese looks for notifications.
fn notification_directory(runtime_directory: &std::path::Path) -> Result<tempfile::TempDir, String> {
fn notification_directory(
runtime_directory: &std::path::Path,
) -> Result<tempfile::TempDir, String> {
tempfile::Builder::new()
.prefix("ferese-x11-")
.rand_bytes(16)
@@ -833,7 +890,8 @@ fn notification_directory(runtime_directory: &std::path::Path) -> Result<tempfil
fn prepare(config: &XwaylandConfig) -> Result<XwaylandManager, String> {
let runtime_directory = runtime_directory()?;
let notify_directory = notification_directory(&runtime_directory)?;
let reservation = Reservation::allocate().map_err(|error| format!("cannot reserve an X11 display: {error}"))?;
let reservation = Reservation::allocate()
.map_err(|error| format!("cannot reserve an X11 display: {error}"))?;
let display = reservation.display_number();
let authority = AuthorityFile::create(&runtime_directory, display)
.map_err(|error| format!("cannot create an Xauthority file: {error}"))?;
@@ -876,31 +934,34 @@ fn register_listeners(loop_handle: &Loop, manager: &mut XwaylandManager) -> Resu
error.to_string()
})?;
let token = loop_handle
.insert_source(Generic::new(listener, Interest::READ, Mode::Level), |_, _, state| {
let handle = state.loop_handle.clone();
.insert_source(
Generic::new(listener, Interest::READ, Mode::Level),
|_, _, state| {
let handle = state.loop_handle.clone();
let ticket = state.xwayland.as_mut().and_then(|manager| {
let ticket = manager.lifecycle.request_start();
if ticket.is_some() {
manager.disable_listeners(&handle);
}
ticket
});
if let Some(generation) = ticket {
handle.insert_idle(move |state| {
let current = state
.xwayland
.as_ref()
.is_some_and(|manager| manager.owns_generation(generation));
if current {
start_generation(state, generation);
let ticket = state.xwayland.as_mut().and_then(|manager| {
let ticket = manager.lifecycle.request_start();
if ticket.is_some() {
manager.disable_listeners(&handle);
}
ticket
});
}
Ok(PostAction::Disable)
})
if let Some(generation) = ticket {
handle.insert_idle(move |state| {
let current = state
.xwayland
.as_ref()
.is_some_and(|manager| manager.owns_generation(generation));
if current {
start_generation(state, generation);
}
});
}
Ok(PostAction::Disable)
},
)
.map_err(|error| {
for token in inserted.drain(..) {
loop_handle.remove(token);
@@ -918,7 +979,9 @@ fn register_listeners(loop_handle: &Loop, manager: &mut XwaylandManager) -> Resu
pub fn retry(state: &mut Ferese) -> Result<(), String> {
if !state.xwayland_config.enabled {
return Err("X11 is disabled in the configuration; enable it and start a new session".to_owned());
return Err(
"X11 is disabled in the configuration; enable it and start a new session".to_owned(),
);
}
if let Some(manager) = state.xwayland.as_mut() {
@@ -946,7 +1009,11 @@ pub fn status_snapshot(state: &Ferese) -> ferese_ipc::xwayland::Status {
XwaylandStartup::Eager => "eager",
}
.to_owned(),
state: if disabled { State::Disabled } else { State::Failed },
state: if disabled {
State::Disabled
} else {
State::Failed
},
display: None,
satellite_pid: None,
generation: None,
@@ -959,11 +1026,9 @@ pub fn status_snapshot(state: &Ferese) -> ferese_ipc::xwayland::Status {
None
} else {
// Never report an empty failure: say what the user can do.
Some(
state.x11_diagnostic.clone().unwrap_or_else(|| {
"the X11 service is not running; retry it or start a new session".to_owned()
}),
)
Some(state.x11_diagnostic.clone().unwrap_or_else(|| {
"the X11 service is not running; retry it or start a new session".to_owned()
}))
},
};
};
@@ -1002,7 +1067,9 @@ fn start_generation(state: &mut Ferese, generation: u64) {
let Some(manager) = state.xwayland.as_mut() else {
return;
};
if !manager.owns_generation(generation) || manager.lifecycle.phase != Phase::Starting(generation) {
if !manager.owns_generation(generation)
|| manager.lifecycle.phase != Phase::Starting(generation)
{
return;
}
@@ -1118,44 +1185,49 @@ fn install_readiness_source(
.map_err(|error| format!("cannot duplicate the readiness endpoint: {error}"))?;
let token = loop_handle
.insert_source(Generic::new(clone, Interest::READ, Mode::Level), move |_, _, state| {
let handle = state.loop_handle.clone();
let Some(manager) = state.xwayland.as_mut() else {
return Ok(PostAction::Remove);
};
if !manager.owns_generation(generation) || manager.lifecycle.phase != Phase::Spawned(generation) {
return Ok(PostAction::Remove);
}
for _ in 0..READINESS_DRAIN_BUDGET {
let Some(socket) = manager.sources.readiness_socket.as_ref() else {
.insert_source(
Generic::new(clone, Interest::READ, Mode::Level),
move |_, _, state| {
let handle = state.loop_handle.clone();
let Some(manager) = state.xwayland.as_mut() else {
return Ok(PostAction::Remove);
};
match socket.recv_ready(pid) {
Ok(true) => {
info!(generation, "X11 service reported readiness");
if manager.lifecycle.ready(generation) {
manager.finish_ready(&handle);
if !manager.owns_generation(generation)
|| manager.lifecycle.phase != Phase::Spawned(generation)
{
return Ok(PostAction::Remove);
}
manager.enable_listeners(&handle);
}
for _ in 0..READINESS_DRAIN_BUDGET {
let Some(socket) = manager.sources.readiness_socket.as_ref() else {
return Ok(PostAction::Remove);
}
// Ignore stale, foreign, or malformed notifications.
Ok(false) => continue,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
// Stay level-triggered for any remaining work.
return Ok(PostAction::Continue);
}
Err(error) => {
warn!(%error, "cannot read the X11 readiness notification");
return Ok(PostAction::Continue);
};
match socket.recv_ready(pid) {
Ok(true) => {
info!(generation, "X11 service reported readiness");
if manager.lifecycle.ready(generation) {
manager.finish_ready(&handle);
manager.enable_listeners(&handle);
}
return Ok(PostAction::Remove);
}
// Ignore stale, foreign, or malformed notifications.
Ok(false) => continue,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
// Stay level-triggered for any remaining work.
return Ok(PostAction::Continue);
}
Err(error) => {
warn!(%error, "cannot read the X11 readiness notification");
return Ok(PostAction::Continue);
}
}
}
}
Ok(PostAction::Continue)
})
Ok(PostAction::Continue)
},
)
.map_err(|error| format!("cannot register the readiness source: {error}"))?;
manager.sources.readiness = Some(token);
@@ -1227,7 +1299,8 @@ mod tests {
return;
}
let mut manager = prepare(&XwaylandConfig::default()).expect("reserve a managed X11 service");
let mut manager =
prepare(&XwaylandConfig::default()).expect("reserve a managed X11 service");
assert_eq!(
manager.status().readiness,
Readiness::NotAttempted,
@@ -1257,7 +1330,11 @@ mod tests {
let next = notification_directory(root.path()).expect("the second instance directory");
let next_socket = ReadinessSocket::create(next.path(), 1).expect("the second endpoint");
assert_ne!(stale.path(), next.path(), "each instance owns its own directory");
assert_ne!(
stale.path(),
next.path(),
"each instance owns its own directory"
);
assert_ne!(
stale_socket.path(),
next_socket.path(),
+52 -18
View File
@@ -121,7 +121,8 @@ impl ReadinessSocket {
from_satellite = true;
}
}
} else if cmsg.cmsg_level == libc::SOL_SOCKET && cmsg.cmsg_type == libc::SCM_RIGHTS {
} else if cmsg.cmsg_level == libc::SOL_SOCKET && cmsg.cmsg_type == libc::SCM_RIGHTS
{
// Accepting a datagram must never adopt descriptors from an
// untrusted sender. Close them instead of dropping them on
// the floor, where they would stay open in this process.
@@ -144,7 +145,9 @@ impl ReadinessSocket {
// Decided only after the control walk: a completely filled payload
// buffer cannot be distinguished from truncation, and the kernel's
// truncation flags never satisfy the readiness contract.
if received as usize == buffer.len() || message.msg_flags & (libc::MSG_CTRUNC | libc::MSG_TRUNC) != 0 {
if received as usize == buffer.len()
|| message.msg_flags & (libc::MSG_CTRUNC | libc::MSG_TRUNC) != 0
{
return Ok(false);
}
@@ -187,8 +190,12 @@ fn exact_line(payload: &[u8], wanted: &[u8]) -> Option<bool> {
}
fn is_key_value(line: &str) -> bool {
line.split_once('=')
.is_some_and(|(key, _)| !key.is_empty() && key.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'_'))
line.split_once('=').is_some_and(|(key, _)| {
!key.is_empty()
&& key
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
})
}
impl Drop for ReadinessSocket {
@@ -247,10 +254,12 @@ mod tests {
}
fn private_runtime_dir(name: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!("ferese-readiness-{name}-{}", std::process::id()));
let root =
std::env::temp_dir().join(format!("ferese-readiness-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700)).unwrap();
std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700))
.unwrap();
root
}
@@ -290,7 +299,8 @@ mod tests {
}
// A descriptor for a known file, so the receiver can prove it
// closed exactly this one rather than guessing at a count.
let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) else {
let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes())
else {
libc::_exit(73);
};
let spare = libc::open(name.as_ptr(), libc::O_RDONLY);
@@ -411,7 +421,11 @@ mod tests {
/// Send `READY=1` with more descriptors than the receiver's control buffer can
/// hold, so the kernel truncates the ancillary data while installing what fits.
fn send_descriptors_beyond_capacity_from_child(path: &Path, sentinel: &Path, count: usize) -> i32 {
fn send_descriptors_beyond_capacity_from_child(
path: &Path,
sentinel: &Path,
count: usize,
) -> i32 {
// SAFETY: the child performs only socket/open/sendmsg/sleep/_exit, all
// async-signal-safe except sleep, which is a bounded wait before exit.
unsafe {
@@ -422,7 +436,8 @@ mod tests {
if fd < 0 {
libc::_exit(70);
}
let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) else {
let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes())
else {
libc::_exit(73);
};
let sentinel_fd = libc::open(name.as_ptr(), libc::O_RDONLY);
@@ -488,7 +503,8 @@ mod tests {
if fd < 0 {
libc::_exit(70);
}
let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) else {
let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes())
else {
libc::_exit(73);
};
let sentinel_fd = libc::open(name.as_ptr(), libc::O_RDONLY);
@@ -510,7 +526,8 @@ mod tests {
control.payload.as_mut_ptr(),
std::mem::size_of::<libc::c_int>(),
);
control.header.cmsg_len = (cmsg_data_offset() + std::mem::size_of::<libc::c_int>()) as _;
control.header.cmsg_len =
(cmsg_data_offset() + std::mem::size_of::<libc::c_int>()) as _;
control.header.cmsg_level = libc::SOL_SOCKET;
control.header.cmsg_type = libc::SCM_RIGHTS;
@@ -518,7 +535,8 @@ mod tests {
message.msg_iov = std::ptr::from_mut(&mut iov);
message.msg_iovlen = 1;
message.msg_control = std::ptr::from_mut(&mut control).cast();
message.msg_controllen = cmsg_data_offset() + std::mem::size_of::<libc::c_int>();
message.msg_controllen =
cmsg_data_offset() + std::mem::size_of::<libc::c_int>();
if libc::sendmsg(fd, std::ptr::from_ref(&message), libc::MSG_NOSIGNAL) < 0 {
libc::_exit(71);
}
@@ -625,7 +643,9 @@ mod tests {
libc::kill(pid, libc::SIGKILL);
libc::waitpid(pid, std::ptr::from_mut(&mut status), 0);
}
panic!("the storm was not delivered within the shared deadline: {judged} of 128 judged");
panic!(
"the storm was not delivered within the shared deadline: {judged} of 128 judged"
);
}
// Every datagram was accepted, so the sender has reached its exit.
@@ -635,7 +655,10 @@ mod tests {
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0,
"child must deliver the storm: {status:#x}"
);
assert!(judged >= 128, "the storm must be delivered and judged: {judged}");
assert!(
judged >= 128,
"the storm must be delivered and judged: {judged}"
);
assert!(
started.elapsed() < std::time::Duration::from_secs(2),
"judging the storm must not stall the consumer: {:?}",
@@ -685,7 +708,10 @@ mod tests {
/// A unique empty file used to identify a delivered descriptor.
fn sentinel_file(name: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!("ferese-readiness-sentinel-{name}-{}", std::process::id()));
let path = std::env::temp_dir().join(format!(
"ferese-readiness-sentinel-{name}-{}",
std::process::id()
));
let _ = std::fs::remove_file(&path);
std::fs::write(&path, b"").unwrap();
path
@@ -709,7 +735,10 @@ mod tests {
let path = socket.path().to_owned();
assert!(path.exists());
assert!(path.starts_with(&runtime));
assert!(path.to_string_lossy().contains('7'), "path must encode the generation");
assert!(
path.to_string_lossy().contains('7'),
"path must encode the generation"
);
path
};
@@ -815,7 +844,10 @@ mod tests {
libc::waitpid(pid, std::ptr::from_mut(&mut status), 0);
}
assert!(accepted, "an exact READY=1 from the expected PID must be accepted");
assert!(
accepted,
"an exact READY=1 from the expected PID must be accepted"
);
}
#[test]
@@ -830,7 +862,9 @@ mod tests {
if pid == 0 {
for _ in 0..500 {
if let Ok(client) = UnixDatagram::unbound()
&& client.send_to(b"RELOADING=1\nSTATUS=starting", &path).is_ok()
&& client
.send_to(b"RELOADING=1\nSTATUS=starting", &path)
.is_ok()
{
libc::_exit(0);
}
+53 -19
View File
@@ -60,7 +60,10 @@ impl Drop for CreatedPath {
fn validate_shared_directory(path: &Path) -> io::Result<()> {
let metadata = fs::symlink_metadata(path)?;
let uid = unsafe { libc::geteuid() };
if !metadata.is_dir() || metadata.file_type().is_symlink() || (metadata.uid() != 0 && metadata.uid() != uid) {
if !metadata.is_dir()
|| metadata.file_type().is_symlink()
|| (metadata.uid() != 0 && metadata.uid() != uid)
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("untrusted X11 directory: {}", path.display()),
@@ -93,10 +96,11 @@ impl Reservation {
{
// Owned inputs, so an ordinary test never competes for a real slot in
// `/tmp/.X11-unix` nor depends on that directory existing.
let (sockets, locks) = crate::xwayland::test_hooks::allocator_root_for_reservation().unwrap_or_else(|| {
let root = crate::xwayland::test_hooks::default_allocator_root();
(root.clone(), root)
});
let (sockets, locks) = crate::xwayland::test_hooks::allocator_root_for_reservation()
.unwrap_or_else(|| {
let root = crate::xwayland::test_hooks::default_allocator_root();
(root.clone(), root)
});
Self::allocate_in(&sockets, &locks, 0..64)
}
@@ -104,7 +108,11 @@ impl Reservation {
Self::allocate_in(Path::new("/tmp/.X11-unix"), Path::new("/tmp"), 0..64)
}
fn allocate_in(socket_directory: &Path, lock_directory: &Path, candidates: Range<u32>) -> io::Result<Self> {
fn allocate_in(
socket_directory: &Path,
lock_directory: &Path,
candidates: Range<u32>,
) -> io::Result<Self> {
validate_shared_directory(socket_directory)?;
validate_shared_directory(lock_directory)?;
@@ -124,7 +132,9 @@ impl Reservation {
};
let lock_path = CreatedPath::record(path, &file.metadata()?);
if let Err(error) = writeln!(file, "{:>10}", std::process::id()).and_then(|()| file.flush()) {
if let Err(error) =
writeln!(file, "{:>10}", std::process::id()).and_then(|()| file.flush())
{
drop(file);
drop(lock_path);
return Err(error);
@@ -227,7 +237,10 @@ mod tests {
use std::os::unix::fs::PermissionsExt;
fn shared_root(name: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!("ferese-xwayland-alloc-{name}-{}", std::process::id()));
let root = std::env::temp_dir().join(format!(
"ferese-xwayland-alloc-{name}-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
for directory in ["sockets", "locks"] {
@@ -240,10 +253,13 @@ mod tests {
#[test]
fn allocates_a_display_with_two_listener_transports() {
let root = shared_root("two-transports");
let reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let reservation =
Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
assert!(reservation.listeners().len() >= 2);
let socket = root.join("sockets").join(format!("X{}", reservation.display_number()));
let socket = root
.join("sockets")
.join(format!("X{}", reservation.display_number()));
assert!(socket.exists(), "conventional socket path must exist");
assert!(
root.join("locks")
@@ -276,14 +292,23 @@ mod tests {
fs::write(&stale, b"9999\n").unwrap();
let reservation = Reservation::allocate_in(&root.join("sockets"), &locks, 0..64).unwrap();
assert_ne!(reservation.display_number(), 3, "occupied number must be skipped");
assert_eq!(fs::read(&stale).unwrap(), b"9999\n", "stale lock must not be reclaimed");
assert_ne!(
reservation.display_number(),
3,
"occupied number must be skipped"
);
assert_eq!(
fs::read(&stale).unwrap(),
b"9999\n",
"stale lock must not be reclaimed"
);
}
#[test]
fn cleanup_removes_paths_this_process_created() {
let root = shared_root("cleanup");
let reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let reservation =
Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let display = reservation.display_number();
let socket = root.join("sockets").join(format!("X{display}"));
let lock = root.join("locks").join(format!(".X{display}-lock"));
@@ -297,20 +322,26 @@ mod tests {
#[test]
fn close_listeners_keeps_the_display_lock_owned() {
let root = shared_root("close-listeners");
let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let mut reservation =
Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let display = reservation.display_number();
reservation.close_listeners();
assert!(reservation.listeners().is_empty());
assert!(root.join("locks").join(format!(".X{display}-lock")).exists());
assert!(
root.join("locks")
.join(format!(".X{display}-lock"))
.exists()
);
assert!(!root.join("sockets").join(format!("X{display}")).exists());
}
#[test]
fn retry_rebinds_the_same_display_number() {
let root = shared_root("retry");
let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let mut reservation =
Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let display = reservation.display_number();
reservation.close_listeners();
@@ -324,7 +355,8 @@ mod tests {
#[test]
fn binding_twice_is_rejected() {
let root = shared_root("double-bind");
let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
let mut reservation =
Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap();
reservation.close_listeners();
reservation.bind_same_display().unwrap();
@@ -334,7 +366,8 @@ mod tests {
#[test]
fn an_untrusted_socket_directory_is_rejected() {
let root = std::env::temp_dir().join(format!("ferese-xwayland-untrusted-{}", std::process::id()));
let root =
std::env::temp_dir().join(format!("ferese-xwayland-untrusted-{}", std::process::id()));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
@@ -352,7 +385,8 @@ mod tests {
#[test]
fn a_missing_socket_directory_is_an_actionable_error() {
let root = shared_root("missing-dir");
let error = Reservation::allocate_in(&root.join("absent"), &root.join("locks"), 0..64).unwrap_err();
let error =
Reservation::allocate_in(&root.join("absent"), &root.join("locks"), 0..64).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
}