mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Headless: forward the program's stdout/stderr to ours by default
Running a homebrew and seeing what it prints is the most basic thing PPSSPPHeadless does, but sceIoWrite() to fd 1 and 2 only ever went into the log, so it took -l to see any of it - which turns on every log channel at debug level and buries the output. The debug-output listener now gets a channel, and headless writes StdOut and StdErr straight through to the host's, unmodified. With no listener (the normal app) the old sanitized Log::Printf line is unchanged. pspautotests writes exclusively to the "emulator:" devctl channel, so nothing there moves; --compare and --bench suppress the forwarding along with the debug channel, keeping test console output as it was. Also fixes a potential one-byte OOB read in the same path when an unmapped address clamps validSize to 0 with size > 0. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
d8a1830587
commit
1dbd1afc20
5 files changed
+77
-20
No files matched your search
+11
-3
@@ -245,22 +245,30 @@ static MIPSExceptionInfo g_exceptionInfo;
|
||||
// This is called on EmuThread before RunLoop.
|
||||
static bool Core_ProcessStepping(MIPSDebugInterface *cpu);
|
||||
|
||||
static std::function<void(std::string_view)> g_debugOutputListener;
|
||||
static std::function<void(DebugOutputChannel, std::string_view)> g_debugOutputListener;
|
||||
static std::function<void(const DebugScreenshotDesc &)> g_debugScreenshotListener;
|
||||
|
||||
void Core_RegisterDebugOutputListeners(std::function<void(std::string_view)> listener, std::function<void(const DebugScreenshotDesc &)> screenshotListener) {
|
||||
void Core_RegisterDebugOutputListeners(std::function<void(DebugOutputChannel, std::string_view)> listener, std::function<void(const DebugScreenshotDesc &)> screenshotListener) {
|
||||
g_debugOutputListener = std::move(listener);
|
||||
g_debugScreenshotListener = std::move(screenshotListener);
|
||||
}
|
||||
|
||||
void Core_SendDebugOutput(LogLevel level, std::string_view string) {
|
||||
if (g_debugOutputListener) {
|
||||
g_debugOutputListener(string);
|
||||
g_debugOutputListener(DebugOutputChannel::Debug, string);
|
||||
} else {
|
||||
GENERIC_LOG(Log::sceIo, level, "%.*s", STR_VIEW(string));
|
||||
}
|
||||
}
|
||||
|
||||
bool Core_SendHostOutput(DebugOutputChannel channel, std::string_view string) {
|
||||
if (!g_debugOutputListener) {
|
||||
return false;
|
||||
}
|
||||
g_debugOutputListener(channel, string);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Core_SendDebugScreenshot(const DebugScreenshotDesc &desc) {
|
||||
if (g_debugScreenshotListener) {
|
||||
g_debugScreenshotListener(desc);
|
||||
|
||||
+11
-1
@@ -301,9 +301,19 @@ struct DebugScreenshotDesc {
|
||||
u32 height;
|
||||
GEBufferFormat format;
|
||||
};
|
||||
// Which of the host's output streams a piece of emulated output belongs on.
|
||||
enum class DebugOutputChannel {
|
||||
Debug, // The "emulator:" devctl channel, plus our own messages about the run. This is what pspautotests uses.
|
||||
StdOut, // sceIoWrite() to fd 1, and to a tty device.
|
||||
StdErr, // sceIoWrite() to fd 2.
|
||||
};
|
||||
|
||||
void Core_SendDebugOutput(LogLevel level, std::string_view string);
|
||||
// Offers raw output from the emulated program to the listener, if any. Returns false if there was
|
||||
// none, in which case the caller should log it instead - that's what the normal app does.
|
||||
bool Core_SendHostOutput(DebugOutputChannel channel, std::string_view string);
|
||||
void Core_SendDebugScreenshot(const DebugScreenshotDesc &desc);
|
||||
void Core_RegisterDebugOutputListeners(std::function<void(std::string_view)> listener, std::function<void(const DebugScreenshotDesc &)> screenshotListener);
|
||||
void Core_RegisterDebugOutputListeners(std::function<void(DebugOutputChannel, std::string_view)> listener, std::function<void(const DebugScreenshotDesc &)> screenshotListener);
|
||||
|
||||
class MIPSState;
|
||||
// Shortcut, just calls Core_MemoryException with automatically determined parameters (function name, etc).
|
||||
|
||||
+17
-11
@@ -1187,6 +1187,20 @@ void SanitizeControlChars(std::string &buf) {
|
||||
}
|
||||
}
|
||||
|
||||
// Output the emulated program wrote to stdout, stderr or a tty. A host that wants it (headless)
|
||||
// gets the bytes exactly as written, so its stdout is a faithful copy of the program's. Otherwise
|
||||
// it goes in the log, sanitized and with the trailing newline trimmed, since the log adds one.
|
||||
static void __IoWriteToHostOutput(DebugOutputChannel channel, const char *str, u32 validSize, int size, const char *name) {
|
||||
if (Core_SendHostOutput(channel, std::string_view(str, validSize))) {
|
||||
return;
|
||||
}
|
||||
const int str_size = size <= 0 || validSize == 0 ? 0 : (str[validSize - 1] == '\n' ? validSize - 1 : validSize);
|
||||
// buffer so we can edit the string.
|
||||
std::string buf(str, str_size);
|
||||
SanitizeControlChars(buf);
|
||||
INFO_LOG(Log::Printf, "%s: %.*s", name, (int)buf.size(), buf.data());
|
||||
}
|
||||
|
||||
static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) {
|
||||
PROFILE_THIS_SCOPE("io_rw");
|
||||
// Low estimate, may be improved later from the WriteFile result.
|
||||
@@ -1199,12 +1213,8 @@ static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) {
|
||||
const u32 validSize = Memory::ClampValidSizeAt(data_addr, size);
|
||||
// Let's handle stdout/stderr specially.
|
||||
if (id == PSP_STDOUT || id == PSP_STDERR) {
|
||||
const char *str = (const char *) data_ptr;
|
||||
const int str_size = size <= 0 ? 0 : (str[validSize - 1] == '\n' ? validSize - 1 : validSize);
|
||||
// buffer so we can edit the string.
|
||||
std::string buf(str, str_size);
|
||||
SanitizeControlChars(buf);
|
||||
INFO_LOG(Log::Printf, "%s: %.*s", id == 1 ? "stdout" : "stderr", (int)buf.size(), buf.data());
|
||||
__IoWriteToHostOutput(id == PSP_STDERR ? DebugOutputChannel::StdErr : DebugOutputChannel::StdOut,
|
||||
(const char *)data_ptr, validSize, size, id == PSP_STDOUT ? "stdout" : "stderr");
|
||||
result = validSize;
|
||||
return true;
|
||||
}
|
||||
@@ -1228,11 +1238,7 @@ static bool __IoWrite(int &result, int id, u32 data_addr, int size, int &us) {
|
||||
NotifyMemInfo(MemBlockFlags::READ, data_addr, size, tag.c_str(), tag.size());
|
||||
|
||||
if (f->isTTY) {
|
||||
const char *str = (const char *)data_ptr;
|
||||
const int str_size = size <= 0 ? 0 : (str[validSize - 1] == '\n' ? validSize - 1 : validSize);
|
||||
std::string buf(str, str_size);
|
||||
SanitizeControlChars(buf);
|
||||
INFO_LOG(Log::Printf, "%s: %.*s", "tty", (int)buf.size(), buf.data());
|
||||
__IoWriteToHostOutput(DebugOutputChannel::StdOut, (const char *)data_ptr, validSize, size, "tty");
|
||||
result = validSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
+21
-3
@@ -85,6 +85,10 @@ static bool g_screenshotFailed = false;
|
||||
static std::string g_debugOutputBuffer;
|
||||
static bool g_writeFailureScreenshot = true;
|
||||
static bool g_writeDebugOutput = true;
|
||||
// Whether the emulated program's stdout/stderr are forwarded to ours. On by default - just running
|
||||
// a homebrew and seeing what it prints is the most basic thing headless does. Off for test runs,
|
||||
// where the only output that should reach the console is what the comparison produces.
|
||||
static bool g_forwardHostOutput = true;
|
||||
// Set from the savestate callback on the emu thread, read after it has been joined.
|
||||
static bool g_stateLoadFailed = false;
|
||||
// Set by --save-state. Saving needs the game to actually be running, so it happens from the run
|
||||
@@ -167,7 +171,20 @@ void SetWriteFailureScreenshot(bool flag) {
|
||||
g_writeFailureScreenshot = flag;
|
||||
}
|
||||
|
||||
void SendDebugOutput(std::string_view output) {
|
||||
void SendDebugOutput(DebugOutputChannel channel, std::string_view output) {
|
||||
if (channel != DebugOutputChannel::Debug) {
|
||||
if (!g_forwardHostOutput)
|
||||
return;
|
||||
// Straight through, unmodified - and flushed, so it interleaves with the debug channel in
|
||||
// the order the program actually wrote it.
|
||||
FlushDebugOutput();
|
||||
fflush(stdout);
|
||||
FILE *stream = channel == DebugOutputChannel::StdErr ? stderr : stdout;
|
||||
fwrite(output.data(), sizeof(char), output.length(), stream);
|
||||
fflush(stream);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_writeDebugOutput)
|
||||
return;
|
||||
#ifdef _WIN32
|
||||
@@ -183,7 +200,7 @@ void SendDebugOutput(std::string_view output) {
|
||||
}
|
||||
|
||||
void SendAndCollectOutput(std::string_view output) {
|
||||
SendDebugOutput(output);
|
||||
SendDebugOutput(DebugOutputChannel::Debug, output);
|
||||
if (PSP_CoreParameter().collectDebugOutput) {
|
||||
*PSP_CoreParameter().collectDebugOutput += output;
|
||||
}
|
||||
@@ -397,7 +414,7 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar
|
||||
if (!opt.bench) {
|
||||
printf("%s", output.c_str());
|
||||
|
||||
SendDebugOutput("TIMEOUT\n");
|
||||
SendDebugOutput(DebugOutputChannel::Debug, "TIMEOUT\n");
|
||||
GitHubActionsPrint("error", "Test timeout for %s", currentTestName.c_str());
|
||||
}
|
||||
|
||||
@@ -971,6 +988,7 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
SetWriteFailureScreenshot(!getenv("GITHUB_ACTIONS") && !testOptions.bench);
|
||||
g_writeDebugOutput = !testOptions.compare && !testOptions.bench;
|
||||
g_forwardHostOutput = !testOptions.compare && !testOptions.bench;
|
||||
|
||||
#if PPSSPP_PLATFORM(ANDROID)
|
||||
// For some reason the debugger installs it with this name?
|
||||
|
||||
+17
-2
@@ -1,6 +1,6 @@
|
||||
# PPSSPPHeadless
|
||||
|
||||
Non-interactive, headless build of PPSSPP. It boots a PSP executable, PRX, or GE frame dump (`.ppdmp`) without a GUI, outputs emulated debug text to the console, optionally captures and compares text output or screenshots, and exits.
|
||||
Non-interactive, headless build of PPSSPP. It boots a PSP executable, PRX, or GE frame dump (`.ppdmp`) without a GUI, forwards what the program prints to the console, optionally captures and compares text output or screenshots, and exits.
|
||||
|
||||
Primarily intended for:
|
||||
- Automated regression testing (via [pspautotests](https://github.com/hrydgard/pspautotests/))
|
||||
@@ -39,7 +39,7 @@ PPSSPPHeadless file.elf|file.prx|file.ppdmp [...] [options]
|
||||
| `@file` | Read list of test filenames from a text file (`@-` for stdin). |
|
||||
| `-m`, `--mount <file.cso>` | Mount an ISO/CSO on `umd1:`. |
|
||||
| `-r`, `--root <path>` | Mount a path on `host0:` (ELF/PRX files must be under this). |
|
||||
| `-l`, `--log` | Full emulator log output (not just emulated `printf`). |
|
||||
| `-l`, `--log` | Full emulator log output, on top of the program's own output (see Program Output below). |
|
||||
| `-o`, `--odslog` | Write log to `OutputDebugString` (Windows only). |
|
||||
| `--graphics=<backend>` | GPU backend: `software`, `gles`, `directx11`, `vulkan`. |
|
||||
| `--screenshot=<file>` | Compare the rendered output against a reference screenshot. |
|
||||
@@ -60,6 +60,21 @@ PPSSPPHeadless file.elf|file.prx|file.ppdmp [...] [options]
|
||||
| `--ignore <file>` | Skip the specified test file. |
|
||||
| `--help` / `-h` | Show usage information. |
|
||||
|
||||
## Program Output
|
||||
|
||||
Running a homebrew with no options at all prints what it prints, and nothing else. Two separate
|
||||
channels feed the console, both on by default:
|
||||
|
||||
- **The emulated program's `stdout` and `stderr`** - `sceIoWrite()` to fd 1 and 2 (which is where a
|
||||
PSPSDK `printf()` ends up), and writes to a tty device. These go to *our* `stdout` and `stderr`
|
||||
respectively, byte for byte, with no prefix or sanitization.
|
||||
- **The `emulator:` devctl channel** - `sceIoDevctl("emulator:", SEND_OUTPUT, ...)`, a PPSSPP
|
||||
extension. This goes to `stdout`. pspautotests uses this channel exclusively, and it is what
|
||||
`--compare` compares against the `.expected` file.
|
||||
|
||||
`--compare` and `--bench` turn both off, so the only thing a test run prints is the comparison
|
||||
result. Emulator log output is separate again, and stays off unless you pass `-l`.
|
||||
|
||||
## GPU Backends
|
||||
|
||||
The `--graphics` option selects the rendering backend:
|
||||
|
||||
Reference in new issue
Block a user