mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
tooling: unify dump/screenshot surface, bound TCP sends, fix beetle input + big-dump truncation
- debug_server.c: dump_ram = alias of read_ram (one request, one response, up to full 2 MB); screenshot = screenshot_file (24-bit BMP, optional "path", default psx_screenshot.bmp); vram_peek cap 64->128; read_ram hex encode via nibble table (was 2M snprintf calls stalling the main thread); send_all_blocking bounded (1 MB chunks, SO_SNDTIMEO 2s, heartbeats the starvation watchdog per chunk) and drops the client on send failure instead of letting the watchdog kill the runtime; handle_quit tags exit origin. - beetle_debug_server.c: read_ram cap 64 KB->2 MB via heap+send_raw; dump_ram/screenshot aliases; screenshot w/h->width/height parity; set_input now holds until clear_input (frames param optional) matching psx-runtime semantics — the old default expired after 1 frame and the off-by-one in the getter cleared the override before it was ever read, so scripted beetle navigation silently did nothing; wtrace_dump response heap-built + send_raw (per-entry send_fmt dropped chunks on multi-MB responses, producing broken JSON). - crash_trace: new psx_crash_trace_set_exit_origin(); report gains exit_origin (tcp_quit / sdl_window_close / unknown). - debug_client.py: O(n) string-aware brace parser (old per-byte loop was O(n^2) — minutes on a 2 MB response, which starved the server's send and got the runtime killed); generic key=value passthrough for any server command without a bespoke CLI mapping. - TCP_COMMANDS.md updated for all of the above. Verified: 2 MB read_ram round-trip 0.75 s; scripted beetle navigation reaches the memcard file-select; wtrace_dump returns valid JSON at 21 MB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+55
-23
@@ -59,6 +59,7 @@ REPL commands:
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import argparse
|
||||
@@ -89,35 +90,54 @@ def connect(host=DEFAULT_HOST, port=NATIVE_PORT, timeout=10.0):
|
||||
def send_cmd(sock, cmd_dict):
|
||||
line = json.dumps(cmd_dict) + "\n"
|
||||
sock.sendall(line.encode())
|
||||
buf = b""
|
||||
# Server may emit one large JSON object across many TCP chunks, with raw
|
||||
# newlines inside arrays. Track brace/bracket balance (string-aware) and
|
||||
# parse once balance returns to zero.
|
||||
depth_curly = 0
|
||||
depth_sq = 0
|
||||
# One complete JSON object = one response. Track brace balance
|
||||
# (string-aware) with a persistent state machine and fast-skips so a
|
||||
# multi-megabyte response costs O(n), not O(n^2) — the old per-byte
|
||||
# loop took minutes on a 2 MB read_ram, the server's blocking send
|
||||
# backed up, and the runtime's starvation watchdog killed the process.
|
||||
buf = bytearray()
|
||||
pos = 0
|
||||
depth = 0
|
||||
in_str = False
|
||||
esc = False
|
||||
started = False
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
chunk = sock.recv(1 << 20)
|
||||
if not chunk:
|
||||
break
|
||||
for b in chunk:
|
||||
buf += bytes([b])
|
||||
c = chr(b)
|
||||
buf.extend(chunk)
|
||||
n = len(buf)
|
||||
while pos < n:
|
||||
if in_str:
|
||||
if esc: esc = False
|
||||
elif c == "\\": esc = True
|
||||
elif c == '"': in_str = False
|
||||
if esc:
|
||||
esc = False
|
||||
pos += 1
|
||||
continue
|
||||
# Fast-skip to the next quote or backslash (C-speed find;
|
||||
# hex payloads are one giant string with neither).
|
||||
q = buf.find(b'"', pos)
|
||||
bs = buf.find(b"\\", pos)
|
||||
if q == -1 and bs == -1:
|
||||
pos = n
|
||||
break
|
||||
if bs != -1 and (q == -1 or bs < q):
|
||||
esc = True
|
||||
pos = bs + 1
|
||||
continue
|
||||
in_str = False
|
||||
pos = q + 1
|
||||
continue
|
||||
if c == '"': in_str = True
|
||||
elif c == "{": depth_curly += 1; started = True
|
||||
elif c == "}": depth_curly -= 1
|
||||
elif c == "[": depth_sq += 1
|
||||
elif c == "]": depth_sq -= 1
|
||||
if started and depth_curly == 0 and depth_sq == 0:
|
||||
# Message complete.
|
||||
return json.loads(buf.decode().strip())
|
||||
c = buf[pos]
|
||||
if c == 0x22: # '"'
|
||||
in_str = True
|
||||
elif c == 0x7B: # '{'
|
||||
depth += 1
|
||||
started = True
|
||||
elif c == 0x7D: # '}'
|
||||
depth -= 1
|
||||
if started and depth == 0:
|
||||
return json.loads(buf[:pos + 1].decode())
|
||||
pos += 1
|
||||
return json.loads(buf.decode().strip())
|
||||
|
||||
|
||||
@@ -378,8 +398,20 @@ def build_cmd(args):
|
||||
d["pc_hi"] = args[3]
|
||||
return d, pretty_json
|
||||
else:
|
||||
# Pass through as raw command
|
||||
return {"cmd": cmd}, pretty_json
|
||||
# Pass through as raw command. Extra args of the form key=value
|
||||
# become JSON fields (ints when numeric, else strings), so every
|
||||
# server command is reachable without a bespoke CLI mapping.
|
||||
d = {"cmd": cmd}
|
||||
for a in args[1:]:
|
||||
key, sep, val = a.partition("=")
|
||||
if not sep:
|
||||
return None, lambda _, a=a: (
|
||||
f"Unrecognized arg '{a}' for raw command; use key=value")
|
||||
if re.fullmatch(r"-?\d+", val):
|
||||
d[key] = int(val)
|
||||
else:
|
||||
d[key] = val
|
||||
return d, pretty_json
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user