mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-22 16:42:33 -05:00
The WebUI only ran inside RetroArch: a frontend change (LoadExec) ended it with every upload in flight. It is now a payload of its own (daemon/webui_daemon.cpp, webui/ps5-retroarch-webui.elf) serving the title's folder on 6769. eboot.bin, the picker and ES-DE each keep a link to it (src/webui_link.h, 127.0.0.1:6770) and the first to find none starts it through the ELF loader (9021) or websrv (8080 /hbldr). It stops 20 s after the title closes, never mid-transfer; an update's Install closes the title and the daemon installs. With no loader, eboot.bin serves the WebUI while RetroArch runs, as before. The page names the frontend shown. Downloads were handed to MHD as a descriptor (sendfile), which stopped at 33,300 bytes on the console; they are read in 256 KiB blocks. Evidence: evidence/webui-daemon (base PS5 13.40). 200 MiB upload across picker -> RetroArch -> picker and 60 MiB across picker -> ES-DE: one token throughout, 201 Created, every 256 KiB block equal on the console; daemon stopped 20 s after the title closed; a 1,917,648,896-byte download's sha256 equals the FTP copy. Command: tools/run-title.sh --picker-test=600:retroarch --retroarch-frames=1800 --watch 110 with build/tmp/webui-handover.py; tools/verify.sh: all five PASS. Host tests: tests/test_webui_link.py, tests/test_webui_daemon.py.
421 lines
14 KiB
C++
421 lines
14 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 <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));
|
|
std::fprintf(file, "%s %s\n", stamp, 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;
|
|
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 (!serving && !root.empty() && !stale && now - last_serve_try >= 1.0)
|
|
{
|
|
last_serve_try = now;
|
|
serving = ps5_webui_start(root.c_str(), PS5_WEBUI_HTTP_PORT, false);
|
|
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 && !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 */
|
|
}
|
|
|
|
const unsigned busy = ps5_webui_connections();
|
|
const std::string update = ps5_update::status().state;
|
|
const bool updating = update == "downloading" || update == "verifying";
|
|
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))
|
|
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;
|
|
}
|
|
for (const auto &link : links)
|
|
close(link.fd);
|
|
if (listener >= 0)
|
|
close(listener);
|
|
ps5_webui_stop();
|
|
say("stopped");
|
|
return 0;
|
|
}
|