From 6c86f88e1068979bbb55c8cb485d257fa770c6c9 Mon Sep 17 00:00:00 2001 From: Mariano Abad Date: Mon, 10 Aug 2026 12:36:31 -0300 Subject: [PATCH] fix: latch an unwired consumer on the timeout path too A binary that does not dispatch the probe argument runs its normal startup; a long-running one outlives the deadline and was killed before the handshake check could run, so PROBE_UNSUPPORTED never latched and every enumeration cycle spawned a full consumer process again. The timeout path now judges the child by what it already wrote: a real probe prints the magic line first and flushes, so its absence after a whole deadline means this is not a probe. Only the buffered bytes are read, non-blocking, because an EOF-seeking read could hang on a grandchild that inherited the write end of the pipe. Also report the exit status when a failed child left stderr empty, which panic=abort and signals do, and name the malformed-list case in the deserialization error. --- src/platform/linux.rs | 52 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 8c94f7d74..08ff6d1eb 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -543,6 +543,19 @@ fn wayland_displays_from_runtime_dir(named_endpoint: bool) -> ResultType= deadline => { let _ = child.kill(); let _ = child.wait(); + // An unwired binary runs its normal startup, and a long-running one (the + // server itself) lands HERE rather than at the handshake check below — latch + // on this path too, or every enumeration cycle spawns a full consumer + // process. Judged by what the child already wrote: a real probe prints the + // magic line first and flushes, so its absence after a whole deadline means + // this is not a probe. Only buffered bytes are read — a blocking read could + // hang on a grandchild that inherited the write end. + if first_buffered_line(child.stdout.take()).as_deref() + != Some(WAYLAND_PROBE_MAGIC) + { + PROBE_UNSUPPORTED.store(true, Ordering::Release); + bail!("the wayland socket probe timed out without the handshake; probe disabled"); + } bail!("the wayland socket probe did not answer and was killed"); } None => std::thread::sleep(std::time::Duration::from_millis(25)), @@ -564,9 +577,18 @@ fn wayland_displays_from_runtime_dir(named_endpoint: bool) -> ResultType = serde_json::from_str(lines.next().unwrap_or_default())?; + let displays: Vec = match serde_json::from_str(lines.next().unwrap_or_default()) + { + Ok(displays) => displays, + Err(err) => bail!("wayland socket probe answered a malformed list: {err}"), + }; // The child already refuses an empty list; refuse it here too, so a truncated pipe cannot // become a cached-for-life empty enumeration. if displays.is_empty() { @@ -579,6 +601,32 @@ fn wayland_displays_from_runtime_dir(named_endpoint: bool) -> ResultType) -> Option { + use std::io::Read; + use std::os::fd::AsRawFd; + let mut pipe = pipe?; + let fd = pipe.as_raw_fd(); + unsafe { + let flags = libc::fcntl(fd, libc::F_GETFL); + if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 { + return None; + } + } + // The magic line is written in one flush and fits many times over; one read is enough. + let mut buf = vec![0u8; 256]; + match pipe.read(&mut buf) { + Ok(n) => { + buf.truncate(n); + String::from_utf8_lossy(&buf).lines().next().map(str::to_owned) + } + Err(_) => None, + } +} + #[cfg(target_os = "linux")] fn probe_runtime_dir(dir: &Path) -> ResultType> { use std::os::unix::net::UnixStream;