Add wsdbg, a small CLI client for the WebSocket debugger

A Rust REPL/one-shot tool for talking directly to PPSSPP's WebSocket
debugger interface (Tools/wsdbg), for scripting and manual poking during
debugging/reverse-engineering sessions.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-07-26 21:01:04 +02:00
1 parent 23c3798253
commit 34b23b611e
5 files changed
+845

No files matched your search

+1
View File
@@ -0,0 +1 @@
target
+509
View File
@@ -0,0 +1,509 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha1"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "tungstenite"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
dependencies = [
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wsdbg"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"serde_json",
"tungstenite",
]
[[package]]
name = "zerocopy"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+11
View File
@@ -0,0 +1,11 @@
[package]
authors = ["Henrik Rydgård <[email protected]>"]
edition = "2024"
name = "wsdbg"
version = "0.1.0"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
tungstenite = "0.24"
serde_json = "1.0"
anyhow = "1.0"
+50
View File
@@ -0,0 +1,50 @@
# wsdbg - WebSocket debugger CLI
A small standalone client for talking directly to PPSSPP's WebSocket debugger
interface (see `docs/WebSocketDebugger.md` in the root of the repo for the
full protocol reference and event catalog).
To install Rust and cargo, [go here](https://www.rust-lang.org/learn/get-started).
To run, with rust installed, change to this `Tools/wsdbg` directory, then:
```bash
cargo run -- <port>
```
(Or build once with `cargo build --release` and run the binary directly from
`target/release/`.)
## Usage
Start PPSSPP with the remote debugger enabled (`--debugger` on the command
line, or Settings > Tools > Developer Tools > Allow remote debugger) and note
the port it's listening on (shown in that same settings screen, and logged at
startup).
Interactive REPL - type an event name and `key=value` params, get responses
and broadcasts (log messages, stepping notifications, etc.) printed as they
arrive:
```bash
cargo run -- 12345
> game.status
-> (ticket 2) {"event":"game.status","ticket":2}
<- {
"event": "game.status",
"game": null,
"paused": false
}
```
One-shot mode - send a single event and exit after a short wait, handy for
scripting:
```bash
cargo run -- 12345 gpu.stats.get
cargo run -- 12345 cpu.setReg thread=0 name=4 value=1000
cargo run -- 12345 --raw '{"event":"cpu.evaluate","expression":"pc"}'
```
Type `:help` in the REPL for a quick reminder, `:quit` to disconnect.
+274
View File
@@ -0,0 +1,274 @@
// wsdbg - a tiny interactive client for the PPSSPP WebSocket debugger interface.
//
// See docs/WebSocketDebugger.md in the root of the ppsspp repo for the protocol reference.
//
// Examples:
// wsdbg 12345 # interactive REPL
// wsdbg 12345 game.status # one-shot: send an event, print responses, exit
// wsdbg 12345 cpu.setReg thread=0 name=0 value=42
use std::io::{self, BufRead, Write};
use std::net::TcpStream;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{Context, Result, anyhow};
use clap::Parser;
use tungstenite::client::IntoClientRequest;
use tungstenite::http::HeaderValue;
use tungstenite::{Message, WebSocket};
const SUBPROTOCOL: &str = "debugger.ppsspp.org";
#[derive(Parser, Debug)]
#[command(
name = "wsdbg",
about = "Talk directly to the PPSSPP WebSocket debugger interface",
long_about = "Talk directly to the PPSSPP WebSocket debugger interface.\n\
Run with just a port to get an interactive REPL, or add an event name\n\
(and optional key=value parameters) to fire a single request and exit.\n\
See docs/WebSocketDebugger.md for the event catalog and protocol details."
)]
struct Args {
/// Port the debugger is listening on (Settings > Tools > Developer Tools > Remote Debugger,
/// or whatever port PPSSPP printed / --debugger chose).
port: u16,
/// Event to send once and then exit, e.g. "game.status" or "cpu.stepping".
/// Omit this to start an interactive REPL instead.
event: Option<String>,
/// Extra parameters for the one-shot event, as key=value pairs.
/// Values are parsed as JSON when possible (numbers, true/false, "quoted strings"),
/// otherwise sent as a plain string.
params: Vec<String>,
/// Host to connect to.
#[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)]
wait: f64,
/// Send this raw JSON text instead of building one from event/params (one-shot mode).
#[arg(long)]
raw: Option<String>,
}
static TICKET: AtomicU64 = AtomicU64::new(1);
fn next_ticket() -> u64 {
TICKET.fetch_add(1, Ordering::Relaxed)
}
fn parse_value(raw: &str) -> serde_json::Value {
// Accept numbers, true/false, null, and "quoted strings" as JSON; anything
// else (including bare words and unescaped strings) is sent as a plain string.
serde_json::from_str::<serde_json::Value>(raw).unwrap_or_else(|_| serde_json::Value::String(raw.to_string()))
}
fn build_event_json(event: &str, params: &[String], ticket: Option<u64>) -> Result<String> {
let mut map = serde_json::Map::new();
map.insert("event".to_string(), serde_json::Value::String(event.to_string()));
if let Some(t) = ticket {
map.insert("ticket".to_string(), serde_json::Value::from(t));
}
for p in params {
let (k, v) = p
.split_once('=')
.ok_or_else(|| anyhow!("Parameter '{p}' is not in key=value form"))?;
map.insert(k.to_string(), parse_value(v));
}
Ok(serde_json::Value::Object(map).to_string())
}
fn connect(host: &str, port: u16) -> Result<WebSocket<TcpStream>> {
let addr = format!("{host}:{port}");
let stream = TcpStream::connect(&addr).with_context(|| format!("Failed to connect to {addr}"))?;
stream.set_nodelay(true).ok();
let url = format!("ws://{addr}/debugger");
let mut request = url
.into_client_request()
.with_context(|| format!("Invalid URL: ws://{addr}/debugger"))?;
request
.headers_mut()
.insert("Sec-WebSocket-Protocol", HeaderValue::from_static(SUBPROTOCOL));
let (socket, _response) = tungstenite::client(request, stream)
.map_err(|e| anyhow!("WebSocket handshake with {addr} failed: {e}"))?;
Ok(socket)
}
fn print_incoming(text: &str) {
match serde_json::from_str::<serde_json::Value>(text) {
Ok(v) => println!("\n<- {}", serde_json::to_string_pretty(&v).unwrap_or_else(|_| text.to_string())),
Err(_) => println!("\n<- {text}"),
}
}
fn is_would_block(e: &tungstenite::Error) -> bool {
matches!(
e,
tungstenite::Error::Io(io_err)
if io_err.kind() == io::ErrorKind::WouldBlock || io_err.kind() == io::ErrorKind::TimedOut
)
}
fn run_one_shot(mut socket: WebSocket<TcpStream>, json_text: String, wait_secs: f64) -> 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;
}
}
}
Ok(())
}
fn print_help() {
println!("wsdbg - connected. Type an event name and optional key=value params, e.g.:");
println!(" game.status");
println!(" cpu.setReg thread=0 name=4 value=1000");
println!("Or paste a full JSON message starting with '{{' to send it verbatim.");
println!("A numeric 'ticket' is auto-assigned to shorthand commands so you can match up responses.");
println!(":help show this message");
println!(":quit / :q disconnect and exit");
println!("See docs/WebSocketDebugger.md in the ppsspp repo for the full event catalog.");
}
fn handle_repl_line(socket: &mut WebSocket<TcpStream>, line: &str) -> Result<()> {
let json_text = if line.starts_with('{') {
line.to_string()
} else {
let mut parts = line.split_whitespace();
let event = parts.next().ok_or_else(|| anyhow!("Empty command"))?;
let rest: Vec<String> = parts.map(|s| s.to_string()).collect();
let ticket = next_ticket();
let json_text = build_event_json(event, &rest, Some(ticket))?;
println!("-> (ticket {ticket}) {json_text}");
socket.send(Message::Text(json_text.into()))?;
return Ok(());
};
println!("-> {json_text}");
socket.send(Message::Text(json_text.into()))?;
Ok(())
}
fn run_repl(mut socket: WebSocket<TcpStream>) -> Result<()> {
socket.get_ref().set_read_timeout(Some(Duration::from_millis(100)))?;
// Politely say hello, per protocol convention (see WebSocket/GameSubscriber.cpp).
let hello = build_event_json(
"version",
&[
"name=wsdbg".to_string(),
format!("version={}", env!("CARGO_PKG_VERSION")),
],
Some(next_ticket()),
)?;
socket.send(Message::Text(hello.into()))?;
print_help();
let (tx, rx) = mpsc::channel::<String>();
thread::spawn(move || {
let stdin = io::stdin();
for line in stdin.lock().lines() {
match line {
Ok(l) => {
if tx.send(l).is_err() {
break;
}
}
Err(_) => break,
}
}
});
print!("> ");
io::stdout().flush().ok();
loop {
match rx.try_recv() {
Ok(line) => {
let line = line.trim();
match line {
":quit" | ":q" | ":exit" => return Ok(()),
":help" | ":h" => print_help(),
"" => {}
_ => {
if let Err(e) = handle_repl_line(&mut socket, line) {
eprintln!("! {e}");
}
}
}
print!("> ");
io::stdout().flush().ok();
}
Err(mpsc::TryRecvError::Disconnected) => return Ok(()),
Err(mpsc::TryRecvError::Empty) => {}
}
match socket.read() {
Ok(Message::Text(text)) => {
print_incoming(&text);
print!("> ");
io::stdout().flush().ok();
}
Ok(Message::Close(frame)) => {
println!("\n[connection closed by PPSSPP: {frame:?}]");
return Ok(());
}
Ok(_) => {}
Err(ref e) if is_would_block(e) => {}
Err(tungstenite::Error::ConnectionClosed | tungstenite::Error::AlreadyClosed) => {
println!("\n[connection closed]");
return Ok(());
}
Err(e) => {
eprintln!("\n[connection error: {e}]");
return Ok(());
}
}
}
}
fn main() -> Result<()> {
let args = Args::parse();
let socket = connect(&args.host, args.port).with_context(|| {
"Could not connect. Is PPSSPP running with the WebSocket debugger enabled? \
(Settings > Tools > Developer Tools > Allow remote debugger, or launch with --debugger)"
})?;
if let Some(raw) = &args.raw {
return run_one_shot(socket, raw.clone(), args.wait);
}
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);
}
run_repl(socket)
}