feat(gui): list connected viewers and let the host kick them
Track viewers by endpoint id instead of a bare count. The JSON event stream gains viewer_joined / viewer_left (each carrying the id), replacing viewer_count; active/max still ride along so the count display is unchanged. The host screen now renders one row per connected viewer with a Kick button. Clicking it sends `kick <id>` to the headless child over a new stdin command channel, which the host turns into a per-viewer CancellationToken cancel; the existing teardown path then emits the leave, so a kick and a self-disconnect look identical downstream. The stdin channel only runs under --output json (the GUI shell-out) and on a detached OS thread, so a read parked on stdin can't hold up the host's Ctrl+C shutdown. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+37
-7
@@ -6,8 +6,8 @@
|
||||
//! egui app drains each frame. stderr is captured into a small ring so a
|
||||
//! failed launch (e.g. a missing gst plugin) can be surfaced in the window.
|
||||
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
@@ -35,7 +35,13 @@ pub enum ChildEvent {
|
||||
max_viewers: u32,
|
||||
max_viewers_source: String,
|
||||
},
|
||||
ViewerCount {
|
||||
ViewerJoined {
|
||||
id: String,
|
||||
active: u32,
|
||||
max: u32,
|
||||
},
|
||||
ViewerLeft {
|
||||
id: String,
|
||||
active: u32,
|
||||
max: u32,
|
||||
},
|
||||
@@ -63,6 +69,9 @@ pub struct ChildProc {
|
||||
child: Child,
|
||||
pub rx: Receiver<ChildEvent>,
|
||||
stderr_tail: Arc<Mutex<Vec<String>>>,
|
||||
/// Write end of the child's stdin, for the line-based command channel
|
||||
/// (see [`ChildProc::send_command`]). `None` once it's been closed.
|
||||
stdin: Option<ChildStdin>,
|
||||
}
|
||||
|
||||
impl ChildProc {
|
||||
@@ -72,11 +81,14 @@ impl ChildProc {
|
||||
let exe = std::env::current_exe()?;
|
||||
let mut child = Command::new(exe)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
// Piped so we can send line commands (e.g. `kick <id>`); the host
|
||||
// only reads it when driven this way (`--output json`).
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let stdout = child.stdout.take().expect("stdout piped");
|
||||
std::thread::spawn(move || {
|
||||
@@ -114,9 +126,23 @@ impl ChildProc {
|
||||
child,
|
||||
rx,
|
||||
stderr_tail,
|
||||
stdin,
|
||||
})
|
||||
}
|
||||
|
||||
/// Send one newline-terminated command to the child over its stdin (the
|
||||
/// host parses these as `kick <endpoint-id>`). Best-effort: a closed pipe
|
||||
/// (child already gone) just drops the command.
|
||||
pub fn send_command(&mut self, cmd: &str) {
|
||||
let Some(stdin) = self.stdin.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = writeln!(stdin, "{cmd}") {
|
||||
tracing::warn!("failed to send command to host child: {e}");
|
||||
self.stdin = None; // pipe is dead; stop trying
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the child is still running.
|
||||
pub fn is_alive(&mut self) -> bool {
|
||||
matches!(self.child.try_wait(), Ok(None))
|
||||
@@ -205,10 +231,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewer_count_round_trips() {
|
||||
fn viewer_join_leave_round_trip() {
|
||||
assert!(matches!(
|
||||
parse(Event::ViewerCount { active: 2, max: 4 }),
|
||||
ChildEvent::ViewerCount { active: 2, max: 4 }
|
||||
parse(Event::ViewerJoined { id: "nodeXYZ", active: 2, max: 4 }),
|
||||
ChildEvent::ViewerJoined { id, active: 2, max: 4 } if id == "nodeXYZ"
|
||||
));
|
||||
assert!(matches!(
|
||||
parse(Event::ViewerLeft { id: "nodeXYZ", active: 1, max: 4 }),
|
||||
ChildEvent::ViewerLeft { id, active: 1, max: 4 } if id == "nodeXYZ"
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user