Format Rust sources at 100 columns
This commit is contained in:
1 parent
4661f595f8
commit
610478f8c7
336 files changed
+23497
-7892
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user