fix(gui): close the window on the first click while hosting
Closing the window while a host/viewer child was running took two clicks: the first only dropped the stream, the second actually closed the window. eframe drops the app synchronously while it destroys the window, which ran `ChildProc`'s teardown — SIGINT plus a up-to-2s grace-period wait — on the event-loop thread. That wait froze the window mid-close, so the first click looked like it only killed the stream and the window lingered until a second close event. (The teardown runs from `Drop`, not from an `on_exit` / `close_requested` hook, so it fires on every backend and close path; those hooks don't fire at all under some winit backends.) Make the teardown non-blocking: hold the child in an `Option`, and on drop SIGINT it synchronously (so the host still runs its ctrl-c teardown even if we exit immediately after) then reap it on a detached thread instead of waiting inline. The app drops instantly, so the window closes on the first click; the kicked-off SIGINT still tears the stream down cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+30
-24
@@ -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<Child>,
|
||||
pub rx: Receiver<ChildEvent>,
|
||||
stderr_tail: Arc<Mutex<Vec<String>>>,
|
||||
/// 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user