mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-21 16:12:27 -05:00
On the console the daemon stopped minutes after every start, the page then refused to connect, and the status said RetroArch was closed while the picker ran. Its log (webui-daemon.txt): "serving the WebUI" 6 s (9 s the time before) after its start, then "linked: picker", "unlinked: title", "unlinked: picker", and 20 s later "no program of the title linked: the title is closed". The WebUI's start (reading the library, once for each of 16 saved scraping jobs) ran on the link loop: the picker's hello waited past its 5 s, the picker gave up for good, and the daemon, linked to nothing, stopped. - The daemon starts the WebUI on a thread of its own (4 MiB stack, joined before a stop): hellos are answered meanwhile. - The saved jobs are read against one scan of the library, not one each. - src/webui_link.cpp: a hello not answered is asked again every second, never given up while the daemon is there. Evidence: tests/test_webui_daemon.py test_hellos_are_answered_while_the_webui_starts (a start made 7 s slow: both hellos answered in under 1.5 s, the WebUI up later with the right frontend). tools/verify.sh: PASS (format unit build integration evidence).
471 lines
17 KiB
C++
471 lines
17 KiB
C++
/* PS5 RetroArch - the WebUI daemon, /app0/webui/ps5-retroarch-webui.elf.
|
|
*
|
|
* Copyright (C) 2026 Mihawk
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* A payload, started through the console's ELF loader by the first program of the
|
|
* title that finds none running (src/webui_link.cpp): it serves the WebUI
|
|
* (src/webui_ps5.cpp) on 6769 from the title's folder for as long as the title is
|
|
* open, whichever frontend it shows, so a frontend change (a LoadExec, which ends the
|
|
* program it leaves) never cuts an upload or a page. The protocol is in
|
|
* src/webui_link.h.
|
|
*
|
|
* It stops when no program of the title has been linked for kIdleSeconds and no
|
|
* request is being answered: the title closed. An update the WebUI is asked to
|
|
* install makes every linked program close the title; once none is left, this
|
|
* installs it (ps5_update::install), the way eboot.bin did after RetroArch quit.
|
|
*
|
|
* Its own record is <title>/webui-daemon.txt (the last run's is webui-daemon.1.txt).
|
|
* tests/test_webui_daemon.py builds it for the host, with its folder, ports and idle
|
|
* time its own.
|
|
*/
|
|
#include "../src/webui_ps5.h"
|
|
#include "../src/webui_update.h"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <cerrno>
|
|
#include <csignal>
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <fcntl.h>
|
|
#include <netinet/in.h>
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <atomic>
|
|
#include <string>
|
|
#include <sys/mount.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
#include "../src/webui_link.h"
|
|
|
|
#ifndef PS5_WEBUI_HOMEBREW
|
|
#define PS5_WEBUI_HOMEBREW "/data/homebrew"
|
|
#endif
|
|
#ifndef PS5_WEBUI_HTTP_PORT
|
|
#define PS5_WEBUI_HTTP_PORT 6769
|
|
#endif
|
|
#ifndef PS5_WEBUI_IDLE_SECONDS
|
|
#define PS5_WEBUI_IDLE_SECONDS 20.0
|
|
#endif
|
|
|
|
#ifdef __PROSPERO__
|
|
/* What the daemon creates in the title's folder is given 0777, as eboot.bin gives
|
|
* everything under /app0 (src/permissions_ps5.cpp), so FTP can change it: open,
|
|
* mkdir and fopen are wrapped at link time (tools/build-webui-daemon.sh). */
|
|
extern "C"
|
|
{
|
|
int __real_mkdir(const char *, mode_t);
|
|
int __real_open(const char *, int, ...);
|
|
std::FILE *__real_fopen(const char *, const char *);
|
|
|
|
int __wrap_mkdir(const char *path, mode_t mode)
|
|
{
|
|
const int result = __real_mkdir(path, mode | 0777);
|
|
if (result == 0)
|
|
chmod(path, 0777);
|
|
return result;
|
|
}
|
|
int __wrap_open(const char *path, int flags, ...)
|
|
{
|
|
if ((flags & O_CREAT) == 0)
|
|
return __real_open(path, flags);
|
|
va_list arguments;
|
|
va_start(arguments, flags);
|
|
const int mode = va_arg(arguments, int);
|
|
va_end(arguments);
|
|
const int fd = __real_open(path, flags, mode | 0777);
|
|
if (fd >= 0)
|
|
fchmod(fd, 0777);
|
|
return fd;
|
|
}
|
|
std::FILE *__wrap_fopen(const char *path, const char *mode)
|
|
{
|
|
std::FILE *const file = __real_fopen(path, mode);
|
|
if (file && mode && std::strpbrk(mode, "wa+"))
|
|
fchmod(fileno(file), 0777);
|
|
return file;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
constexpr double kIdleSeconds = PS5_WEBUI_IDLE_SECONDS; /* no program linked: closed */
|
|
constexpr double kInstallSeconds = 5.0; /* no program linked, an install asked */
|
|
constexpr double kNoHelloSeconds = 30.0; /* started, and no program ever linked */
|
|
|
|
struct Link
|
|
{
|
|
int fd;
|
|
std::string pending, frontend;
|
|
bool hello = false;
|
|
};
|
|
|
|
std::string root, title, version, log_path;
|
|
std::vector<std::string> early_lines;
|
|
|
|
double now_s()
|
|
{
|
|
timespec now{};
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
return double(now.tv_sec) + double(now.tv_nsec) / 1e9;
|
|
}
|
|
|
|
void say(const char *format, ...)
|
|
{
|
|
char line[512];
|
|
va_list arguments;
|
|
va_start(arguments, format);
|
|
std::vsnprintf(line, sizeof line, format, arguments);
|
|
va_end(arguments);
|
|
std::fprintf(stderr, "[webui-daemon] %s\n", line);
|
|
if (log_path.empty())
|
|
{
|
|
early_lines.emplace_back(line);
|
|
return;
|
|
}
|
|
if (std::FILE *file = std::fopen(log_path.c_str(), "a"))
|
|
{
|
|
std::time_t t = std::time(nullptr);
|
|
char stamp[32];
|
|
std::strftime(stamp, sizeof stamp, "%Y-%m-%d %H:%M:%S", std::gmtime(&t));
|
|
/* The process: a stale daemon still finishing writes to the same file. */
|
|
std::fprintf(file, "%s [%d] %s\n", stamp, int(getpid()), line);
|
|
std::fclose(file);
|
|
}
|
|
}
|
|
|
|
std::string read_small(const std::string &path)
|
|
{
|
|
std::string out;
|
|
if (std::FILE *file = std::fopen(path.c_str(), "rb"))
|
|
{
|
|
char buffer[4096];
|
|
size_t got;
|
|
while ((got = std::fread(buffer, 1, sizeof buffer, file)) > 0 && out.size() < 65536)
|
|
out.append(buffer, got);
|
|
std::fclose(file);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool is_file(const std::string &path)
|
|
{
|
|
struct stat st{};
|
|
return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode);
|
|
}
|
|
|
|
bool valid_id(const std::string &id)
|
|
{
|
|
if (id.size() != 9)
|
|
return false;
|
|
for (char c : id)
|
|
if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/* The title's folder: what is mounted as its application folder
|
|
* (/data/homebrew/PPSA99169 -> /system_ex/app/PPSA99169, docs/FINDINGS.md), else
|
|
* the usual place; either only when it holds this title's eboot.bin and WebUI. */
|
|
std::string title_folder(const std::string &id)
|
|
{
|
|
std::vector<std::string> candidates;
|
|
#ifdef __PROSPERO__
|
|
struct statfs *mounts = nullptr;
|
|
const int count = getmntinfo(&mounts, MNT_NOWAIT);
|
|
const std::string app = "/system_ex/app/" + id;
|
|
for (int i = 0; i < count; ++i)
|
|
if (app == mounts[i].f_mntonname && mounts[i].f_mntfromname[0] == '/')
|
|
candidates.emplace_back(mounts[i].f_mntfromname);
|
|
#endif
|
|
candidates.push_back(PS5_WEBUI_HOMEBREW "/" + id);
|
|
for (const auto &folder : candidates)
|
|
if (is_file(folder + "/eboot.bin") && is_file(folder + "/webui/index.html") &&
|
|
read_small(folder + "/sce_sys/param.json").find(id) != std::string::npos)
|
|
return folder;
|
|
return "";
|
|
}
|
|
|
|
bool send_line(int fd, const char *line)
|
|
{
|
|
const size_t size = std::strlen(line);
|
|
return send(fd, line, size, 0) == ssize_t(size);
|
|
}
|
|
|
|
int listen_local(unsigned short port)
|
|
{
|
|
const int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fd < 0)
|
|
return -1;
|
|
int yes = 1;
|
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
|
|
sockaddr_in address{};
|
|
address.sin_family = AF_INET;
|
|
address.sin_port = htons(port);
|
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
if (bind(fd, reinterpret_cast<sockaddr *>(&address), sizeof address) != 0 || listen(fd, 8) != 0)
|
|
{
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
void show_frontend(const std::vector<Link> &links)
|
|
{
|
|
const char *shown = "";
|
|
for (const auto &link : links)
|
|
if (link.hello)
|
|
shown = link.frontend.c_str();
|
|
ps5_webui_set_frontend(shown);
|
|
}
|
|
} // namespace
|
|
|
|
int main()
|
|
{
|
|
std::signal(SIGPIPE, SIG_IGN);
|
|
int listener = listen_local(PS5_WEBUI_LINK_PORT);
|
|
if (listener < 0)
|
|
{
|
|
std::fprintf(stderr, "[webui-daemon] another daemon has %d; this one stops\n",
|
|
PS5_WEBUI_LINK_PORT);
|
|
return 0;
|
|
}
|
|
ps5_webui_set_frontend("");
|
|
std::vector<Link> links;
|
|
bool serving = false, stale = false, install_sent = false, installed = false;
|
|
const double started = now_s();
|
|
double idle_since = started, installed_at = 0, last_serve_try = 0;
|
|
unsigned serve_failures = 0;
|
|
/* The WebUI starts on a thread of its own (reading the library and the saved scraping
|
|
* jobs takes seconds): the title's hello is answered meanwhile. 0 idle, 1 starting,
|
|
* 2 serving, 3 failed. */
|
|
static std::atomic<int> start_state{0};
|
|
static std::string start_folder;
|
|
pthread_t starter{};
|
|
bool starter_running = false;
|
|
say("started; listening for the title on 127.0.0.1:%d", PS5_WEBUI_LINK_PORT);
|
|
|
|
for (;;)
|
|
{
|
|
std::vector<pollfd> waits;
|
|
if (listener >= 0)
|
|
waits.push_back({listener, POLLIN, 0});
|
|
for (const auto &link : links)
|
|
waits.push_back({link.fd, POLLIN, 0});
|
|
poll(waits.data(), waits.size(), 250);
|
|
|
|
if (listener >= 0 && (waits[0].revents & POLLIN))
|
|
{
|
|
const int fd = accept(listener, nullptr, nullptr);
|
|
if (fd >= 0 && links.size() < 16)
|
|
links.push_back({fd, "", "", false});
|
|
else if (fd >= 0)
|
|
close(fd);
|
|
}
|
|
for (auto &link : links)
|
|
{
|
|
pollfd wait{link.fd, POLLIN, 0};
|
|
if (poll(&wait, 1, 0) <= 0)
|
|
continue;
|
|
char buffer[512];
|
|
const ssize_t got = recv(link.fd, buffer, sizeof buffer, 0);
|
|
if (got <= 0 || link.pending.size() > 4096)
|
|
{
|
|
close(link.fd);
|
|
link.fd = -1;
|
|
continue;
|
|
}
|
|
link.pending.append(buffer, size_t(got));
|
|
size_t end;
|
|
while (link.fd >= 0 && (end = link.pending.find('\n')) != std::string::npos)
|
|
{
|
|
const std::string line = link.pending.substr(0, end);
|
|
link.pending.erase(0, end + 1);
|
|
char word[16] = {}, frontend[32] = {}, id[16] = {};
|
|
const int fields = std::sscanf(line.c_str(), "%15s %31s %15s", word, frontend, id);
|
|
if (fields == 3 && std::strcmp(word, "hello") == 0)
|
|
{
|
|
const char *reply = "ok\n";
|
|
if (stale)
|
|
reply = "stale\n";
|
|
else if (root.empty())
|
|
{
|
|
const std::string folder = valid_id(id) ? title_folder(id) : "";
|
|
if (folder.empty())
|
|
{
|
|
say("hello from %s of %s: no folder of that title found", frontend, id);
|
|
reply = "unknown\n";
|
|
}
|
|
else
|
|
{
|
|
root = folder;
|
|
title = id;
|
|
version = read_small(root + "/webui/version.json");
|
|
log_path = root + "/webui-daemon.txt";
|
|
rename(log_path.c_str(), (root + "/webui-daemon.1.txt").c_str());
|
|
for (const auto &early : early_lines)
|
|
say("%s", early.c_str());
|
|
early_lines.clear();
|
|
say("the title is %s, in %s", title.c_str(), root.c_str());
|
|
}
|
|
}
|
|
else if (title != id)
|
|
reply = "other\n";
|
|
else if (read_small(root + "/webui/version.json") != version)
|
|
{
|
|
say("hello from another build of the title: this daemon stops once "
|
|
"its requests end");
|
|
stale = true;
|
|
reply = "stale\n";
|
|
}
|
|
send_line(link.fd, reply);
|
|
if (std::strcmp(reply, "ok\n") == 0)
|
|
{
|
|
link.hello = true;
|
|
link.frontend = frontend;
|
|
say("linked: %s (%zu linked)", frontend, links.size());
|
|
if (install_sent)
|
|
send_line(link.fd, "install\n");
|
|
}
|
|
else
|
|
{
|
|
close(link.fd);
|
|
link.fd = -1;
|
|
}
|
|
}
|
|
else if (fields >= 2 && std::strcmp(word, "frontend") == 0 && link.hello)
|
|
{
|
|
link.frontend = frontend;
|
|
say("frontend: %s", frontend);
|
|
}
|
|
}
|
|
}
|
|
for (auto it = links.begin(); it != links.end();)
|
|
if (it->fd < 0)
|
|
{
|
|
if (it->hello)
|
|
say("unlinked: %s", it->frontend.c_str());
|
|
it = links.erase(it);
|
|
}
|
|
else
|
|
++it;
|
|
show_frontend(links);
|
|
|
|
const double now = now_s();
|
|
bool linked = false;
|
|
for (const auto &link : links)
|
|
linked |= link.hello;
|
|
if (linked)
|
|
idle_since = 0;
|
|
else if (idle_since == 0)
|
|
idle_since = now;
|
|
|
|
if (start_state == 2 || start_state == 3)
|
|
{
|
|
if (starter_running)
|
|
pthread_join(starter, nullptr);
|
|
starter_running = false;
|
|
serving = start_state == 2;
|
|
start_state = 0;
|
|
if (serving)
|
|
say("serving the WebUI on %d from %s (%u s busy before)", PS5_WEBUI_HTTP_PORT,
|
|
root.c_str(), serve_failures);
|
|
else if (serve_failures++ == 0)
|
|
say("%d is busy (another daemon's last transfer?); trying every second",
|
|
PS5_WEBUI_HTTP_PORT);
|
|
}
|
|
if (!serving && start_state == 0 && !root.empty() && !stale && now - last_serve_try >= 1.0)
|
|
{
|
|
last_serve_try = now;
|
|
start_state = 1;
|
|
start_folder = root;
|
|
/* A payload's default thread stack is small; the library scan needs more. */
|
|
pthread_attr_t attributes;
|
|
pthread_attr_init(&attributes);
|
|
pthread_attr_setstacksize(&attributes, 4u << 20);
|
|
starter_running =
|
|
pthread_create(
|
|
&starter, &attributes,
|
|
[](void *) -> void *
|
|
{
|
|
#ifdef PS5_WEBUI_TEST_START_DELAY_MS /* the tests: a start as slow as a big library's */
|
|
usleep(PS5_WEBUI_TEST_START_DELAY_MS * 1000);
|
|
#endif
|
|
start_state =
|
|
ps5_webui_start(start_folder.c_str(), PS5_WEBUI_HTTP_PORT, false) ? 2
|
|
: 3;
|
|
return nullptr;
|
|
},
|
|
nullptr) == 0;
|
|
pthread_attr_destroy(&attributes);
|
|
if (!starter_running)
|
|
start_state = 3;
|
|
}
|
|
if (serving && !install_sent && ps5_update::exit_requested())
|
|
{
|
|
install_sent = true;
|
|
say("install requested: asking %zu linked program(s) to close the title", links.size());
|
|
for (const auto &link : links)
|
|
if (link.hello)
|
|
send_line(link.fd, "install\n");
|
|
}
|
|
if (install_sent && !installed && !linked && now - idle_since >= kInstallSeconds)
|
|
{
|
|
installed = true;
|
|
installed_at = now;
|
|
const bool ok = ps5_update::install();
|
|
say("install: %s", ok ? "done" : "failed (see .update-result)");
|
|
stale = true; /* this daemon is the old build's now */
|
|
}
|
|
|
|
// What must not be cut: transfers in flight, not an open page's idle keep-alive
|
|
// connections (counting those, a stale daemon kept 6769 from the new build).
|
|
const unsigned busy = ps5_webui_transfers();
|
|
const std::string update = ps5_update::status().state;
|
|
// A download, or a scraping job, goes on with the title closed and no page open.
|
|
const bool updating =
|
|
update == "downloading" || update == "verifying" || ps5_webui_scraping();
|
|
bool stop = false;
|
|
if (root.empty() && !linked && now - started >= kNoHelloSeconds)
|
|
{
|
|
say("no program of a title linked within %.0f s", kNoHelloSeconds);
|
|
stop = true;
|
|
}
|
|
else if (stale && !busy && !linked && (!installed || now - installed_at >= 15.0))
|
|
{
|
|
say("stopping for the new build's daemon (no transfer in flight%s)",
|
|
ps5_webui_scraping() ? "; the scraping job is saved and resumes there" : "");
|
|
stop = true;
|
|
}
|
|
else if (!root.empty() && !linked && !busy && !updating && !install_sent &&
|
|
now - idle_since >= kIdleSeconds)
|
|
{
|
|
say("no program of the title linked for %.0f s: the title is closed", kIdleSeconds);
|
|
stop = true;
|
|
}
|
|
if (stale && listener >= 0 && !installed)
|
|
{
|
|
close(listener); /* the new build's daemon can start meanwhile */
|
|
listener = -1;
|
|
}
|
|
if (stop)
|
|
break;
|
|
}
|
|
if (starter_running)
|
|
pthread_join(starter, nullptr); /* a start under way ends before the stop */
|
|
for (const auto &link : links)
|
|
close(link.fd);
|
|
if (listener >= 0)
|
|
close(listener);
|
|
ps5_webui_stop();
|
|
say("stopped");
|
|
return 0;
|
|
}
|