mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
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:
1 parent
23c3798253
commit
34b23b611e
5 files changed
+845
No files matched your search
@@ -0,0 +1 @@
|
||||
target
|
||||
Generated
+509
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user