mirror of
https://github.com/itsPLK/ps5-webkit-autoloader.git
synced 2026-10-19 02:09:45 -05:00
feat: detect and repair corrupted WebKit cache with clear-and-retry flow
When AppCache download fails due to corrupted on-disk WebKit data, the installer page detects the failure and presents a prompt offering to wipe the current user's webkit data (/user/home/<userid>/webkit/shell/) and retry. - Add webkit_cleaner module to safely wipe webkit/shell for the foreground user - Add /clear-webkit-data and /exit HTTP server routes and lifecycle handling - Pass uid parameter to the installer page to display the exact target path - Support one-shot retry (?retry=1) with persistent error reporting and clean exit - Add corruption simulation build modes (SIMULATE=1|2) and helper script for testing
This commit is contained in:
1 parent
a86320f9bb
commit
653d3f359d
12 files changed
+527
-7
No files matched your search
@@ -16,7 +16,8 @@ LIBS := $(TARGET)/lib/libmicrohttpd.a \
|
|||||||
|
|
||||||
# Source Files
|
# Source Files
|
||||||
SRCS := src/main.c src/http_server.c src/app_installer.c \
|
SRCS := src/main.c src/http_server.c src/app_installer.c \
|
||||||
src/notification.c src/ps5_launcher.c src/log.c src/inflate.c
|
src/notification.c src/ps5_launcher.c src/log.c src/inflate.c \
|
||||||
|
src/webkit_cleaner.c src/simulate_corrupt.c
|
||||||
ELF := installer.elf
|
ELF := installer.elf
|
||||||
|
|
||||||
# Generated file registry
|
# Generated file registry
|
||||||
@@ -55,6 +56,20 @@ WKAL_HOST_SOURCES := pc-host/host.py $(FRONTEND_FILES)
|
|||||||
CFLAGS := -Os -Wall -ffunction-sections -fdata-sections $(INCLUDES)
|
CFLAGS := -Os -Wall -ffunction-sections -fdata-sections $(INCLUDES)
|
||||||
LDFLAGS := -Wl,--gc-sections
|
LDFLAGS := -Wl,--gc-sections
|
||||||
|
|
||||||
|
# Test builds that simulate a corrupted WebKit AppCache (see tools/build_cache_corruption_test_elfs.sh):
|
||||||
|
# SIMULATE=0 (default) - production behavior, corruption detection only
|
||||||
|
# SIMULATE=1 - every cache download fails until /clear-webkit-data succeeds
|
||||||
|
# (tests the clear-and-retry repair flow end to end)
|
||||||
|
# SIMULATE=2 - every cache download always fails (tests the terminal-error retry path)
|
||||||
|
# NOTE: CFLAGS changes are not tracked by make, so always `make clean all` when
|
||||||
|
# switching SIMULATE modes.
|
||||||
|
SIMULATE ?= 0
|
||||||
|
ifeq ($(SIMULATE),1)
|
||||||
|
CFLAGS += -DWKALI_SIMULATE_CACHE_CORRUPTION=1
|
||||||
|
else ifeq ($(SIMULATE),2)
|
||||||
|
CFLAGS += -DWKALI_SIMULATE_CACHE_CORRUPTION=2
|
||||||
|
endif
|
||||||
|
|
||||||
all: $(ELF)
|
all: $(ELF)
|
||||||
|
|
||||||
# Regenerate the version header on every build (cheap, only rewrites on change)
|
# Regenerate the version header on every build (cheap, only rewrites on change)
|
||||||
|
|||||||
@@ -155,6 +155,82 @@
|
|||||||
#logContainer.error {
|
#logContainer.error {
|
||||||
color: #f00;
|
color: #f00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(2, 6, 23, 0.85);
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: center;
|
||||||
|
-webkit-justify-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay .card {
|
||||||
|
background: #1b2436;
|
||||||
|
border: 1px solid #2c3e5a;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 34rem;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay h2 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fbbf24;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay p {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #cbd5e1;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay .actions {
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-pack: center;
|
||||||
|
-webkit-justify-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay .actions[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay .btn {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay .btn.primary {
|
||||||
|
background: #2563eb;
|
||||||
|
background: -webkit-linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cacheCorruptOverlay .btn.secondary {
|
||||||
|
background: #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@@ -186,6 +262,21 @@
|
|||||||
var progressBar = document.getElementById('progressBar');
|
var progressBar = document.getElementById('progressBar');
|
||||||
var progressPct = document.getElementById('progressPct');
|
var progressPct = document.getElementById('progressPct');
|
||||||
var completed = false;
|
var completed = false;
|
||||||
|
var isDownloading = false;
|
||||||
|
var isRetry = false;
|
||||||
|
var currentUid = '';
|
||||||
|
var simulateCorrupt = false;
|
||||||
|
var corruptionPromptShown = false;
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
isRetry = params.get('retry') === '1';
|
||||||
|
currentUid = params.get('uid') || '';
|
||||||
|
/* Test hook: ?simulate=corrupt forces the corruption prompt without
|
||||||
|
* needing a real AppCache error (e.g. for quick UI testing in a
|
||||||
|
* desktop browser via `make dev`). */
|
||||||
|
simulateCorrupt = params.get('simulate') === 'corrupt';
|
||||||
|
})();
|
||||||
|
|
||||||
var logContainer = document.getElementById('logContainer');
|
var logContainer = document.getElementById('logContainer');
|
||||||
var seenLogs = "";
|
var seenLogs = "";
|
||||||
@@ -281,6 +372,8 @@
|
|||||||
xhr.onreadystatechange = function () {
|
xhr.onreadystatechange = function () {
|
||||||
if (xhr.readyState === 4) {
|
if (xhr.readyState === 4) {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
|
var hdrUid = xhr.getResponseHeader("X-User-Id");
|
||||||
|
if (hdrUid) currentUid = hdrUid;
|
||||||
probeOk = true;
|
probeOk = true;
|
||||||
setCaching();
|
setCaching();
|
||||||
} else {
|
} else {
|
||||||
@@ -327,7 +420,109 @@
|
|||||||
installApp();
|
installApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCacheCorrupted() {
|
||||||
|
if (corruptionPromptShown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
corruptionPromptShown = true;
|
||||||
|
completed = true;
|
||||||
|
progressWrap.hidden = true;
|
||||||
|
logContainer.hidden = true;
|
||||||
|
statusEl.textContent = 'Caching failed';
|
||||||
|
detailEl.textContent = '';
|
||||||
|
|
||||||
|
var overlay = document.createElement('div');
|
||||||
|
overlay.id = 'cacheCorruptOverlay';
|
||||||
|
|
||||||
|
var card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
|
||||||
|
var title = document.createElement('h2');
|
||||||
|
var msg = document.createElement('p');
|
||||||
|
var actions = null;
|
||||||
|
|
||||||
|
var webkitShellPath = '/user/home/' + (currentUid || '<userid>') + '/webkit/shell/';
|
||||||
|
|
||||||
|
if (isRetry) {
|
||||||
|
title.textContent = 'Cache issue persists';
|
||||||
|
msg.textContent = 'Clearing WebKit data from ' + webkitShellPath + ' didn\'t seem to fix the issue. You can close this page.';
|
||||||
|
var exitXhr = new XMLHttpRequest();
|
||||||
|
exitXhr.open('GET', '/exit', true);
|
||||||
|
exitXhr.send();
|
||||||
|
} else {
|
||||||
|
title.textContent = 'WebKit cache appears corrupted';
|
||||||
|
msg.textContent = 'Your PS5\'s WebKit browser data may be corrupted, preventing the cache from completing. Clearing it may fix the issue. ';
|
||||||
|
var strong = document.createElement('strong');
|
||||||
|
strong.textContent = 'This will remove all browser data in ' + webkitShellPath + ' (AppCache, cookies, local storage) for the current user.';
|
||||||
|
msg.appendChild(strong);
|
||||||
|
|
||||||
|
actions = document.createElement('div');
|
||||||
|
actions.className = 'actions';
|
||||||
|
|
||||||
|
var clearBtn = document.createElement('button');
|
||||||
|
clearBtn.className = 'btn primary';
|
||||||
|
clearBtn.textContent = 'Clear WebKit Data & Retry';
|
||||||
|
clearBtn.addEventListener('click', function () {
|
||||||
|
actions.hidden = true;
|
||||||
|
actions.style.display = 'none';
|
||||||
|
msg.textContent = 'Clearing data and restarting browser...';
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', '/clear-webkit-data', true);
|
||||||
|
xhr.timeout = 30000;
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState === 4 && xhr.status !== 200) {
|
||||||
|
msg.textContent = 'Failed to clear WebKit data';
|
||||||
|
actions.hidden = false;
|
||||||
|
actions.style.display = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
msg.textContent = 'Failed to clear WebKit data';
|
||||||
|
actions.hidden = false;
|
||||||
|
actions.style.display = '';
|
||||||
|
};
|
||||||
|
xhr.ontimeout = function () {
|
||||||
|
msg.textContent = 'Failed to clear WebKit data';
|
||||||
|
actions.hidden = false;
|
||||||
|
actions.style.display = '';
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
actions.appendChild(clearBtn);
|
||||||
|
|
||||||
|
var cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.className = 'btn secondary';
|
||||||
|
cancelBtn.textContent = 'Cancel';
|
||||||
|
cancelBtn.addEventListener('click', function () {
|
||||||
|
msg.textContent = 'You can close this page. Re-run the installer ELF to try again.';
|
||||||
|
actions.hidden = true;
|
||||||
|
actions.style.display = 'none';
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', '/exit', true);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
actions.appendChild(cancelBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.appendChild(title);
|
||||||
|
card.appendChild(msg);
|
||||||
|
if (actions) {
|
||||||
|
card.appendChild(actions);
|
||||||
|
}
|
||||||
|
overlay.appendChild(card);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
|
/* Test hook: ?simulate=corrupt forces the corruption prompt as if an
|
||||||
|
* AppCache error fired during download, and skips the probe/install
|
||||||
|
* polling so the prompt stays up for UI testing (e.g. make dev). */
|
||||||
|
if (simulateCorrupt) {
|
||||||
|
isDownloading = true;
|
||||||
|
setCacheCorrupted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
probeServer();
|
probeServer();
|
||||||
|
|
||||||
/* The shortcut is created only once the AppCache is current, so the
|
/* The shortcut is created only once the AppCache is current, so the
|
||||||
@@ -368,6 +563,7 @@
|
|||||||
appendLog("[AppCache] Checking for manifest...");
|
appendLog("[AppCache] Checking for manifest...");
|
||||||
});
|
});
|
||||||
window.applicationCache.addEventListener('downloading', function () {
|
window.applicationCache.addEventListener('downloading', function () {
|
||||||
|
isDownloading = true;
|
||||||
appendLog("[AppCache] Downloading cache...");
|
appendLog("[AppCache] Downloading cache...");
|
||||||
});
|
});
|
||||||
window.applicationCache.addEventListener('progress', function (e) {
|
window.applicationCache.addEventListener('progress', function (e) {
|
||||||
@@ -380,20 +576,25 @@
|
|||||||
window.applicationCache.addEventListener('error', function (e) {
|
window.applicationCache.addEventListener('error', function (e) {
|
||||||
appendLog("[AppCache] Error occurred! Status: " + window.applicationCache.status);
|
appendLog("[AppCache] Error occurred! Status: " + window.applicationCache.status);
|
||||||
logContainer.className = 'error';
|
logContainer.className = 'error';
|
||||||
|
if (isDownloading) {
|
||||||
|
setCacheCorrupted();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.applicationCache.addEventListener('obsolete', function (e) {
|
window.applicationCache.addEventListener('obsolete', function (e) {
|
||||||
appendLog("[AppCache] Manifest is obsolete.");
|
appendLog("[AppCache] Manifest is obsolete.");
|
||||||
});
|
});
|
||||||
window.applicationCache.addEventListener('cached', function() {
|
window.applicationCache.addEventListener('cached', function() {
|
||||||
|
isDownloading = false;
|
||||||
appendLog("[AppCache] Caching complete.");
|
appendLog("[AppCache] Caching complete.");
|
||||||
maybeInstall();
|
maybeInstall();
|
||||||
});
|
});
|
||||||
window.applicationCache.addEventListener('noupdate', function() {
|
window.applicationCache.addEventListener('noupdate', function() {
|
||||||
|
isDownloading = false;
|
||||||
appendLog("[AppCache] No update needed.");
|
appendLog("[AppCache] No update needed.");
|
||||||
maybeInstall();
|
maybeInstall();
|
||||||
});
|
});
|
||||||
window.applicationCache.addEventListener('updateready', function () {
|
window.applicationCache.addEventListener('updateready', function () {
|
||||||
|
isDownloading = false;
|
||||||
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
|
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
|
||||||
window.applicationCache.swapCache();
|
window.applicationCache.swapCache();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
@@ -3,9 +3,15 @@
|
|||||||
#include <microhttpd.h>
|
#include <microhttpd.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
|
||||||
/* Shared flag — set to 0 by a successful /install, read by the main loop. */
|
/* Shared flag — set to 0 by a successful /install or /exit, read by the main loop. */
|
||||||
extern atomic_int http_keep_running;
|
extern atomic_int http_keep_running;
|
||||||
|
|
||||||
|
/* Set to 1 only when /install succeeds so shutdown knows to notify success. */
|
||||||
|
extern atomic_int install_completed;
|
||||||
|
|
||||||
|
/* Set to 1 by /clear-webkit-data, read by main loop to re-launch browser. */
|
||||||
|
extern atomic_int webkit_data_cleared;
|
||||||
|
|
||||||
/* MHD request handler callback — dispatches all routes. */
|
/* MHD request handler callback — dispatches all routes. */
|
||||||
enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
|
enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
|
||||||
const char *url, const char *method,
|
const char *url, const char *method,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <microhttpd.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#if defined(WKALI_SIMULATE_CACHE_CORRUPTION) && WKALI_SIMULATE_CACHE_CORRUPTION > 0
|
||||||
|
|
||||||
|
void simulate_corrupt_manifest(const char *url, void **payload, size_t *payload_size,
|
||||||
|
enum MHD_ResponseMemoryMode *mem_mode);
|
||||||
|
void simulate_on_clear_success(void);
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
static inline void simulate_corrupt_manifest(const char *url, void **payload,
|
||||||
|
size_t *payload_size,
|
||||||
|
enum MHD_ResponseMemoryMode *mem_mode) {
|
||||||
|
(void)url;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_size;
|
||||||
|
(void)mem_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void simulate_on_clear_success(void) {}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/* Recursively deletes all contents of the webkit/shell folder under the
|
||||||
|
* current (foreground) user's home directory. Returns 0 on success, -1 on
|
||||||
|
* error. */
|
||||||
|
int wkali_clear_webkit_data(void);
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
#define ROUTE_CACHE_MANIFEST "/cache.appcache"
|
#define ROUTE_CACHE_MANIFEST "/cache.appcache"
|
||||||
#define ROUTE_INSTALL "/install"
|
#define ROUTE_INSTALL "/install"
|
||||||
#define ROUTE_VERSION "/version"
|
#define ROUTE_VERSION "/version"
|
||||||
|
#define ROUTE_CLEAR_WEBKIT_DATA "/clear-webkit-data"
|
||||||
|
#define ROUTE_EXIT "/exit"
|
||||||
|
|
||||||
/* Logging (implementation in log.c) */
|
/* Logging (implementation in log.c) */
|
||||||
void wkali_log(const char *fmt, ...);
|
void wkali_log(const char *fmt, ...);
|
||||||
|
|||||||
+47
-1
@@ -15,6 +15,8 @@
|
|||||||
#include "file_registry.h"
|
#include "file_registry.h"
|
||||||
#include "inflate.h"
|
#include "inflate.h"
|
||||||
#include "app_installer.h"
|
#include "app_installer.h"
|
||||||
|
#include "webkit_cleaner.h"
|
||||||
|
#include "simulate_corrupt.h"
|
||||||
|
|
||||||
/* CORS is intentionally `*`: the installer page is also served from the PC
|
/* CORS is intentionally `*`: the installer page is also served from the PC
|
||||||
* host (manuals.playstation.net over HTTPS), which cross-origin XHRs this
|
* host (manuals.playstation.net over HTTPS), which cross-origin XHRs this
|
||||||
@@ -23,10 +25,18 @@
|
|||||||
* Do not restrict unless that flow changes. */
|
* Do not restrict unless that flow changes. */
|
||||||
#define CORS_ORIGIN "*"
|
#define CORS_ORIGIN "*"
|
||||||
|
|
||||||
/* Shared flag — set to 0 by a successful /install, read by the main loop.
|
/* Shared flag — set to 0 by a successful /install or /exit, read by the main loop.
|
||||||
* atomic so the store in a connection thread is visible to the main loop. */
|
* atomic so the store in a connection thread is visible to the main loop. */
|
||||||
atomic_int http_keep_running = 1;
|
atomic_int http_keep_running = 1;
|
||||||
|
|
||||||
|
/* Set to 1 only when /install succeeds so shutdown knows to notify success. */
|
||||||
|
atomic_int install_completed = 0;
|
||||||
|
|
||||||
|
/* Set to 1 by a successful /clear-webkit-data, read by the main loop to
|
||||||
|
* re-launch the browser. atomic so the store in a connection thread is
|
||||||
|
* visible to the main loop. */
|
||||||
|
atomic_int webkit_data_cleared = 0;
|
||||||
|
|
||||||
static void add_cors_headers(struct MHD_Response *resp) {
|
static void add_cors_headers(struct MHD_Response *resp) {
|
||||||
MHD_add_response_header(resp, "Access-Control-Allow-Origin", CORS_ORIGIN);
|
MHD_add_response_header(resp, "Access-Control-Allow-Origin", CORS_ORIGIN);
|
||||||
}
|
}
|
||||||
@@ -88,6 +98,7 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
|
|||||||
MHD_RESPMEM_PERSISTENT);
|
MHD_RESPMEM_PERSISTENT);
|
||||||
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
||||||
http_status = MHD_HTTP_OK;
|
http_status = MHD_HTTP_OK;
|
||||||
|
atomic_store(&install_completed, 1);
|
||||||
atomic_store(&http_keep_running, 0);
|
atomic_store(&http_keep_running, 0);
|
||||||
} else {
|
} else {
|
||||||
wkali_log("[WKALI] App install failed (%d). Staying up.\n", err);
|
wkali_log("[WKALI] App install failed (%d). Staying up.\n", err);
|
||||||
@@ -97,11 +108,25 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
|
|||||||
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
||||||
http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
|
http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
|
||||||
}
|
}
|
||||||
|
} else if (strcmp(url, ROUTE_EXIT) == 0) {
|
||||||
|
wkali_log("[WKALI] Exit requested by client. Stopping server...\n");
|
||||||
|
resp = MHD_create_response_from_buffer(2, (void *)"OK",
|
||||||
|
MHD_RESPMEM_PERSISTENT);
|
||||||
|
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
||||||
|
http_status = MHD_HTTP_OK;
|
||||||
|
atomic_store(&http_keep_running, 0);
|
||||||
} else if (strcmp(url, ROUTE_VERSION) == 0) {
|
} else if (strcmp(url, ROUTE_VERSION) == 0) {
|
||||||
resp = MHD_create_response_from_buffer(strlen(WKAL_FULL_VERSION),
|
resp = MHD_create_response_from_buffer(strlen(WKAL_FULL_VERSION),
|
||||||
(void *)WKAL_FULL_VERSION,
|
(void *)WKAL_FULL_VERSION,
|
||||||
MHD_RESPMEM_PERSISTENT);
|
MHD_RESPMEM_PERSISTENT);
|
||||||
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
||||||
|
extern int sceUserServiceGetForegroundUser(int *);
|
||||||
|
int uid = -1;
|
||||||
|
if (sceUserServiceGetForegroundUser(&uid) == 0 && uid > 0) {
|
||||||
|
char uid_hdr[32];
|
||||||
|
snprintf(uid_hdr, sizeof(uid_hdr), "%08x", (unsigned int)uid);
|
||||||
|
MHD_add_response_header(resp, "X-User-Id", uid_hdr);
|
||||||
|
}
|
||||||
} else if (strcmp(url, "/logs") == 0) {
|
} else if (strcmp(url, "/logs") == 0) {
|
||||||
const char *pos_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "pos");
|
const char *pos_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "pos");
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
@@ -120,6 +145,24 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
|
|||||||
const char *oom = "500 Internal Server Error\n";
|
const char *oom = "500 Internal Server Error\n";
|
||||||
resp = MHD_create_response_from_buffer(strlen(oom), (void *)oom, MHD_RESPMEM_PERSISTENT);
|
resp = MHD_create_response_from_buffer(strlen(oom), (void *)oom, MHD_RESPMEM_PERSISTENT);
|
||||||
}
|
}
|
||||||
|
} else if (strcmp(url, ROUTE_CLEAR_WEBKIT_DATA) == 0) {
|
||||||
|
int err = wkali_clear_webkit_data();
|
||||||
|
if (err == 0) {
|
||||||
|
wkali_log("[WKALI] WebKit data cleared successfully. Will re-launch browser.\n");
|
||||||
|
simulate_on_clear_success();
|
||||||
|
resp = MHD_create_response_from_buffer(2, (void *)"OK",
|
||||||
|
MHD_RESPMEM_PERSISTENT);
|
||||||
|
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
||||||
|
http_status = MHD_HTTP_OK;
|
||||||
|
atomic_store(&webkit_data_cleared, 1);
|
||||||
|
} else {
|
||||||
|
wkali_log("[WKALI] WebKit data clear failed.\n");
|
||||||
|
const char *fail = "Clear failed";
|
||||||
|
resp = MHD_create_response_from_buffer(strlen(fail), (void *)fail,
|
||||||
|
MHD_RESPMEM_PERSISTENT);
|
||||||
|
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
||||||
|
http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const FileEntry *entry = registry_lookup(url);
|
const FileEntry *entry = registry_lookup(url);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
@@ -223,6 +266,9 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Test simulation hook (no-op unless compiled with SIMULATE=1|2) */
|
||||||
|
simulate_corrupt_manifest(url, &payload, &payload_size, &mem_mode);
|
||||||
|
|
||||||
resp = MHD_create_response_from_buffer(payload_size, payload,
|
resp = MHD_create_response_from_buffer(payload_size, payload,
|
||||||
mem_mode);
|
mem_mode);
|
||||||
MHD_add_response_header(resp, "Content-Type", entry->content_type);
|
MHD_add_response_header(resp, "Content-Type", entry->content_type);
|
||||||
|
|||||||
+35
-2
@@ -69,6 +69,7 @@ static pid_t find_pid(const char *name) {
|
|||||||
/* PS5 System Calls (Internal) */
|
/* PS5 System Calls (Internal) */
|
||||||
extern int sceNetCtlInit();
|
extern int sceNetCtlInit();
|
||||||
extern int sceUserServiceInitialize(void *);
|
extern int sceUserServiceInitialize(void *);
|
||||||
|
extern int sceUserServiceGetForegroundUser(int *);
|
||||||
|
|
||||||
__attribute__((used)) volatile const char wkali_version_sig[] =
|
__attribute__((used)) volatile const char wkali_version_sig[] =
|
||||||
"WKALI_VER:" WKAL_FULL_VERSION;
|
"WKALI_VER:" WKAL_FULL_VERSION;
|
||||||
@@ -128,20 +129,52 @@ int main(void) {
|
|||||||
|
|
||||||
wkali_log("[WKALI] Server running. Waiting for the browser to cache content...\n");
|
wkali_log("[WKALI] Server running. Waiting for the browser to cache content...\n");
|
||||||
|
|
||||||
|
/* Query foreground user ID to pass to the frontend URL so the UI can
|
||||||
|
* display the exact /user/home/<userid>/webkit/shell/ path in prompts. */
|
||||||
|
int uid = -1;
|
||||||
|
char uid_param[32] = "";
|
||||||
|
if (sceUserServiceGetForegroundUser(&uid) == 0 && uid > 0) {
|
||||||
|
snprintf(uid_param, sizeof(uid_param), "&uid=%08x", (unsigned int)uid);
|
||||||
|
}
|
||||||
|
|
||||||
/* Launch the browser at a versioned URL so the old AppCache master entry
|
/* Launch the browser at a versioned URL so the old AppCache master entry
|
||||||
* for "/" is never served from the previous install. */
|
* for "/" is never served from the previous install. */
|
||||||
char browser_url[256];
|
char browser_url[256];
|
||||||
snprintf(browser_url, sizeof(browser_url),
|
snprintf(browser_url, sizeof(browser_url),
|
||||||
"http://127.0.0.1:%d/?v=%s", WKALI_PORT, WKAL_FULL_VERSION);
|
"http://127.0.0.1:%d/?v=%s%s", WKALI_PORT, WKAL_FULL_VERSION, uid_param);
|
||||||
ps5_launch_browser(browser_url);
|
ps5_launch_browser(browser_url);
|
||||||
|
|
||||||
/* Main loop — runs until /install succeeds (which also installs the
|
/* Main loop — runs until /install succeeds (which also installs the
|
||||||
* homescreen app) and sets http_keep_running to 0 */
|
* homescreen app) and sets http_keep_running to 0 */
|
||||||
|
int webkit_clear_attempts = 0;
|
||||||
|
|
||||||
while (atomic_load(&http_keep_running)) {
|
while (atomic_load(&http_keep_running)) {
|
||||||
|
/* Check if the frontend requested a WebKit data clear */
|
||||||
|
if (atomic_load(&webkit_data_cleared)) {
|
||||||
|
atomic_store(&webkit_data_cleared, 0);
|
||||||
|
webkit_clear_attempts++;
|
||||||
|
|
||||||
|
if (webkit_clear_attempts <= 1) {
|
||||||
|
/* Give the HTTP response time to flush before re-launching */
|
||||||
|
usleep(500000);
|
||||||
|
wkali_log("[WKALI] Re-launching browser after WebKit data clear (attempt %d)...\n",
|
||||||
|
webkit_clear_attempts);
|
||||||
|
char retry_url[256];
|
||||||
|
snprintf(retry_url, sizeof(retry_url),
|
||||||
|
"http://127.0.0.1:%d/?v=%s%s&retry=1",
|
||||||
|
WKALI_PORT, WKAL_FULL_VERSION, uid_param);
|
||||||
|
ps5_launch_browser(retry_url);
|
||||||
|
} else {
|
||||||
|
wkali_log("[WKALI] WebKit clear already attempted %d time(s), not re-launching.\n",
|
||||||
|
webkit_clear_attempts);
|
||||||
|
}
|
||||||
|
}
|
||||||
usleep(100000); /* 100ms sleep */
|
usleep(100000); /* 100ms sleep */
|
||||||
}
|
}
|
||||||
|
|
||||||
wkali_notify("WebKit Autoloader v%s cached successfully!", WKAL_FULL_VERSION);
|
if (atomic_load(&install_completed)) {
|
||||||
|
wkali_notify("WebKit Autoloader v%s cached successfully!", WKAL_FULL_VERSION);
|
||||||
|
}
|
||||||
wkali_log_wakeup();
|
wkali_log_wakeup();
|
||||||
|
|
||||||
/* Give the /logs thread half a second to wake up and flush the final logs
|
/* Give the /logs thread half a second to wake up and flush the final logs
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#include "simulate_corrupt.h"
|
||||||
|
|
||||||
|
#if defined(WKALI_SIMULATE_CACHE_CORRUPTION) && WKALI_SIMULATE_CACHE_CORRUPTION > 0
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "wkali.h"
|
||||||
|
|
||||||
|
/* Test-only: set to 1 once /clear-webkit-data succeeds. Used by the one-shot
|
||||||
|
* (mode 1) simulation so the post-clear retry download runs a clean manifest,
|
||||||
|
* mirroring a wipe actually fixing a corrupted on-disk cache. */
|
||||||
|
static atomic_int simulate_healed = 0;
|
||||||
|
|
||||||
|
void simulate_on_clear_success(void) {
|
||||||
|
#if WKALI_SIMULATE_CACHE_CORRUPTION == 1
|
||||||
|
atomic_store(&simulate_healed, 1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void simulate_corrupt_manifest(const char *url, void **payload, size_t *payload_size,
|
||||||
|
enum MHD_ResponseMemoryMode *mem_mode) {
|
||||||
|
if (strcmp(url, ROUTE_CACHE_MANIFEST) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (WKALI_SIMULATE_CACHE_CORRUPTION == 1 && atomic_load(&simulate_healed))
|
||||||
|
return;
|
||||||
|
|
||||||
|
static const char fake[] = "simulate_corrupted.bin\n";
|
||||||
|
size_t fake_len = sizeof(fake) - 1;
|
||||||
|
const char *src = (const char *)*payload;
|
||||||
|
size_t src_len = *payload_size;
|
||||||
|
|
||||||
|
/* Insert after the CACHE: header so the fake entry is treated as a regular
|
||||||
|
* cache member (and 404s). Fall back to the top of the file if the header
|
||||||
|
* is somehow missing. */
|
||||||
|
const char *marker = NULL;
|
||||||
|
for (const char *p = src; p + 6 <= src + src_len; p++) {
|
||||||
|
if (memcmp(p, "CACHE:", 6) == 0) {
|
||||||
|
marker = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char *insert_at = src;
|
||||||
|
if (marker) {
|
||||||
|
const char *nl = memchr(marker, '\n', (src + src_len) - marker);
|
||||||
|
if (nl) insert_at = nl + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t off = (size_t)(insert_at - src);
|
||||||
|
char *out = malloc(src_len + fake_len + 1);
|
||||||
|
if (!out) return;
|
||||||
|
memcpy(out, src, off);
|
||||||
|
memcpy(out + off, fake, fake_len);
|
||||||
|
memcpy(out + off + fake_len, src + off, src_len - off);
|
||||||
|
out[src_len + fake_len] = '\0';
|
||||||
|
|
||||||
|
if (*mem_mode == MHD_RESPMEM_MUST_FREE) free((void *)src);
|
||||||
|
*payload = out;
|
||||||
|
*payload_size = src_len + fake_len;
|
||||||
|
*mem_mode = MHD_RESPMEM_MUST_FREE;
|
||||||
|
|
||||||
|
wkali_log("[WKALI] SIMULATED corrupted cache: manifest now includes simulate_corrupted.bin (mode %d)\n",
|
||||||
|
WKALI_SIMULATE_CACHE_CORRUPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include "wkali.h"
|
||||||
|
#include "webkit_cleaner.h"
|
||||||
|
|
||||||
|
#define USER_HOME_BASE "/user/home"
|
||||||
|
|
||||||
|
/* Recursively delete all contents of dir_path (files + subdirs),
|
||||||
|
* but preserve dir_path itself. Returns 0 on success, -1 on error. */
|
||||||
|
static int rm_rf_contents(const char *dir_path) {
|
||||||
|
DIR *d = opendir(dir_path);
|
||||||
|
if (!d) return -1;
|
||||||
|
|
||||||
|
struct dirent *entry;
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
|
while ((entry = readdir(d)) != NULL) {
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
char path[512];
|
||||||
|
snprintf(path, sizeof(path), "%s/%s", dir_path, entry->d_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(path, &st) != 0) { ret = -1; continue; }
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
if (rm_rf_contents(path) != 0) ret = -1;
|
||||||
|
if (rmdir(path) != 0) ret = -1;
|
||||||
|
} else {
|
||||||
|
if (unlink(path) != 0) ret = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(d);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wkali_clear_webkit_data(void) {
|
||||||
|
/* Only the foreground (signed-in) user's data is cleared. */
|
||||||
|
extern int sceUserServiceGetForegroundUser(int *);
|
||||||
|
int user_id = -1;
|
||||||
|
if (sceUserServiceGetForegroundUser(&user_id) != 0 || user_id <= 0) {
|
||||||
|
wkali_log("[WKALI] Cannot determine current user\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char ws_path[512];
|
||||||
|
snprintf(ws_path, sizeof(ws_path), "%s/%08x/webkit/shell",
|
||||||
|
USER_HOME_BASE, (unsigned int)user_id);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(ws_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||||
|
wkali_log("[WKALI] No webkit/shell for current user %08x, nothing to clear\n",
|
||||||
|
(unsigned int)user_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
wkali_log("[WKALI] Clearing %s ...\n", ws_path);
|
||||||
|
if (rm_rf_contents(ws_path) == 0) {
|
||||||
|
wkali_log("[WKALI] Cleared webkit/shell for current user %08x\n",
|
||||||
|
(unsigned int)user_id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
wkali_log("[WKALI] Errors clearing %s\n", ws_path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build test ELFs that simulate a corrupted WebKit AppCache.
|
||||||
|
#
|
||||||
|
# Produces:
|
||||||
|
# installer-simulate-once.elf - every cache download fails until the user
|
||||||
|
# confirms "Clear WebKit Data & Retry", then
|
||||||
|
# the post-clear retry succeeds (tests the
|
||||||
|
# full repair flow end to end)
|
||||||
|
# installer-simulate-always.elf - every cache download always fails, so the
|
||||||
|
# retry also errors (tests the terminal
|
||||||
|
# "Cache issue persists" path)
|
||||||
|
#
|
||||||
|
# The build ends with a normal (SIMULATE=0) installer.elf restored.
|
||||||
|
#
|
||||||
|
# Usage from the repo root:
|
||||||
|
# bash tools/build_cache_corruption_test_elfs.sh
|
||||||
|
# or inside the SDK container:
|
||||||
|
# docker run --rm -v "$(pwd)":/src -w /src ps5-webkit-autoloader-sdk \
|
||||||
|
# bash tools/build_cache_corruption_test_elfs.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
build() {
|
||||||
|
local mode="$1"
|
||||||
|
if [ -x /opt/ps5-payload-sdk/bin/prospero-clang ]; then
|
||||||
|
make clean all "SIMULATE=$mode"
|
||||||
|
else
|
||||||
|
echo " (no local SDK - building via Docker ps5-webkit-autoloader-sdk)"
|
||||||
|
docker run --rm -v "$(pwd)":/src -w /src ps5-webkit-autoloader-sdk \
|
||||||
|
make clean all "SIMULATE=$mode"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== [1/3] SIMULATE=1 (fail until clear, then heal) ==="
|
||||||
|
build 1
|
||||||
|
cp installer.elf installer-simulate-once.elf
|
||||||
|
|
||||||
|
echo "=== [2/3] SIMULATE=2 (always fail) ==="
|
||||||
|
build 2
|
||||||
|
cp installer.elf installer-simulate-always.elf
|
||||||
|
|
||||||
|
echo "=== [3/3] Restoring normal SIMULATE=0 build ==="
|
||||||
|
build 0
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. Test payloads (send to PS5 via elfldr / Payload Manager):"
|
||||||
|
ls -la installer.elf installer-simulate-once.elf installer-simulate-always.elf
|
||||||
@@ -55,7 +55,7 @@ def git(*args):
|
|||||||
"""Run a git command in the repo; returns stdout stripped or ''."""
|
"""Run a git command in the repo; returns stdout stripped or ''."""
|
||||||
try:
|
try:
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
["git", *args], cwd=REPO, capture_output=True, text=True
|
["git", *args], cwd=REPO, capture_output=True, text=True, errors="replace"
|
||||||
).stdout.strip()
|
).stdout.strip()
|
||||||
except OSError:
|
except OSError:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Reference in new issue
Block a user