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
|
||||
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
|
||||
|
||||
# Generated file registry
|
||||
@@ -55,6 +56,20 @@ WKAL_HOST_SOURCES := pc-host/host.py $(FRONTEND_FILES)
|
||||
CFLAGS := -Os -Wall -ffunction-sections -fdata-sections $(INCLUDES)
|
||||
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)
|
||||
|
||||
# Regenerate the version header on every build (cheap, only rewrites on change)
|
||||
|
||||
@@ -155,6 +155,82 @@
|
||||
#logContainer.error {
|
||||
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>
|
||||
|
||||
</head>
|
||||
@@ -186,6 +262,21 @@
|
||||
var progressBar = document.getElementById('progressBar');
|
||||
var progressPct = document.getElementById('progressPct');
|
||||
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 seenLogs = "";
|
||||
@@ -281,6 +372,8 @@
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
var hdrUid = xhr.getResponseHeader("X-User-Id");
|
||||
if (hdrUid) currentUid = hdrUid;
|
||||
probeOk = true;
|
||||
setCaching();
|
||||
} else {
|
||||
@@ -327,7 +420,109 @@
|
||||
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 () {
|
||||
/* 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();
|
||||
|
||||
/* The shortcut is created only once the AppCache is current, so the
|
||||
@@ -368,6 +563,7 @@
|
||||
appendLog("[AppCache] Checking for manifest...");
|
||||
});
|
||||
window.applicationCache.addEventListener('downloading', function () {
|
||||
isDownloading = true;
|
||||
appendLog("[AppCache] Downloading cache...");
|
||||
});
|
||||
window.applicationCache.addEventListener('progress', function (e) {
|
||||
@@ -380,20 +576,25 @@
|
||||
window.applicationCache.addEventListener('error', function (e) {
|
||||
appendLog("[AppCache] Error occurred! Status: " + window.applicationCache.status);
|
||||
logContainer.className = 'error';
|
||||
if (isDownloading) {
|
||||
setCacheCorrupted();
|
||||
}
|
||||
});
|
||||
window.applicationCache.addEventListener('obsolete', function (e) {
|
||||
appendLog("[AppCache] Manifest is obsolete.");
|
||||
});
|
||||
window.applicationCache.addEventListener('cached', function() {
|
||||
isDownloading = false;
|
||||
appendLog("[AppCache] Caching complete.");
|
||||
maybeInstall();
|
||||
});
|
||||
window.applicationCache.addEventListener('noupdate', function() {
|
||||
isDownloading = false;
|
||||
appendLog("[AppCache] No update needed.");
|
||||
maybeInstall();
|
||||
});
|
||||
window.applicationCache.addEventListener('updateready', function () {
|
||||
|
||||
isDownloading = false;
|
||||
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
|
||||
window.applicationCache.swapCache();
|
||||
window.location.reload();
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
#include <microhttpd.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;
|
||||
|
||||
/* 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. */
|
||||
enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
|
||||
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_INSTALL "/install"
|
||||
#define ROUTE_VERSION "/version"
|
||||
#define ROUTE_CLEAR_WEBKIT_DATA "/clear-webkit-data"
|
||||
#define ROUTE_EXIT "/exit"
|
||||
|
||||
/* Logging (implementation in log.c) */
|
||||
void wkali_log(const char *fmt, ...);
|
||||
|
||||
+47
-1
@@ -15,6 +15,8 @@
|
||||
#include "file_registry.h"
|
||||
#include "inflate.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
|
||||
* host (manuals.playstation.net over HTTPS), which cross-origin XHRs this
|
||||
@@ -23,10 +25,18 @@
|
||||
* Do not restrict unless that flow changes. */
|
||||
#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_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) {
|
||||
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_add_response_header(resp, "Content-Type", "text/plain");
|
||||
http_status = MHD_HTTP_OK;
|
||||
atomic_store(&install_completed, 1);
|
||||
atomic_store(&http_keep_running, 0);
|
||||
} else {
|
||||
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");
|
||||
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) {
|
||||
resp = MHD_create_response_from_buffer(strlen(WKAL_FULL_VERSION),
|
||||
(void *)WKAL_FULL_VERSION,
|
||||
MHD_RESPMEM_PERSISTENT);
|
||||
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) {
|
||||
const char *pos_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "pos");
|
||||
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";
|
||||
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 {
|
||||
const FileEntry *entry = registry_lookup(url);
|
||||
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,
|
||||
mem_mode);
|
||||
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) */
|
||||
extern int sceNetCtlInit();
|
||||
extern int sceUserServiceInitialize(void *);
|
||||
extern int sceUserServiceGetForegroundUser(int *);
|
||||
|
||||
__attribute__((used)) volatile const char wkali_version_sig[] =
|
||||
"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");
|
||||
|
||||
/* 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
|
||||
* for "/" is never served from the previous install. */
|
||||
char browser_url[256];
|
||||
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);
|
||||
|
||||
/* Main loop — runs until /install succeeds (which also installs the
|
||||
* homescreen app) and sets http_keep_running to 0 */
|
||||
int webkit_clear_attempts = 0;
|
||||
|
||||
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 */
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
/* 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 ''."""
|
||||
try:
|
||||
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()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
Reference in new issue
Block a user