diff --git a/CMakeLists.txt b/CMakeLists.txt index 2265f2b5de..9ea61d912b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1404,13 +1404,16 @@ add_library(${CoreLibName} ${CoreLinkType} Core/Debugger/DisassemblyManager.h Core/Debugger/WebSocket.cpp Core/Debugger/WebSocket.h - Core/Debugger/WebSocket/Common.h + Core/Debugger/WebSocket/CPUCoreSubscriber.cpp + Core/Debugger/WebSocket/CPUCoreSubscriber.h Core/Debugger/WebSocket/GameBroadcaster.cpp Core/Debugger/WebSocket/GameBroadcaster.h Core/Debugger/WebSocket/LogBroadcaster.cpp Core/Debugger/WebSocket/LogBroadcaster.h Core/Debugger/WebSocket/SteppingBroadcaster.cpp Core/Debugger/WebSocket/SteppingBroadcaster.h + Core/Debugger/WebSocket/WebSocketUtils.cpp + Core/Debugger/WebSocket/WebSocketUtils.h Core/Dialog/PSPDialog.cpp Core/Dialog/PSPDialog.h Core/Dialog/PSPGamedataInstallDialog.cpp diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index e713cd5057..c102fd2cb7 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -185,9 +185,11 @@ + + @@ -537,7 +539,8 @@ - + + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 3449fa0158..3a8a72a82e 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -707,6 +707,12 @@ Debugger\WebSocket + + Debugger\WebSocket + + + Debugger\WebSocket + @@ -1301,7 +1307,10 @@ Debugger\WebSocket - + + Debugger\WebSocket + + Debugger\WebSocket diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp index 770772a1de..2e3e540843 100644 --- a/Core/Debugger/WebSocket.cpp +++ b/Core/Debugger/WebSocket.cpp @@ -16,11 +16,7 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include "Core/Debugger/WebSocket.h" -#include "Core/Debugger/WebSocket/Common.h" - -#include "Core/Debugger/WebSocket/GameBroadcaster.h" -#include "Core/Debugger/WebSocket/LogBroadcaster.h" -#include "Core/Debugger/WebSocket/SteppingBroadcaster.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" // This WebSocket (connected through the same port as disc sharing) allows API/debugger access to PPSSPP. // Currently, the only subprotocol "debugger.ppsspp.org" uses a simple JSON based interface. @@ -40,14 +36,17 @@ // - "level": Integer severity level. (1 = NOTICE, 2 = ERROR, 3 = WARN, 4 = INFO, 5 = DEBUG, 6 = VERBOSE) // - "ticket": Optional, present if in response to an event with a "ticket" field, simply repeats that value. -// TODO: Just for now, testing... -static void WebSocketTestEvent(net::WebSocketServer *ws, const JsonGet &data) { - ws->Send(DebuggerErrorEvent("Test message", LogTypes::LNOTICE, data)); -} +#include "Core/Debugger/WebSocket/GameBroadcaster.h" +#include "Core/Debugger/WebSocket/LogBroadcaster.h" +#include "Core/Debugger/WebSocket/SteppingBroadcaster.h" -typedef void (*DebuggerEventHandler)(net::WebSocketServer *ws, const JsonGet &data); +#include "Core/Debugger/WebSocket/CPUCoreSubscriber.h" + +typedef void (*DebuggerEventHandler)(DebuggerRequest &req); static const std::unordered_map debuggerEvents({ - {"test", &WebSocketTestEvent}, + {"cpu.getAllRegs", &WebSocketCPUGetAllRegs}, + {"cpu.getReg", &WebSocketCPUGetReg}, + {"cpu.setReg", &WebSocketCPUSetReg}, }); void HandleDebuggerRequest(const http::Request &request) { @@ -73,11 +72,14 @@ void HandleDebuggerRequest(const http::Request &request) { return; } + DebuggerRequest req(event, ws, root); + auto eventFunc = debuggerEvents.find(event); if (eventFunc != debuggerEvents.end()) { - eventFunc->second(ws, root); + eventFunc->second(req); + req.Finish(); } else { - ws->Send(DebuggerErrorEvent("Bad message: unknown event", LogTypes::LERROR, root)); + req.Fail("Bad message: unknown event"); } }); ws->SetBinaryHandler([&](const std::vector &d) { diff --git a/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp b/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp new file mode 100644 index 0000000000..c3ef6e6ff5 --- /dev/null +++ b/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp @@ -0,0 +1,224 @@ +// 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/. + +#include "Common/StringUtils.h" +#include "Core/Core.h" +#include "Core/Debugger/WebSocket/CPUCoreSubscriber.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" +#include "Core/MIPS/MIPS.h" +#include "Core/MIPS/MIPSDebugInterface.h" + +static std::string RegValueAsFloat(uint32_t u) { + union { + uint32_t u; + float f; + } bits = { u }; + return StringFromFormat("%f", bits.f); +} + +void WebSocketCPUGetAllRegs(DebuggerRequest &req) { + JsonWriter &json = req.Respond(); + + json.pushArray("categories"); + for (int c = 0; c < currentDebugMIPS->GetNumCategories(); ++c) { + json.pushDict(); + json.writeInt("id", c); + json.writeString("name", currentDebugMIPS->GetCategoryName(c)); + + int total = currentDebugMIPS->GetNumRegsInCategory(c); + + json.pushArray("names"); + for (int r = 0; r < total; ++r) + json.writeString(currentDebugMIPS->GetRegName(c, r)); + if (c == 0) { + json.writeString("pc"); + json.writeString("hi"); + json.writeString("lo"); + } + json.pop(); + + json.pushArray("intValues"); + // Writing as floating point to avoid negatives. Actually double, so safe. + for (int r = 0; r < total; ++r) + json.writeFloat(currentDebugMIPS->GetRegValue(c, r)); + if (c == 0) { + json.writeFloat(currentDebugMIPS->GetPC()); + json.writeFloat(currentDebugMIPS->GetHi()); + json.writeFloat(currentDebugMIPS->GetLo()); + } + json.pop(); + + json.pushArray("floatValues"); + // Note: String so it can have Infinity and NaN. + for (int r = 0; r < total; ++r) + json.writeString(RegValueAsFloat(currentDebugMIPS->GetRegValue(c, r))); + if (c == 0) { + json.writeString(RegValueAsFloat(currentDebugMIPS->GetPC())); + json.writeString(RegValueAsFloat(currentDebugMIPS->GetHi())); + json.writeString(RegValueAsFloat(currentDebugMIPS->GetLo())); + } + json.pop(); + + json.pop(); + } + json.pop(); +} + +enum class DebuggerRegType { + INVALID, + NORMAL, + PC, + HI, + LO, +}; + +static DebuggerRegType ValidateRegName(DebuggerRequest &req, const std::string &name, int *cat, int *reg) { + if (name == "pc") { + *cat = 0; + *reg = 32; + return DebuggerRegType::PC; + } + if (name == "hi") { + *cat = 0; + *reg = 33; + return DebuggerRegType::HI; + } + if (name == "lo") { + *cat = 0; + *reg = 34; + return DebuggerRegType::LO; + } + + for (int c = 0; c < currentDebugMIPS->GetNumCategories(); ++c) { + int total = currentDebugMIPS->GetNumRegsInCategory(c); + for (int r = 0; r < total; ++r) { + if (name == currentDebugMIPS->GetRegName(c, r)) { + *cat = c; + *reg = r; + return DebuggerRegType::NORMAL; + } + } + } + + req.Fail("Invalid 'name' parameter"); + return DebuggerRegType::INVALID; +} + +static DebuggerRegType ValidateCatReg(DebuggerRequest &req, int *cat, int *reg) { + const char *name = req.data.getString("name", nullptr); + if (name) + return ValidateRegName(req, name, cat, reg); + + *cat = req.data.getInt("category", -1); + *reg = req.data.getInt("register", -1); + + if (*cat < 0 || *cat >= currentDebugMIPS->GetNumCategories()) { + req.Fail("Invalid 'category' parameter"); + return DebuggerRegType::INVALID; + } + + // TODO: We fake it for GPR... not sure yet if this is a good thing. + if (*cat == 0) { + // Intentionally retains the reg value. + if (*reg == 32) + return DebuggerRegType::PC; + if (*reg == 33) + return DebuggerRegType::HI; + if (*reg == 34) + return DebuggerRegType::LO; + } + + if (*reg < 0 || *reg >= currentDebugMIPS->GetNumRegsInCategory(*cat)) { + req.Fail("Invalid 'register' parameter"); + return DebuggerRegType::INVALID; + } + + return DebuggerRegType::NORMAL; +} + +void WebSocketCPUGetReg(DebuggerRequest &req) { + int cat, reg; + uint32_t val; + switch (ValidateCatReg(req, &cat, ®)) { + case DebuggerRegType::NORMAL: + val = currentDebugMIPS->GetRegValue(cat, reg); + break; + + case DebuggerRegType::PC: + val = currentDebugMIPS->GetPC(); + break; + case DebuggerRegType::HI: + val = currentDebugMIPS->GetHi(); + break; + case DebuggerRegType::LO: + val = currentDebugMIPS->GetLo(); + break; + + case DebuggerRegType::INVALID: + // Error response already sent. + return; + } + + JsonWriter &json = req.Respond(); + json.writeInt("category", cat); + json.writeInt("register", reg); + json.writeFloat("intValue", val); + json.writeString("floatValue", RegValueAsFloat(val)); +} + +void WebSocketCPUSetReg(DebuggerRequest &req) { + if (!currentDebugMIPS->isAlive()) { + return req.Fail("CPU not started"); + } + if (!Core_IsStepping()) { + return req.Fail("CPU currently running (cpu.interrupt first)"); + } + + uint32_t val; + if (!req.ParamU32OrFloatBits("value", &val)) { + // Already sent error. + return; + } + + int cat, reg; + switch (ValidateCatReg(req, &cat, ®)) { + case DebuggerRegType::NORMAL: + currentDebugMIPS->SetRegValue(cat, reg, val); + break; + + case DebuggerRegType::PC: + currentDebugMIPS->SetPC(val); + break; + case DebuggerRegType::HI: + currentDebugMIPS->SetHi(val); + break; + case DebuggerRegType::LO: + currentDebugMIPS->SetLo(val); + break; + + case DebuggerRegType::INVALID: + // Error response already sent. + return; + } + + JsonWriter &json = req.Respond(); + // Repeat it back just to avoid confusion on how it parsed. + json.writeInt("category", cat); + json.writeInt("register", reg); + json.writeFloat("intValue", val); + json.writeString("floatValue", RegValueAsFloat(val)); +} diff --git a/Core/Debugger/WebSocket/CPUCoreSubscriber.h b/Core/Debugger/WebSocket/CPUCoreSubscriber.h new file mode 100644 index 0000000000..e24b16398f --- /dev/null +++ b/Core/Debugger/WebSocket/CPUCoreSubscriber.h @@ -0,0 +1,24 @@ +// 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 + +struct DebuggerRequest; + +void WebSocketCPUGetAllRegs(DebuggerRequest &req); +void WebSocketCPUGetReg(DebuggerRequest &req); +void WebSocketCPUSetReg(DebuggerRequest &req); diff --git a/Core/Debugger/WebSocket/GameBroadcaster.cpp b/Core/Debugger/WebSocket/GameBroadcaster.cpp index 86ee9df777..e1a9d2ae4e 100644 --- a/Core/Debugger/WebSocket/GameBroadcaster.cpp +++ b/Core/Debugger/WebSocket/GameBroadcaster.cpp @@ -15,8 +15,8 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. -#include "Core/Debugger/WebSocket/Common.h" #include "Core/Debugger/WebSocket/GameBroadcaster.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" #include "Core/System.h" void GameBroadcaster::Broadcast(net::WebSocketServer *ws) { diff --git a/Core/Debugger/WebSocket/LogBroadcaster.cpp b/Core/Debugger/WebSocket/LogBroadcaster.cpp index 3f23751d7c..2474294e0a 100644 --- a/Core/Debugger/WebSocket/LogBroadcaster.cpp +++ b/Core/Debugger/WebSocket/LogBroadcaster.cpp @@ -18,8 +18,8 @@ #include #include #include "Common/LogManager.h" -#include "Core/Debugger/WebSocket/Common.h" #include "Core/Debugger/WebSocket/LogBroadcaster.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" class DebuggerLogListener : public LogListener { public: diff --git a/Core/Debugger/WebSocket/SteppingBroadcaster.cpp b/Core/Debugger/WebSocket/SteppingBroadcaster.cpp index 60405b4c48..ca44bc06b4 100644 --- a/Core/Debugger/WebSocket/SteppingBroadcaster.cpp +++ b/Core/Debugger/WebSocket/SteppingBroadcaster.cpp @@ -16,8 +16,8 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include "Core/Core.h" -#include "Core/Debugger/WebSocket/Common.h" #include "Core/Debugger/WebSocket/SteppingBroadcaster.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" #include "Core/System.h" void SteppingBroadcaster::Broadcast(net::WebSocketServer *ws) { diff --git a/Core/Debugger/WebSocket/WebSocketUtils.cpp b/Core/Debugger/WebSocket/WebSocketUtils.cpp new file mode 100644 index 0000000000..3293e5da22 --- /dev/null +++ b/Core/Debugger/WebSocket/WebSocketUtils.cpp @@ -0,0 +1,106 @@ +// 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/. + +#include "Common/StringUtils.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" + +JsonWriter &DebuggerRequest::Respond() { + writer_.begin(); + writer_.writeString("event", name); + DebuggerJsonAddTicket(writer_, data); + + responseBegun_ = true; + return writer_; +} + +void DebuggerRequest::Finish() { + if (responseBegun_ && !responseSent_) { + writer_.end(); + ws->Send(writer_.str()); + responseBegun_ = false; + responseSent_ = true; + } +} + +static bool U32FromString(const char *str, uint32_t *out, bool allowFloat) { + if (TryParse(str, out)) + return true; + + // Now let's try signed (the above parses only positive.) + if (str[0] == '-' && TryParse(&str[1], out)) { + *out = static_cast(-static_cast(*out)); + return true; + } + + // We have to try float last because we use float bits. + union { + uint32_t u; + float f; + } bits; + if (allowFloat && TryParse(str, &bits.f)) { + *out = bits.u; + return true; + } + + return false; +} + +bool DebuggerRequest::ParamU32(const char *name, uint32_t *out) { + const JsonNode *node = data.get(name); + if (!node) { + Fail(StringFromFormat("Missing '%s' parameter", name)); + return false; + } + + // TODO: For now, only supporting strings. Switch to gason? + // Otherwise we get overflow (signed integer parsing.) + if (node->value.getTag() != JSON_STRING) { + Fail(StringFromFormat("Invalid '%s' parameter type", name)); + return false; + } + + if (U32FromString(node->value.toString(), out, false)) + return true; + + Fail(StringFromFormat("Could not parse '%s' parameter", name)); + return false; +} + +bool DebuggerRequest::ParamU32OrFloatBits(const char *name, uint32_t *out) { + const JsonNode *node = data.get(name); + if (!node) { + Fail(StringFromFormat("Missing '%s' parameter", name)); + return false; + } + + // TODO: For now, only supporting strings and floats. Switch to gason? + // Otherwise we get overflow (signed integer parsing.) + if (node->value.getTag() == JSON_NUMBER) { + Fail(StringFromFormat("Could not parse '%s' parameter: outside 32 bit range (use string for float)", name)); + return false; + } + if (node->value.getTag() != JSON_STRING) { + Fail(StringFromFormat("Invalid '%s' parameter type", name)); + return false; + } + + if (U32FromString(node->value.toString(), out, true)) + return true; + + Fail(StringFromFormat("Could not parse '%s' parameter", name)); + return false; +} diff --git a/Core/Debugger/WebSocket/Common.h b/Core/Debugger/WebSocket/WebSocketUtils.h similarity index 66% rename from Core/Debugger/WebSocket/Common.h rename to Core/Debugger/WebSocket/WebSocketUtils.h index bdca223656..19894939bb 100644 --- a/Core/Debugger/WebSocket/Common.h +++ b/Core/Debugger/WebSocket/WebSocketUtils.h @@ -17,12 +17,19 @@ #pragma once +#include #include #include "json/json_reader.h" #include "json/json_writer.h" #include "net/websocket_server.h" #include "Common/Log.h" +static inline void DebuggerJsonAddTicket(JsonWriter &writer, const JsonGet &data) { + const JsonNode *value = data.get("ticket"); + if (value) + writer.writeRaw("ticket", json_stringify(value)); +} + struct DebuggerErrorEvent { DebuggerErrorEvent(const std::string m, LogTypes::LOG_LEVELS l, const JsonGet data = JsonValue(JSON_NULL)) : message(m), level(l) { @@ -51,3 +58,29 @@ struct DebuggerErrorEvent { return j.str(); } }; + +struct DebuggerRequest { + DebuggerRequest(const char *n, net::WebSocketServer *w, const JsonGet &d) + : name(n), ws(w), data(d) { + } + + const char *name; + net::WebSocketServer *ws; + const JsonGet data; + + void Fail(const std::string &message) { + ws->Send(DebuggerErrorEvent(message, LogTypes::LERROR, data)); + responseSent_ = true; + } + + bool ParamU32(const char *name, uint32_t *out); + bool ParamU32OrFloatBits(const char *name, uint32_t *out); + + JsonWriter &Respond(); + void Finish(); + +private: + JsonWriter writer_; + bool responseBegun_ = false; + bool responseSent_ = false; +}; diff --git a/android/jni/Android.mk b/android/jni/Android.mk index d21c854849..b5b8dda1d2 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -301,9 +301,11 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/Debugger/Breakpoints.cpp \ $(SRC)/Core/Debugger/SymbolMap.cpp \ $(SRC)/Core/Debugger/WebSocket.cpp \ + $(SRC)/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/GameBroadcaster.cpp \ $(SRC)/Core/Debugger/WebSocket/LogBroadcaster.cpp \ $(SRC)/Core/Debugger/WebSocket/SteppingBroadcaster.cpp \ + $(SRC)/Core/Debugger/WebSocket/WebSocketUtils.cpp \ $(SRC)/Core/Dialog/PSPDialog.cpp \ $(SRC)/Core/Dialog/PSPGamedataInstallDialog.cpp \ $(SRC)/Core/Dialog/PSPMsgDialog.cpp \