mirror of
https://github.com/manos555555/PS5-Suite.git
synced 2026-10-06 08:00:25 +02:00
347 lines
13 KiB
Python
347 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
# v6.1.3 hotfix patcher for payload/main.c (CRLF file, UTF-8)
|
|
import sys
|
|
|
|
PATH = "main.c"
|
|
|
|
with open(PATH, "r", encoding="utf-8", newline="") as f:
|
|
src = f.read()
|
|
|
|
patches = []
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FIX 1: auto_unmount_deleted_games — remove the kernel-wedging deletes
|
|
# ---------------------------------------------------------------------------
|
|
patches.append((
|
|
"auto_unmount: drop rmdir_recursive calls",
|
|
""" // v6.1.2: use the same safe order as the unmount command —\r
|
|
// unmount FIRST (above), then unregister (fire-and-forget —\r
|
|
// the registry IPC can stall), then delete.\r
|
|
unregister_title_async(e->d_name);\r
|
|
\r
|
|
char user_app_dir[PATH_MAX];\r
|
|
snprintf(user_app_dir, sizeof(user_app_dir), \r
|
|
"/user/app/%s", e->d_name);\r
|
|
rmdir_recursive(user_app_dir);\r
|
|
\r
|
|
char appmeta_dir[PATH_MAX];\r
|
|
snprintf(appmeta_dir, sizeof(appmeta_dir), \r
|
|
"/user/appmeta/%s", e->d_name);\r
|
|
rmdir_recursive(appmeta_dir);\r
|
|
\r
|
|
unmounted++;""",
|
|
""" // v6.1.3: same policy as full_title_cleanup — NO deletes\r
|
|
// here. ShellCore holds /user/app + /user/appmeta icons OPEN\r
|
|
// (the pic0.dds home-background case, live test 2026-10-01);\r
|
|
// every unlink()/rmdir() on them wedges the kernel and kills\r
|
|
// the whole payload — this was the Mount-Games freezer. We\r
|
|
// unmount + unregister only. The stale mount.lnk keeps the\r
|
|
// entry visible in the game list so it can be unmounted later\r
|
|
// (which unregisters it too).\r
|
|
unregister_title_async(e->d_name);\r
|
|
\r
|
|
unmounted++;""",
|
|
))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FIX 1b: update the full_title_cleanup comment to reflect the wider policy
|
|
# ---------------------------------------------------------------------------
|
|
patches.append((
|
|
"full_title_cleanup comment: policy now global",
|
|
""" // them. Leftovers are cleaned by the system or at the next Mount Games.""",
|
|
""" // them. v6.1.3: the same no-delete policy now applies to the Mount\r
|
|
// Games stale cleanup (auto_unmount_deleted_games) — zero deletes anywhere.""",
|
|
))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FIX 2: process_game — fine-grained progress so a live test pinpoints the
|
|
# exact freezing step
|
|
# ---------------------------------------------------------------------------
|
|
patches.append((
|
|
"process_game: Checking message",
|
|
""" // Copy title_id out if requested\r
|
|
if (title_id_out && tid_size > 0) {""",
|
|
""" {\r
|
|
char chk[128];\r
|
|
snprintf(chk, sizeof(chk), "Checking %s...", title_id);\r
|
|
send_progress_message(chk);\r
|
|
}\r
|
|
\r
|
|
// Copy title_id out if requested\r
|
|
if (title_id_out && tid_size > 0) {""",
|
|
))
|
|
|
|
patches.append((
|
|
"process_game: pre-mount messages",
|
|
""" mkdir(system_ex_app, 0755);\r
|
|
\r
|
|
if (is_mounted(system_ex_app)) {\r
|
|
unmount_guarded(system_ex_app, 0);\r
|
|
}\r
|
|
\r
|
|
if (mount_nullfs(game_path, system_ex_app)) {\r
|
|
return -1;\r
|
|
}""",
|
|
""" mkdir(system_ex_app, 0755);\r
|
|
\r
|
|
if (is_mounted(system_ex_app)) {\r
|
|
{\r
|
|
char chk[128];\r
|
|
snprintf(chk, sizeof(chk), "%s: unmounting stale mount...", title_id);\r
|
|
send_progress_message(chk);\r
|
|
}\r
|
|
unmount_guarded(system_ex_app, 0);\r
|
|
}\r
|
|
\r
|
|
{\r
|
|
char chk[128];\r
|
|
snprintf(chk, sizeof(chk), "%s: mounting game data...", title_id);\r
|
|
send_progress_message(chk);\r
|
|
}\r
|
|
if (mount_nullfs(game_path, system_ex_app)) {\r
|
|
{\r
|
|
char chk[128];\r
|
|
snprintf(chk, sizeof(chk), "%s: ERROR - mount failed", title_id);\r
|
|
send_progress_message(chk);\r
|
|
}\r
|
|
return -1;\r
|
|
}""",
|
|
))
|
|
|
|
patches.append((
|
|
"process_game: post-mount messages",
|
|
""" copy_dir(src_sce_sys, user_sce_sys);\r
|
|
copy_sce_sys_to_appmeta(src_sce_sys, title_id);""",
|
|
""" {\r
|
|
char chk[128];\r
|
|
snprintf(chk, sizeof(chk), "%s: copying sce_sys files...", title_id);\r
|
|
send_progress_message(chk);\r
|
|
}\r
|
|
copy_dir(src_sce_sys, user_sce_sys);\r
|
|
copy_sce_sys_to_appmeta(src_sce_sys, title_id);""",
|
|
))
|
|
|
|
patches.append((
|
|
"process_game: registration messages",
|
|
""" int reg_rc = register_title(title_id);\r
|
|
if (reg_rc == -2 || reg_rc == -3) {""",
|
|
""" {\r
|
|
char chk[128];\r
|
|
snprintf(chk, sizeof(chk), "%s: registering with system...", title_id);\r
|
|
send_progress_message(chk);\r
|
|
}\r
|
|
int reg_rc = register_title(title_id);\r
|
|
if (reg_rc == -2 || reg_rc == -3) {""",
|
|
))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FIX 3: handle_mount_games — detached worker + heartbeat (same as unmount).
|
|
# The mount_job_t typedef must precede the worker (C declares-before-use),
|
|
# so this patch inserts it together with the renamed worker signature.
|
|
# ---------------------------------------------------------------------------
|
|
patches.append((
|
|
"mount worker: typedef + rename signature",
|
|
"""void handle_mount_games(client_session_t *session) {\r
|
|
// Stream RESP_PROGRESS lines to THIS client while the scan runs\r
|
|
progress_socket_set(session->sock);\r
|
|
\r
|
|
// Serialize concurrent mounts""",
|
|
"""// v6.1.3: job state shared by the mount worker and its heartbeat thread.\r
|
|
typedef struct {\r
|
|
int client_sock;\r
|
|
int done; // atomic: heartbeat stops once the worker finishes\r
|
|
} mount_job_t;\r
|
|
\r
|
|
static void* mount_games_worker(void* arg) {\r
|
|
mount_job_t* job = (mount_job_t*)arg;\r
|
|
int sock = job->client_sock;\r
|
|
// Stream RESP_PROGRESS lines to THIS client while the scan runs\r
|
|
progress_socket_set(sock);\r
|
|
\r
|
|
// Serialize concurrent mounts""",
|
|
))
|
|
|
|
patches.append((
|
|
"mount worker: final send + heartbeat + new handler",
|
|
""" send_notification("Game Mounter\\nNo games found to mount");\r
|
|
}\r
|
|
\r
|
|
send_ok(session->sock, response);\r
|
|
progress_socket_clear(session->sock);\r
|
|
}""",
|
|
""" send_notification("Game Mounter\\nNo games found to mount");\r
|
|
}\r
|
|
\r
|
|
// done BEFORE the final OK so the heartbeat can never interleave a\r
|
|
// progress frame with the response (mirrors the unmount worker).\r
|
|
__atomic_store_n(&job->done, 1, __ATOMIC_SEQ_CST);\r
|
|
send_ok(sock, response);\r
|
|
progress_socket_clear(sock);\r
|
|
}\r
|
|
\r
|
|
// Heartbeat thread: streams a RESP_PROGRESS every 3s so the client never sits\r
|
|
// on a silent socket past its read timeout while the mount grinds on.\r
|
|
static void* mount_games_heartbeat(void* arg) {\r
|
|
mount_job_t* job = (mount_job_t*)arg;\r
|
|
struct timespec one_sec = { 1, 0 };\r
|
|
for (int i = 0; i < 40; i++) { // 40 x 3s = 2 min, then give up\r
|
|
for (int j = 0; j < 3; j++) {\r
|
|
if (__atomic_load_n(&job->done, __ATOMIC_SEQ_CST)) return NULL;\r
|
|
nanosleep(&one_sec, NULL);\r
|
|
}\r
|
|
if (__atomic_load_n(&job->done, __ATOMIC_SEQ_CST)) return NULL;\r
|
|
send_progress_message("Mount Games still working...");\r
|
|
}\r
|
|
return NULL;\r
|
|
}\r
|
|
\r
|
|
// v6.1.3: Mount Games now runs on a detached worker with its own heartbeat\r
|
|
// thread (same pattern as the unmount path). Before, the whole scan ran on\r
|
|
// the command thread: a single blocked syscall (busy vnode, wedged dlopen)\r
|
|
// froze the command loop — no heartbeats, no other commands, payload looked\r
|
|
// dead and had to be killed from the Toolbox. Now the command loop returns\r
|
|
// immediately and RESP_PROGRESS lines keep the client fed.\r
|
|
void handle_mount_games(client_session_t *session) {\r
|
|
mount_job_t* job = (mount_job_t*)malloc(sizeof(mount_job_t));\r
|
|
if (!job) {\r
|
|
send_error(session->sock, "Out of memory");\r
|
|
return;\r
|
|
}\r
|
|
memset(job, 0, sizeof(*job));\r
|
|
job->client_sock = session->sock;\r
|
|
\r
|
|
// Immediate acknowledgement: the client hears from us within\r
|
|
// milliseconds. The worker owns progress + final OK from here on.\r
|
|
progress_socket_set(session->sock);\r
|
|
send_progress_message("Starting Mount Games...");\r
|
|
\r
|
|
pthread_attr_t at;\r
|
|
pthread_attr_init(&at);\r
|
|
pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED);\r
|
|
pthread_t hb_tid;\r
|
|
bool hb_ok = (pthread_create(&hb_tid, &at, mount_games_heartbeat, job) == 0);\r
|
|
if (!hb_ok) pthread_attr_destroy(&at);\r
|
|
\r
|
|
pthread_t w_tid;\r
|
|
if (pthread_create(&w_tid, hb_ok ? NULL : &at, mount_games_worker, job) != 0) {\r
|
|
if (hb_ok) pthread_cancel(hb_tid);\r
|
|
pthread_attr_destroy(&at);\r
|
|
free(job);\r
|
|
progress_socket_clear(session->sock);\r
|
|
send_error(session->sock, "Failed to start mount worker");\r
|
|
return;\r
|
|
}\r
|
|
if (hb_ok) pthread_attr_destroy(&at);\r
|
|
// Session thread returns to the command loop immediately.\r
|
|
}""",
|
|
))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FIX 4: opt_module — never hold g_opt_mods_lock across dlopen, and never
|
|
# queue a second dlopen behind a wedged one
|
|
# ---------------------------------------------------------------------------
|
|
patches.append((
|
|
"opt_module: dlopen outside the lock + in-flight guard",
|
|
"""// dlopen a module once; returns NULL if unavailable on this loader.\r
|
|
static void *opt_module(opt_module_t m) {\r
|
|
pthread_mutex_lock(&g_opt_mods_lock);\r
|
|
if (!g_opt_mods_tried[m]) {\r
|
|
g_opt_mods_tried[m] = 1;\r
|
|
g_opt_mods[m] = dlopen(g_opt_mod_names[m], RTLD_LAZY);\r
|
|
}\r
|
|
void *h = g_opt_mods[m];\r
|
|
pthread_mutex_unlock(&g_opt_mods_lock);\r
|
|
return h;\r
|
|
}""",
|
|
"""// v6.1.3: dlopen() can WEDGE inside the loader (live test 2026-10-01: a\r
|
|
// background unreg thread stuck inside dlopen while the mount froze on\r
|
|
// g_opt_mods_lock behind it). Two hardenings:\r
|
|
// 1. never block on g_opt_mods_lock while a dlopen is in flight — fail fast\r
|
|
// (callers map NULL to -3 "module unavailable", all watchdog-guarded);\r
|
|
// 2. a wedged dlopen marks the module tried+absent forever, so every later\r
|
|
// call fails fast and no thread ever re-enters the loader.\r
|
|
static volatile int g_dlopen_in_progress = 0;\r
|
|
\r
|
|
// dlopen a module once; returns NULL if unavailable on this loader.\r
|
|
static void *opt_module(opt_module_t m) {\r
|
|
if (__atomic_load_n(&g_dlopen_in_progress, __ATOMIC_SEQ_CST))\r
|
|
return NULL; // a dlopen is wedged/in-flight — do not queue\r
|
|
pthread_mutex_lock(&g_opt_mods_lock);\r
|
|
if (!g_opt_mods_tried[m]) {\r
|
|
if (__atomic_load_n(&g_dlopen_in_progress, __ATOMIC_SEQ_CST)) {\r
|
|
pthread_mutex_unlock(&g_opt_mods_lock);\r
|
|
return NULL;\r
|
|
}\r
|
|
g_opt_mods_tried[m] = 1; // set BEFORE dlopen: a wedge = absent forever\r
|
|
__atomic_store_n(&g_dlopen_in_progress, 1, __ATOMIC_SEQ_CST);\r
|
|
pthread_mutex_unlock(&g_opt_mods_lock); // NEVER hold a lock across dlopen\r
|
|
void *h = dlopen(g_opt_mod_names[m], RTLD_LAZY);\r
|
|
__atomic_store_n(&g_dlopen_in_progress, 0, __ATOMIC_SEQ_CST);\r
|
|
pthread_mutex_lock(&g_opt_mods_lock);\r
|
|
g_opt_mods[m] = h;\r
|
|
}\r
|
|
void *h = g_opt_mods[m];\r
|
|
pthread_mutex_unlock(&g_opt_mods_lock);\r
|
|
return h;\r
|
|
}""",
|
|
))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Version bump
|
|
# ---------------------------------------------------------------------------
|
|
patches.append((
|
|
"system info version",
|
|
'"server_version=6.1.2\\n"',
|
|
'"server_version=6.1.3\\n"',
|
|
))
|
|
|
|
patches.append((
|
|
"boot notification version",
|
|
'"PS5 Upload Server v6.1.2: %s:%d - By Manos"',
|
|
'"PS5 Upload Server v6.1.3: %s:%d - By Manos"',
|
|
))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Apply with strict count checking — NOTHING is written unless every patch
|
|
# matches exactly once and all sanity checks pass.
|
|
# ---------------------------------------------------------------------------
|
|
failed = False
|
|
for name, old, new in patches:
|
|
n = src.count(old)
|
|
if n != 1:
|
|
print(f"FAIL [{name}]: expected 1 match, found {n}")
|
|
failed = True
|
|
if failed:
|
|
sys.exit(1)
|
|
|
|
for name, old, new in patches:
|
|
src = src.replace(old, new)
|
|
print(f"OK [{name}]")
|
|
|
|
# Sanity checks on the patched source (in memory).
|
|
start = src.index("static void* mount_games_worker(void* arg)")
|
|
end = src.index("static void* mount_games_heartbeat", start)
|
|
worker_body = src[start:end]
|
|
for bad in ("session->", "client_session_t"):
|
|
if bad in worker_body:
|
|
print(f"FAIL: worker body still references {bad}")
|
|
sys.exit(1)
|
|
|
|
handler_start = src.index("void handle_mount_games(client_session_t *session)")
|
|
handler_body = src[handler_start:handler_start + 2000]
|
|
for need in ("mount_games_worker", "mount_games_heartbeat", "mount_job_t"):
|
|
if need not in handler_body:
|
|
print(f"FAIL: handler missing {need}")
|
|
sys.exit(1)
|
|
|
|
# The worker body must come before the heartbeat and handler in the file
|
|
# (declaration order for the job type).
|
|
if not (start < end < handler_start):
|
|
print("FAIL: worker/heartbeat/handler out of order")
|
|
sys.exit(1)
|
|
|
|
with open(PATH, "w", encoding="utf-8", newline="") as f:
|
|
f.write(src)
|
|
|
|
print("\nAll v6.1.3 patches applied cleanly.")
|