Files
ppsspp/Core/Debugger/WebSocket/WebSocketUtils.h
T
Henrik RydgårdandClaude Opus 5.5 7625f7529a Debugger: Say in "deferred" when a ticketed reply follows
input.buttons.press, gpu.stats.get, gpu.record.dump and the game.state.*
operations with wait answer later with a message carrying the request's
ticket. Their deferred acknowledgement carried that ticket too, and wsdbg
took it for the answer, so a waited save that failed exited 0 and --sync
moved on before it was done.

Handlers that answer this way now call ReplyLater(), and the deferred event
says "replyFollows": true. wsdbg keeps waiting for the reply in one-shot
mode, under --sync and for :nowait lines. An error reply or a missing one
to a :nowait line now fails the run, a raw line can't reuse a ticket that
is still pending, and auto-assigned tickets skip past ones a raw line chose.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 09:34:27 +02:00

173 lines
5.3 KiB
C++

// Copyright (c) 2018- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#pragma once
#include "ppsspp_config.h"
#include <string>
#include "Common/Log.h"
#include "Common/Data/Format/JSONReader.h"
#include "Common/Data/Format/JSONWriter.h"
#include "Common/Net/WebsocketServer.h"
#if PPSSPP_PLATFORM(UWP)
// Enum name overlapped with UWP macro, quick hack to disable it
#undef OPTIONAL
#endif
using namespace json;
struct WebSocketClientInfo {
WebSocketClientInfo() = default;
std::string name;
std::string version;
std::map<std::string, bool> disallowed;
// Whether to send a "deferred" event for requests that only finish later. Off by default,
// and it has to stay that way: an extra message would break a client that correlates purely
// by ticket, or one that waits for the bare event name a resume or a button press answers
// with. See client.config.set.
bool acknowledgeDeferred = false;
};
// Sent, if the client asked for it, when a handler accepted a request but arranged to finish it
// later - a resume, a step, a button held for some frames. Deliberately not named after the
// request: the event that reports the actual outcome uses that name, and for input.buttons.press
// it carries the same ticket too, so anything less distinct would be impossible to tell apart.
// replyFollows says that outcome is a reply carrying the request's ticket (see ReplyLater()).
struct DebuggerDeferredEvent {
DebuggerDeferredEvent(const char *n, const JsonGet data, bool replyFollows) : name(n), replyFollows(replyFollows) {
const JsonNode *value = data ? data.get("ticket") : nullptr;
if (value)
ticketRaw = json_stringify(value);
}
const char *name;
bool replyFollows;
std::string ticketRaw;
operator std::string() const {
JsonWriter j;
j.begin();
j.writeString("event", "deferred");
j.writeString("for", name);
if (replyFollows) {
j.writeBool("replyFollows", true);
}
if (!ticketRaw.empty()) {
j.writeRaw("ticket", ticketRaw);
}
j.end();
return j.str();
}
};
struct DebuggerErrorEvent {
DebuggerErrorEvent(const std::string m, LogLevel l, const JsonGet data = JsonValue(JSON_NULL))
: message(m), level(l) {
// Need to format right away, before it's out of scope.
if (data) {
const JsonNode *value = data.get("ticket");
if (value)
ticketRaw = json_stringify(value);
}
}
std::string message;
LogLevel level;
std::string ticketRaw;
operator std::string() const {
JsonWriter j;
j.begin();
j.writeString("event", "error");
j.writeString("message", message);
j.writeInt("level", (int)level);
if (!ticketRaw.empty()) {
j.writeRaw("ticket", ticketRaw);
}
j.end();
return j.str();
}
};
enum class DebuggerParamType {
REQUIRED,
OPTIONAL,
REQUIRED_LOOSE,
OPTIONAL_LOOSE,
};
struct DebuggerRequest {
DebuggerRequest(const char *n, net::WebSocketServer *w, const JsonGet &d, WebSocketClientInfo *client_info)
: name(n), ws(w), data(d), client(client_info) {
}
const char *name;
net::WebSocketServer *ws;
const JsonGet data;
WebSocketClientInfo *client;
void Fail(const std::string &message) {
ws->Send(DebuggerErrorEvent(message, LogLevel::LERROR, data));
responseSent_ = true;
}
bool HasParam(const char *name, bool ignoreNull = false);
bool ParamU32(const char *name, uint32_t *out, bool allowFloatBits = false, DebuggerParamType type = DebuggerParamType::REQUIRED);
// For quantities that don't fit in 32 bits, like emulated microseconds - JSON numbers are
// doubles anyway, which is exact well past any plausible session length.
bool ParamF64(const char *name, double *out, DebuggerParamType type = DebuggerParamType::REQUIRED);
bool ParamBool(const char *name, bool *out, DebuggerParamType type = DebuggerParamType::REQUIRED);
bool ParamString(const char *name, std::string *out, DebuggerParamType type = DebuggerParamType::REQUIRED);
JsonWriter &Respond();
void Flush();
bool Finish();
// For a handler that answers later with a message carrying the request's ticket, rather than
// a ticketless broadcast. The deferred event then tells the client to keep waiting for it.
void ReplyLater() {
replyLater_ = true;
}
bool RepliesLater() const {
return replyLater_;
}
private:
JsonWriter writer_;
bool responseBegun_ = false;
bool responseSent_ = false;
bool responsePartial_ = false;
bool replyLater_ = false;
};
class DebuggerSubscriber {
public:
virtual ~DebuggerSubscriber() {}
// Subscribers can also broadcast if they have simple cases to.
virtual void Broadcast(net::WebSocketServer *ws) {}
};
typedef std::function<void(DebuggerRequest &req)> DebuggerEventHandler;
typedef std::unordered_map<std::string_view, DebuggerEventHandler> DebuggerEventHandlerMap;
uint32_t RoundMemAddressUp(uint32_t addr);