From d4e2382e1acc736b62c51a522a127c3e1c8c9fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 17 Aug 2026 20:05:15 +0200 Subject: [PATCH] wsdbg: one-shot mode waits for the reply, not a fixed sleep One-shot mode drained the socket for --wait seconds (default 2) and exited, whatever arrived. So every invocation cost two seconds regardless of how fast the answer came, and a slower one got cut off with no indication that it had been. Scripts calling wsdbg in a loop paid that per call. Now that every request is answered, the reply can be matched by ticket the same way the REPL does: return as soon as it arrives, and treat --wait (now 10s) as an upper bound rather than a delay. A cpu.status that used to take 2.0s takes 0.03s. Not getting an answer within the bound is reported and exits non-zero instead of passing silently. --wait-all keeps the old behaviour, for when the point is to watch broadcasts (log lines, gpu.stats.feed) rather than ask a question. --raw without a ticket of its own falls back to it too, since there's nothing to match. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- Tools/wsdbg/README.md | 11 +++++-- Tools/wsdbg/src/main.rs | 67 ++++++++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/Tools/wsdbg/README.md b/Tools/wsdbg/README.md index 2140721342..d5a936c04d 100644 --- a/Tools/wsdbg/README.md +++ b/Tools/wsdbg/README.md @@ -38,8 +38,8 @@ cargo run -- 12345 } ``` -One-shot mode - send a single event and exit after a short wait, handy for -scripting: +One-shot mode - send a single event and exit as soon as its reply arrives, handy +for scripting: ```bash cargo run -- 12345 gpu.stats.get @@ -47,6 +47,13 @@ cargo run -- 12345 cpu.setReg thread=0 name=4 value=1000 cargo run -- 12345 --raw '{"event":"cpu.evaluate","expression":"pc"}' ``` +The reply is matched by ticket, so this returns in milliseconds rather than +padding every invocation with a fixed sleep. `--wait` (default 10s) is only the +upper bound before it gives up and exits non-zero. Pass `--wait-all` to go back +to "print everything that arrives for `--wait` seconds", which is what you want +when watching broadcasts (log lines, `gpu.stats.feed`) rather than asking a +question. + Type `:help` in the REPL for a quick reminder, `:quit` to disconnect. ## Scripting a multi-step sequence diff --git a/Tools/wsdbg/src/main.rs b/Tools/wsdbg/src/main.rs index 325778e839..3667d48c22 100644 --- a/Tools/wsdbg/src/main.rs +++ b/Tools/wsdbg/src/main.rs @@ -54,10 +54,19 @@ struct Args { #[arg(long, default_value = "127.0.0.1")] host: String, - /// How long to wait for responses/broadcasts in one-shot mode, in seconds. - #[arg(long, default_value_t = 2.0)] + /// One-shot mode: how long to wait for the response before giving up, in seconds. Normally + /// it returns as soon as the reply for this request arrives (matched by ticket), so this is + /// only an upper bound, not a fixed delay. Pass --wait-all to go back to "print everything + /// that arrives for N seconds". + #[arg(long, default_value_t = 10.0)] wait: f64, + /// One-shot mode: keep reading for the whole --wait period instead of stopping at the + /// response. Useful for watching broadcasts (log lines, gpu.stats.feed) rather than asking + /// a question. + #[arg(long)] + wait_all: bool, + /// Send this raw JSON text instead of building one from event/params (one-shot mode). #[arg(long)] raw: Option, @@ -322,29 +331,39 @@ fn is_would_block(e: &tungstenite::Error) -> bool { ) } -fn run_one_shot(mut socket: WebSocket, json_text: String, wait_secs: f64) -> Result<()> { +// Returns false if the request went unanswered within the timeout, so one-shot mode can exit +// non-zero rather than looking successful after having printed nothing useful. +// +// Every request is answered (PPSSPP acknowledges even the ones whose result arrives later), so +// waiting for this request's ticket beats sleeping for a fixed --wait: a quick question returns +// immediately instead of padding every invocation, and a slow one isn't cut off early. +fn run_one_shot( + mut socket: WebSocket, + json_text: String, + ticket: Option, + wait_secs: f64, + wait_all: bool, +) -> Result { println!("-> {json_text}"); socket.send(Message::Text(json_text.into()))?; socket.get_ref().set_read_timeout(Some(Duration::from_millis(50)))?; let deadline = Instant::now() + Duration::from_secs_f64(wait_secs.max(0.0)); - while Instant::now() < deadline { - match socket.read() { - Ok(Message::Text(text)) => print_incoming(&text), - Ok(Message::Close(frame)) => { - println!("[connection closed: {frame:?}]"); - break; - } - Ok(_) => {} - Err(ref e) if is_would_block(e) => {} - Err(tungstenite::Error::ConnectionClosed | tungstenite::Error::AlreadyClosed) => break, - Err(e) => { - eprintln!("[connection error: {e}]"); - break; - } - } + + // Without a ticket (--raw with none supplied) there's nothing to match, so fall back to + // draining until the deadline, same as --wait-all. + let Some(ticket) = ticket.filter(|_| !wait_all) else { + pump_until(&mut socket, deadline, |_| false); + return Ok(true); + }; + + let answered = pump_until(&mut socket, deadline, |v| { + v.get("ticket").and_then(|t| t.as_u64()) == Some(ticket) + }); + if !answered { + eprintln!("! timed out after {wait_secs}s waiting for a reply (ticket {ticket})"); } - Ok(()) + Ok(answered) } fn print_help() { @@ -746,13 +765,19 @@ fn main() -> Result<()> { })?; if let Some(raw) = &args.raw { - return run_one_shot(socket, raw.clone(), args.wait); + // Recover the ticket if the caller supplied one; there's nothing to wait on otherwise. + let ticket = serde_json::from_str::(raw) + .ok() + .and_then(|v| v.get("ticket").and_then(|t| t.as_u64())); + let ok = run_one_shot(socket, raw.clone(), ticket, args.wait, args.wait_all)?; + return if ok { Ok(()) } else { std::process::exit(1) }; } if let Some(event) = &args.event { let ticket = next_ticket(); let json_text = build_event_json(event, &args.params, Some(ticket))?; - return run_one_shot(socket, json_text, args.wait); + let ok = run_one_shot(socket, json_text, Some(ticket), args.wait, args.wait_all)?; + return if ok { Ok(()) } else { std::process::exit(1) }; } // Non-zero exit when something in the script failed, so a caller doesn't have to grep output