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;
|
const STDERR_TAIL_MAX: usize = 60;
|
||||||
|
|
||||||
pub struct ChildProc {
|
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>,
|
pub rx: Receiver<ChildEvent>,
|
||||||
stderr_tail: Arc<Mutex<Vec<String>>>,
|
stderr_tail: Arc<Mutex<Vec<String>>>,
|
||||||
/// Write end of the child's stdin, for the line-based command channel
|
/// Write end of the child's stdin, for the line-based command channel
|
||||||
@@ -123,7 +125,7 @@ impl ChildProc {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
child,
|
child: Some(child),
|
||||||
rx,
|
rx,
|
||||||
stderr_tail,
|
stderr_tail,
|
||||||
stdin,
|
stdin,
|
||||||
@@ -145,38 +147,42 @@ impl ChildProc {
|
|||||||
|
|
||||||
/// Whether the child is still running.
|
/// Whether the child is still running.
|
||||||
pub fn is_alive(&mut self) -> bool {
|
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.
|
/// The last captured stderr lines, joined — for error display.
|
||||||
pub fn stderr_tail(&self) -> String {
|
pub fn stderr_tail(&self) -> String {
|
||||||
self.stderr_tail.lock().unwrap().join("\n")
|
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 {
|
impl Drop for ChildProc {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Closing the window (dropping the app, hence the session) must not
|
// Leaving a host/viewer screen, or closing the window, must not orphan
|
||||||
// orphan a live host child streaming to viewers.
|
// a live child — but it must also not *block*. eframe runs this drop
|
||||||
self.stop();
|
// 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