From 1bbf15fdcad3b56db216bd04cd49f7385978552a Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Sun, 18 Oct 2026 15:46:14 +0300 Subject: [PATCH] fix(nescope): end nescope when the hub stays gone after its socket closes The input source now removes itself on EOF or reset and raises a flag; the main loop exits 2 s later unless a signal already ended it, so a hub crash mid-run still ends the compositor (and the box via CompositorDied) instead of holding a GPU with no input. Also corrects the comment that promised a reconnect that does not exist. --- apps/nescope/src/input_ipc.rs | 15 +++++++++++++++ apps/nescope/src/main.rs | 25 ++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/nescope/src/input_ipc.rs b/apps/nescope/src/input_ipc.rs index b92bbdbe..861590a3 100644 --- a/apps/nescope/src/input_ipc.rs +++ b/apps/nescope/src/input_ipc.rs @@ -1,12 +1,16 @@ use std::io::{self, Read}; use std::os::unix::io::{AsFd, BorrowedFd}; use std::os::unix::net::UnixStream; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use calloop::{EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory}; pub struct InputIpcSource { stream: UnixStream, buf: Vec, + /// Set once the hub has closed its end, for the main loop to act on. + closed: Arc, } impl InputIpcSource { @@ -16,9 +20,17 @@ impl InputIpcSource { Ok(Self { stream, buf: Vec::new(), + closed: Arc::new(AtomicBool::new(false)), }) } + /// True once the hub has closed the socket. The main loop gives that a + /// grace period and then ends nescope: a compositor with no input holds + /// the GPU for nothing. + pub fn closed_flag(&self) -> Arc { + self.closed.clone() + } + pub fn try_clone(&self) -> io::Result { self.stream.try_clone() } @@ -34,6 +46,7 @@ impl InputIpcSource { Ok(n) => self.buf.extend_from_slice(&tmp[..n]), Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(false), Err(e) if e.kind() == io::ErrorKind::Interrupted => {} + Err(e) if e.kind() == io::ErrorKind::ConnectionReset => return Ok(true), Err(e) => return Err(e), } } @@ -81,6 +94,7 @@ impl EventSource for InputIpcSource { } if closed { + self.closed.store(true, Ordering::Relaxed); tracing::warn!("input IPC socket closed by the hub; no more input from it"); return Ok(PostAction::Remove); } @@ -112,6 +126,7 @@ mod tests { InputIpcSource { stream: a, buf: Vec::new(), + closed: Arc::new(AtomicBool::new(false)), }, b, ) diff --git a/apps/nescope/src/main.rs b/apps/nescope/src/main.rs index 425def56..eef9469f 100644 --- a/apps/nescope/src/main.rs +++ b/apps/nescope/src/main.rs @@ -389,11 +389,15 @@ fn main() { // ── Input IPC source ───────────────────────────────────────────────── // Connect to the neshub input socket and feed events into the - // compositor seat. Reconnection is handled in the idle callback. + // compositor seat. There is no reconnection: when the hub closes the socket + // the source removes itself, and the loop below ends nescope after a short + // grace (a signal that arrives first ends it sooner). let ipc_path = args.input_ipc.clone(); + let mut hub_closed_flag: Option> = None; match input_ipc::InputIpcSource::connect(&ipc_path) { Ok(source) => { tracing::info!("Connected to input IPC socket: {ipc_path}"); + hub_closed_flag = Some(source.closed_flag()); match source.try_clone() { Ok(write_stream) => { state.ipc_write = Some(write_stream); @@ -476,6 +480,7 @@ fn main() { // never arrives there is nothing to wait for and no game to run. let startup = std::time::Instant::now(); let mut xwayland_timed_out = false; + let mut hub_closed_at: Option = None; event_loop .run(Some(Duration::from_secs(1)), &mut data, move |data| { @@ -578,6 +583,24 @@ fn main() { } } + // ── Hub gone ────────────────────────────────────────────── + // neshub closing the input socket mid-run leaves a compositor + // nobody can drive. The grace lets a shutdown that is already + // under way (SIGTERM stops the loop itself) finish its own way; + // otherwise nescope ends, and nesinit ends the box on seeing it. + if hub_closed_flag + .as_ref() + .is_some_and(|f| f.load(std::sync::atomic::Ordering::Relaxed)) + { + let since = hub_closed_at.get_or_insert_with(std::time::Instant::now); + if since.elapsed() > Duration::from_secs(2) { + tracing::error!("input hub gone for 2 s with no shutdown — exiting"); + kill_all_children(); + data.loop_signal.stop(); + return; + } + } + // ── Poll primary process ────────────────────────────────── // The launcher (e.g. Steam's shell wrapper) may exit quickly // while the real game client stays alive as a reparented