diff --git a/src/gui/child.rs b/src/gui/child.rs index cc267b5..97b7ee4 100644 --- a/src/gui/child.rs +++ b/src/gui/child.rs @@ -66,7 +66,9 @@ pub enum CaptureState { const STDERR_TAIL_MAX: usize = 60; pub struct ChildProc { - child: Child, + /// `Some` while the child is owned here; `Drop` takes it to hand off to a + /// detached reaper thread (see the `Drop` impl). + child: Option, pub rx: Receiver, stderr_tail: Arc>>, /// Write end of the child's stdin, for the line-based command channel @@ -123,7 +125,7 @@ impl ChildProc { }); Ok(Self { - child, + child: Some(child), rx, stderr_tail, stdin, @@ -145,38 +147,42 @@ impl ChildProc { /// Whether the child is still running. pub fn is_alive(&mut self) -> bool { - matches!(self.child.try_wait(), Ok(None)) + matches!(self.child.as_mut().map(Child::try_wait), Some(Ok(None))) } /// The last captured stderr lines, joined — for error display. pub fn stderr_tail(&self) -> String { self.stderr_tail.lock().unwrap().join("\n") } - - /// Gracefully stop the child: SIGINT (so the host runs its ctrl-c teardown - /// — tears down capture, closes the endpoint), with a ~2 s grace period - /// before a hard kill. Idempotent. - pub fn stop(&mut self) { - if matches!(self.child.try_wait(), Ok(Some(_))) { - return; // already exited - } - let _ = kill(Pid::from_raw(self.child.id() as i32), Signal::SIGINT); - for _ in 0..40 { - if matches!(self.child.try_wait(), Ok(Some(_))) { - return; - } - std::thread::sleep(Duration::from_millis(50)); - } - let _ = self.child.kill(); - let _ = self.child.wait(); - } } impl Drop for ChildProc { fn drop(&mut self) { - // Closing the window (dropping the app, hence the session) must not - // orphan a live host child streaming to viewers. - self.stop(); + // Leaving a host/viewer screen, or closing the window, must not orphan + // a live child — but it must also not *block*. eframe runs this drop + // synchronously while it destroys the window, so a grace-period wait + // here freezes the window mid-close: the first click looks like it did + // nothing (the stream just drops) and the window only goes away on a + // second click. So SIGINT now — synchronously, so the host always gets + // its ctrl-c teardown (capture down, endpoint closed) even if we exit + // right after — then reap on a detached thread instead of waiting. + let Some(mut child) = self.child.take() else { + return; + }; + if matches!(child.try_wait(), Ok(Some(_))) { + return; // already exited; nothing to signal or reap + } + let _ = kill(Pid::from_raw(child.id() as i32), Signal::SIGINT); + std::thread::spawn(move || { + for _ in 0..40 { + if matches!(child.try_wait(), Ok(Some(_))) { + return; + } + std::thread::sleep(Duration::from_millis(50)); + } + let _ = child.kill(); + let _ = child.wait(); + }); } }