Files
PS5_RetroArch/tests/relaunch_ps5_test.cpp
T
Mihawk d8cd370ce2 Prove the frontend handover's title restart on the console
The frontend picker hands over by restarting the title in a new process
(approach B). src/relaunch_ps5.cpp measures that restart: armed by
/app0/relaunch-test.txt (tools/run-title.sh --relaunch-test[=N]), each process
records its generation, argv, flexible memory, pid and clock in
/app0/relaunch-test.jsonl, restarts the title through
sceSystemServiceLoadExec("/app0/eboot.bin", {"--ps5-relaunch=N", NULL}), and
the last one continues into RetroArch. main() now takes the process arguments.

Found on the console: LoadExec's arguments become the new process's whole
argv, with no program name in front (argc=1, argv[0]="--ps5-relaunch=1"); a
home-screen launch gets argc=1, argv[0]="". The first run (build f935ae4e)
read from argv[1] and failed its argument check; the parser now reads from
argv[0] and the host test feeds argv in the console's shape.

Verified:
  TMPDIR=build/tmp bash tools/verify.sh -> PASS (format unit build
    integration evidence); build identity 453864d6...
  tools/run-title.sh --no-build --relaunch-test=5 --watch 60 on the base
    PS5 (13.40): 5 restarts, pids 117-122, each with its arguments,
    419430400 bytes flexible free at every start, 0.56-0.61 s per restart;
    kernel log EXEC x6, "Kill for LoadExec => 0" x5, no fatal signal;
    RetroArch started after the chain with XMB assets loaded.
Evidence: evidence/loadexec-relaunch/ (python3 tools/evidence.py compare)
2026-10-04 13:34:42 -04:00

187 lines
6.9 KiB
C++

/* The relaunch test's chain, run on the host against a LoadExec that either
* replaces the process (a thrown Replaced), refuses, or is accepted and ignored.
* The processes get argv as the console gives it: LoadExec's arguments are the
* whole argv, with no program name in front, and a launch from the home screen
* gets one empty argument. argv[1] is a scratch directory; the results file is
* left there for tests/test_relaunch_ps5.py to parse as JSON. */
#include <cassert>
#include <cstdio>
#include <string>
#include <vector>
#include "../src/relaunch_ps5.cpp"
namespace
{
struct Replaced
{
};
enum class Exec
{
replace,
refuse,
ignore
};
Exec exec_mode = Exec::replace;
int exec_calls = 0;
std::string exec_path, exec_argument;
std::vector<std::string> marks;
std::string slurp(const std::string &path)
{
std::string out;
ps5::relaunch::read_file(path, out);
return out;
}
bool exists(const std::string &path)
{
std::FILE *file = std::fopen(path.c_str(), "rb");
if (file)
std::fclose(file);
return file != nullptr;
}
void arm(const ps5::relaunch::Paths &paths, const char *text)
{
std::FILE *file = std::fopen(paths.arm.c_str(), "wb");
assert(file);
std::fputs(text, file);
std::fclose(file);
}
unsigned lines(const std::string &text)
{
unsigned count = 0;
for (char c : text)
count += c == '\n';
return count;
}
/* One process of the chain: true when LoadExec replaced it. */
bool launch(const ps5::relaunch::Paths &paths, std::vector<const char *> argv, bool &continued)
{
argv.push_back(nullptr);
try
{
continued = ps5::relaunch::run_test(paths, static_cast<int>(argv.size() - 1),
const_cast<char **>(argv.data()), 0);
return false;
}
catch (const Replaced &)
{
return true;
}
}
} // namespace
extern "C" int sceSystemServiceLoadExec(const char *path, const char *const *argv)
{
exec_calls++;
exec_path = path;
exec_argument = argv && argv[0] ? argv[0] : "";
assert(argv && argv[0] && !argv[1]);
if (exec_mode == Exec::replace)
throw Replaced{};
return exec_mode == Exec::refuse ? static_cast<int>(0x80020002u) : 0;
}
extern "C" int sceKernelAvailableFlexibleMemorySize(std::size_t *size)
{
*size = 403u << 20;
return 0;
}
void ps5::debug::mark(const char *step) noexcept
{
marks.emplace_back(step);
}
void ps5::debug::mark_value(const char *step, long long value) noexcept
{
marks.emplace_back(std::string(step) + " " + std::to_string(value));
}
int main(int argc, char **argv)
{
assert(argc == 2);
const std::string dir = argv[1];
ps5::relaunch::Paths paths{dir + "/relaunch-test.txt", dir + "/relaunch-test.jsonl",
"/app0/eboot.bin"};
bool continued = true;
// Unarmed: nothing is read, written or started.
assert(!launch(paths, {""}, continued) && !continued);
assert(!exists(paths.results) && exec_calls == 0);
// Arm files that must not start anything; each is removed.
for (const char *bad : {"0 run", "21 run", "3", "3 bad/run", "3 \"quote\"", "x run"})
{
arm(paths, bad);
assert(!launch(paths, {""}, continued) && !continued);
assert(!exists(paths.arm) && !exists(paths.results) && exec_calls == 0);
}
// A full chain: generation 0 from the home screen, three restarts, then RetroArch.
// Another run's line already in the file does not count toward this one.
{
std::FILE *old = std::fopen(paths.results.c_str(), "wb");
std::fputs("{\"run\":\"older\",\"generation\":0,\"action\":\"restart\"}\n", old);
std::fclose(old);
}
arm(paths, "3 chain-1\n");
assert(launch(paths, {""}, continued));
assert(exec_calls == 1 && exec_path == "/app0/eboot.bin" &&
exec_argument == "--ps5-relaunch=1");
assert(launch(paths, {"--ps5-relaunch=1"}, continued) && exec_argument == "--ps5-relaunch=2");
// A restart whose arguments were lost still counts from the file; the last
// one finds its token after another argument.
assert(launch(paths, {""}, continued) && exec_argument == "--ps5-relaunch=3");
assert(!launch(paths, {"other", "--ps5-relaunch=3"}, continued) && !continued);
assert(exec_calls == 3 && !exists(paths.arm));
const std::string chain = slurp(paths.results);
assert(lines(chain) == 5);
assert(ps5::relaunch::generation(chain, "chain-1") == 4);
assert(ps5::relaunch::generation(chain, "older") == 1);
assert(chain.find("\"generation\":0,\"count\":3,\"argument_generation\":-1") !=
std::string::npos);
assert(chain.find("\"generation\":1,\"count\":3,\"argument_generation\":1") !=
std::string::npos);
assert(chain.find("\"generation\":2,\"count\":3,\"argument_generation\":-1") !=
std::string::npos);
assert(chain.find("\"generation\":3,\"count\":3,\"argument_generation\":3") !=
std::string::npos);
assert(chain.find("\"flexible_free\":422576128") != std::string::npos);
assert(chain.rfind("\"action\":\"continue\"}") != std::string::npos);
// Refused: the result is recorded and the launch continues into RetroArch.
exec_mode = Exec::refuse;
arm(paths, "2 refused\n");
assert(!launch(paths, {""}, continued) && continued && !exists(paths.arm));
std::string text = slurp(paths.results);
assert(text.find("{\"run\":\"refused\",\"generation\":0,\"event\":\"loadexec\",\"result\":-"
"2147352574}") != std::string::npos);
assert(ps5::relaunch::generation(text, "refused") == 1);
// Accepted, yet this process is still running after the wait: said, and disarmed.
exec_mode = Exec::ignore;
arm(paths, "2 ignored\n");
assert(!launch(paths, {""}, continued) && continued && !exists(paths.arm));
text = slurp(paths.results);
assert(text.find("\"run\":\"ignored\",\"generation\":0,\"event\":\"loadexec\",\"result\":0,"
"\"still_running_after_s\":0}") != std::string::npos);
// Arguments are escaped: the file stays JSON whatever the shell passes.
exec_mode = Exec::replace;
arm(paths, "1 escaped\n");
assert(launch(paths, {"eboot", "quote\"back\\slash", "tab\there"}, continued));
text = slurp(paths.results);
assert(text.find("\"argv\":[\"eboot\",\"quote\\\"back\\\\slash\",\"tab\\u0009here\"]") !=
std::string::npos);
assert(!launch(paths, {"--ps5-relaunch=1"}, continued) && !continued);
// A record that cannot be written never restarts.
const int calls = exec_calls;
ps5::relaunch::Paths unwritable = paths;
unwritable.results = dir + "/missing-directory/relaunch-test.jsonl";
arm(unwritable, "2 unwritable\n");
assert(!launch(unwritable, {""}, continued) && !continued);
assert(exec_calls == calls && !exists(unwritable.arm));
std::puts("relaunch_ps5: arming, chain counting, lost arguments, refused and ignored LoadExec, "
"escaping and unwritable records PASS");
return 0;
}