fix: synchronize autoload lifecycle and bound loader transfers

This commit is contained in:
najdek committed 2026-10-13 00:08:13 +02:00
1 parent ab82dbf05e
commit c47b725344
22 files changed
+925 -347

No files matched your search

+4 -2
View File
@@ -22,8 +22,10 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check concurrent config, sources and history with ThreadSanitizer - name: Check concurrent config, sources, history and autoload with ThreadSanitizer
run: python3 tests/run_config_tests.py --thread-sanitize run: |
python3 tests/run_config_tests.py --thread-sanitize
python3 tests/run_autoload_tests.py --thread-sanitize
frontend: frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+17 -1
View File
@@ -144,6 +144,22 @@ A multi-file settings request validates fully first, publishes lists before scal
settings, and reports 500 if any publication fails. Earlier completed files stay settings, and reports 500 if any publication fails. Earlier completed files stay
published; per-file atomicity does not provide an all-or-nothing transaction. published; per-file atomicity does not provide an all-or-nothing transaction.
Autoload now owns one joinable worker and returns a coherent locked snapshot.
Active reset preserves waiting/countdown/execution; terminal reset cannot rerun
it. Abort/stop interrupt delays and loader waits. Sequences are parsed once from
one regular file (4095 bytes, entries up to 511 bytes, delays up to one hour);
status retains that running sequence if the file is edited. Processed counts and
per-entry outcomes distinguish success/failure/skip. Anonymous/foreign status
reads remain passive; an authorized read explicitly signals frontend readiness.
Loader connects time out after five seconds, and full transfers after thirty;
nonblocking/poll loops handle partial sends and cooperative cancellation. A
cancel cannot undo a payload already executed or bytes already sent. The actual
worker, socketpair loader recorder and real HTTP worker fixtures are part of
`make test-host`; `run_autoload_tests.py --thread-sanitize` covers concurrent
status/reset/start paths. The PS-button detector remains the existing headless
per-read implementation.
Collected arrays are limited to **4,096 items** and **64 MiB** of allocated array Collected arrays are limited to **4,096 items** and **64 MiB** of allocated array
capacity, whichever is smaller for the item type. Repository parsing accepts up capacity, whichever is smaller for the item type. Repository parsing accepts up
to **16 MiB** of input text. These bounds apply to the parser/scanner collections; to **16 MiB** of input text. These bounds apply to the parser/scanner collections;
@@ -182,7 +198,7 @@ build a candidate under `scratch/` rather than run `make clean`:
```bash ```bash
mkdir -p scratch mkdir -p scratch
docker run --rm --network=none -v "$(pwd):/src" -w /src \ docker run --rm --network=none -v "$(pwd):/src" -w /src \
ps5-payload-sdk-pldmgr make ELF=scratch/pldmgr-step5.elf ps5-payload-sdk-pldmgr make ELF=scratch/pldmgr-candidate.elf
``` ```
This command assumes the SDK image, frontend distribution, and embedded CA This command assumes the SDK image, frontend distribution, and embedded CA
+3
View File
@@ -57,7 +57,10 @@ test-host:
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_allocation_tests.py HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_allocation_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_integrity_tests.py HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_integrity_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_storage_tests.py HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_storage_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_autoload_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_launcher_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_config_tests.py HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_config_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py --autoload-only
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py --config-only HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py --config-only
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py
@@ -3,12 +3,14 @@ import { CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '../../utils/helpers' import { cn } from '../../utils/helpers'
import PayloadName from '../ui/PayloadName' import PayloadName from '../ui/PayloadName'
import { autoloadPhase, autoloadItemProgress } from '../../utils/autoloadProgress'
const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => { const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
const { t } = useTranslation() const { t } = useTranslation()
const isCountdown = status.remaining > 0 || (status.remaining === 0 && !status.current); const phase = autoloadPhase(status);
const isExecuting = status.remaining === 0 && !!status.current && status.current !== 'DONE'; const isCountdown = phase === 'waiting' || phase === 'countdown';
const isDone = status.current === 'DONE'; const isExecuting = phase === 'executing';
const isDone = phase === 'done';
const payloadList = (typeof status.list === 'string') ? status.list.split(',').filter(p => p.trim() !== '') : []; const payloadList = (typeof status.list === 'string') ? status.list.split(',').filter(p => p.trim() !== '') : [];
const listRef = useRef(null); const listRef = useRef(null);
const displayTotal = status.total > 0 ? status.total : payloadList.length; const displayTotal = status.total > 0 ? status.total : payloadList.length;
@@ -139,7 +141,7 @@ const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
> >
{t("autoload_overlay.return_btn", "Return to Dashboard")} {t("autoload_overlay.return_btn", "Return to Dashboard")}
</button> </button>
) : isCountdown ? ( ) : (isCountdown || isExecuting) ? (
<button <button
onClick={onCancel} onClick={onCancel}
autoFocus autoFocus
@@ -178,8 +180,7 @@ const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
<div className="space-y-3"> <div className="space-y-3">
{payloadList.map((name, i) => { {payloadList.map((name, i) => {
const active = !isDone && isExecuting && i === status.done; const { active, processed: done, failed, skipped } = autoloadItemProgress(status, i);
const done = isDone || i < status.done;
return ( return (
<div <div
key={i} key={i}
@@ -187,10 +188,13 @@ const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
className={cn( className={cn(
"flex items-center justify-between p-5 rounded-2xl border transition-all duration-500", "flex items-center justify-between p-5 rounded-2xl border transition-all duration-500",
active ? 'bg-ps-blue/20 border-ps-blue scale-[1.02] z-10' : active ? 'bg-ps-blue/20 border-ps-blue scale-[1.02] z-10' :
done ? 'bg-emerald-500/5 border-emerald-500/20 opacity-100' : 'bg-white/5 border-white/10 opacity-40' failed ? 'bg-red-500/5 border-red-500/20 opacity-100' :
skipped ? 'bg-white/5 border-white/10 opacity-70' :
done ? 'bg-emerald-500/5 border-emerald-500/20 opacity-100' : 'bg-white/5 border-white/10 opacity-40'
)}> )}>
<div className="flex items-center space-x-5"> <div className="flex items-center space-x-5">
{done ? <CheckCircle2 className="w-6 h-6 text-emerald-500" /> : {failed || skipped ? <AlertTriangle className="w-6 h-6 text-amber-500" /> :
done ? <CheckCircle2 className="w-6 h-6 text-emerald-500" /> :
active ? <Loader2 className="w-6 h-6 text-ps-blue animate-spin" /> : active ? <Loader2 className="w-6 h-6 text-ps-blue animate-spin" /> :
<div className="w-6 h-6 rounded-full border-2 border-white/10" />} <div className="w-6 h-6 rounded-full border-2 border-white/10" />}
<PayloadName <PayloadName
@@ -200,8 +204,8 @@ const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
/> />
</div> </div>
{done && ( {done && (
<span className="text-emerald-500 text-[10px] font-black uppercase tracking-widest italic"> <span className={cn("text-[10px] font-black uppercase tracking-widest italic", failed ? "text-red-400" : skipped ? "text-zinc-400" : "text-emerald-500")}>
{t("autoload_overlay.success", "Success")} {failed ? t("autoload_overlay.failed", "Failed") : skipped ? t("autoload_overlay.skipped", "Skipped") : t("autoload_overlay.success", "Success")}
</span> </span>
)} )}
</div> </div>
+17
View File
@@ -0,0 +1,17 @@
export function autoloadPhase(status) {
if (status.state) return status.state
if (status.current === 'DONE') return 'done'
if (status.remaining === 0 && status.current) return 'executing'
return status.remaining >= 0 ? 'countdown' : 'disabled'
}
export function autoloadItemProgress(status, index) {
const outcome = status.outcomes?.[index]
const processed = outcome ? outcome !== 'p' : autoloadPhase(status) === 'done' || index < status.done
return {
active: autoloadPhase(status) === 'executing' && status.current !== 'DELAY' && index === status.done,
processed,
failed: outcome === 'f',
skipped: outcome === 'k',
}
}
+22
View File
@@ -0,0 +1,22 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { autoloadPhase, autoloadItemProgress } from '../src/utils/autoloadProgress.js'
test('autoload phases distinguish waiting, execution delays, done and aborted', () => {
for (const state of ['waiting', 'countdown', 'executing', 'done', 'aborted', 'failed', 'disabled']) {
assert.equal(autoloadPhase({ state, remaining: 0, current: '' }), state)
}
assert.equal(autoloadPhase({ remaining: 0, current: 'DONE' }), 'done')
assert.equal(autoloadPhase({ remaining: 0, current: 'local/a' }), 'executing')
})
test('processed failed/skipped entries never appear successful and pending entries remain pending after abort', () => {
const status = { state: 'aborted', done: 3, current: '', outcomes: 'sfkp' }
assert.deepEqual(autoloadItemProgress(status, 0), { active: false, processed: true, failed: false, skipped: false })
assert.deepEqual(autoloadItemProgress(status, 1), { active: false, processed: true, failed: true, skipped: false })
assert.deepEqual(autoloadItemProgress(status, 2), { active: false, processed: true, failed: false, skipped: true })
assert.deepEqual(autoloadItemProgress(status, 3), { active: false, processed: false, failed: false, skipped: false })
})
test('sequence delays do not mark the next payload as currently launching', () => {
assert.equal(autoloadItemProgress({ state: 'executing', current: 'DELAY', done: 1, outcomes: 'sp' }, 1).active, false)
assert.equal(autoloadItemProgress({ state: 'executing', current: 'local/next', done: 1, outcomes: 'sp' }, 1).active, true)
})
+30 -9
View File
@@ -1,13 +1,34 @@
#pragma once #pragma once
#include <stddef.h>
/* typedef enum {
* Start the autoload sequence in a background thread. AUTOLOAD_DISABLED, AUTOLOAD_WAITING, AUTOLOAD_COUNTDOWN, AUTOLOAD_EXECUTING,
* Returns 0 on success, -1 on failure. AUTOLOAD_DONE, AUTOLOAD_ABORTED, AUTOLOAD_FAILED
*/ } AutoloadState;
int pldmgr_autoload_start();
void pldmgr_autoload_abort(); typedef struct {
void pldmgr_autoload_reset(); AutoloadState state;
int pldmgr_autoload_get_remaining_seconds(); int remaining, total, done, successful, failed, skipped;
long long pldmgr_autoload_get_remaining_ms(); int delay, scan_usb_payloads;
long long remaining_ms;
char current[128];
char list[4096];
char outcomes[4096]; /* Per payload: p=pending, s=success, f=failed, k=skipped. */
} AutoloadSnapshot;
/* One run per start/stop lifecycle. Repeated start is idempotent, even after
* completion/reset. Stop cooperatively cancels and joins the owned worker.
* Never call stop from the worker or a callback executing inside it. */
int pldmgr_autoload_start(void);
void pldmgr_autoload_stop(void);
void pldmgr_autoload_abort(void);
/* Ignore reset while waiting/counting/executing; clear terminal display only. */
void pldmgr_autoload_reset(void);
/* Pure readers. Signal readiness separately after authorizing frontend access. */
void pldmgr_autoload_frontend_ready(void);
void pldmgr_autoload_get_snapshot(AutoloadSnapshot *out);
const char *pldmgr_autoload_state_name(AutoloadState state);
int pldmgr_autoload_get_remaining_seconds(void);
long long pldmgr_autoload_get_remaining_ms(void);
void pldmgr_autoload_get_status(int *total, int *done, char *current); void pldmgr_autoload_get_status(int *total, int *done, char *current);
int pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename); int pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename);
+6
View File
@@ -10,6 +10,12 @@
*/ */
int ps5_launch_elf(const char* path); int ps5_launch_elf(const char* path);
/* Stop callback is checked before socket work and between send/wait chunks.
* Connect and total transfer have finite deadlines. Cancellation cannot undo
* bytes already delivered or a payload the loader has already executed. */
typedef int (*PayloadLaunchCancelled)(void *context);
int ps5_launch_elf_cancellable(const char *path, PayloadLaunchCancelled cancelled, void *context);
/** /**
* Launch the PS5 web browser with a specific URL. * Launch the PS5 web browser with a specific URL.
* @param uri The URL to open. * @param uri The URL to open.
+296 -230
View File
@@ -4,256 +4,322 @@
#include <string.h> #include <string.h>
#include <ctype.h> #include <ctype.h>
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h> #include <fcntl.h>
#include <sys/time.h> #include <time.h>
#include <errno.h>
static volatile long long countdown_end_time = 0; #include <sys/stat.h>
long long get_current_time_ms() {
struct timeval tv;
gettimeofday(&tv, NULL);
return (long long)(tv.tv_sec) * 1000 + (tv.tv_usec) / 1000;
}
#include "pldmgr.h" #include "pldmgr.h"
#include "autoload.h" #include "autoload.h"
#include "config.h" #include "config.h"
#include "payload_mgr.h" #include "json_helpers.h"
#include "slot.h" #include "slot.h"
#include "ps5_launcher.h" #include "ps5_launcher.h"
static volatile int abort_flag = 0; #ifndef PLDMGR_FRONTEND_WAIT_MS
static volatile int remaining_seconds = -1; #define PLDMGR_FRONTEND_WAIT_MS 5000
static volatile int is_executing = 0; #endif
static pthread_t autoload_thread; #define MAX_SEQUENCE_TEXT 4095
#define MAX_ENTRY_TEXT 511
#define MAX_ENTRY_DELAY_MS 3600000
static char autoload_current_name[128] = ""; /* Ownership lock serializes start/stop/join; worker uses only state_mutex.
static int autoload_total_count = 0; * Never hold state_mutex during join, filesystem access, notifications or launch. */
static int autoload_done_count = 0; static pthread_mutex_t lifecycle_mutex = PTHREAD_MUTEX_INITIALIZER;
static int autoload_triggered = 0; // Starts at 0, becomes 1 when frontend connects static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t state_cond = PTHREAD_COND_INITIALIZER;
static pthread_t worker_thread;
static int owned, running, cancel_requested, frontend_ready;
static long long countdown_end;
static AutoloadSnapshot status = {.state=AUTOLOAD_DISABLED, .remaining=-1, .remaining_ms=-1000};
int pldmgr_autoload_get_remaining_seconds() { typedef struct { char key[MAX_ENTRY_TEXT + 1]; int delay_ms; } SequenceEntry;
return remaining_seconds;
static long long now_ms(void) {
struct timespec time;
clock_gettime(CLOCK_MONOTONIC, &time);
return (long long)time.tv_sec * 1000 + time.tv_nsec / 1000000;
} }
const char *pldmgr_autoload_state_name(AutoloadState state) {
long long pldmgr_autoload_get_remaining_ms() { const char *names[] = {"disabled", "waiting", "countdown", "executing", "done", "aborted", "failed"};
if (countdown_end_time == 0) return (long long)remaining_seconds * 1000; return state >= AUTOLOAD_DISABLED && state <= AUTOLOAD_FAILED ? names[state] : "failed";
long long now = get_current_time_ms(); }
if (now >= countdown_end_time) return 0; void pldmgr_autoload_get_snapshot(AutoloadSnapshot *out) {
return countdown_end_time - now; if (!out) return;
pthread_mutex_lock(&state_mutex);
*out = status;
if (status.state == AUTOLOAD_COUNTDOWN) {
out->remaining_ms = countdown_end - now_ms();
if (out->remaining_ms < 0) out->remaining_ms = 0;
out->remaining = (int)((out->remaining_ms + 999) / 1000);
}
pthread_mutex_unlock(&state_mutex);
}
int pldmgr_autoload_get_remaining_seconds(void) {
AutoloadSnapshot snapshot; pldmgr_autoload_get_snapshot(&snapshot); return snapshot.remaining;
}
long long pldmgr_autoload_get_remaining_ms(void) {
AutoloadSnapshot snapshot; pldmgr_autoload_get_snapshot(&snapshot); return snapshot.remaining_ms;
} }
void pldmgr_autoload_get_status(int *total, int *done, char *current) { void pldmgr_autoload_get_status(int *total, int *done, char *current) {
autoload_triggered = 1; // Signal that frontend is ready/active AutoloadSnapshot snapshot; pldmgr_autoload_get_snapshot(&snapshot);
*total = autoload_total_count; if (total) *total=snapshot.total;
*done = autoload_done_count; if (done) *done=snapshot.done;
if (current) strcpy(current, autoload_current_name); if (current) memcpy(current, snapshot.current, sizeof(snapshot.current));
} }
void pldmgr_autoload_frontend_ready(void) {
void* pldmgr_autoload_worker(void* arg) { pthread_mutex_lock(&state_mutex);
struct stat st; frontend_ready=1;
int has_config = (stat(AUTOLOAD_CONFIG_PATH, &st) == 0); pthread_cond_broadcast(&state_cond);
pthread_mutex_unlock(&state_mutex);
int enabled = 0;
int browser_open = 1;
int auto_delay = 5;
FILE *ef = fopen(PLDMGR_CONFIG_PATH, "r");
if (ef) {
char line[128];
while (fgets(line, sizeof(line), ef)) {
if (strncmp(line, "AUTOLOAD_ENABLED=", 17) == 0) {
enabled = atoi(line + 17);
} else if (strncmp(line, "AUTO_BROWSER_OPEN=", 18) == 0) {
browser_open = atoi(line + 18);
} else if (strncmp(line, "AUTOLOAD_DELAY=", 15) == 0) {
auto_delay = atoi(line + 15);
}
}
fclose(ef);
}
if (!enabled || !has_config) return NULL;
remaining_seconds = auto_delay;
if (browser_open) {
pldmgr_log("[Autoload] Browser Mode: Waiting for frontend connection...\n");
/* Wait for trigger (frontend connect) or safety timeout (15s) */
int wait_timeout = 50; // 50 * 100ms = 5 seconds
while (!autoload_triggered && wait_timeout-- > 0) {
if (abort_flag) return NULL;
usleep(100000);
}
if (autoload_triggered) {
pldmgr_log("[Autoload] Frontend connected. Starting countdown.\n");
} else {
pldmgr_log("[Autoload] Frontend timeout. Starting countdown anyway.\n");
}
}
countdown_end_time = get_current_time_ms() + (long long)auto_delay * 1000;
/* Perform countdown for the specified delay in all modes */
if (auto_delay > 0) {
int klog_fd = -1;
/* Only fallback to on-screen notification and PS button if browser is NOT automatically opened */
if (!browser_open) {
if (!pldmgr_server_is_active()) {
char ip[64];
if (pldmgr_get_local_ip(ip, sizeof(ip)) != 0) strcpy(ip, "0.0.0.0");
pldmgr_notify("Payload Manager Running\nhttp://%s:%d", ip, MENU_PORT);
pldmgr_notify("Autoloading in %ds\nPress PS Button to Abort", auto_delay);
}
klog_fd = open("/dev/klog", O_RDONLY | O_NONBLOCK);
if (klog_fd >= 0) {
/* Flush existing log buffer so we only catch NEW button presses */
char flush_buf[4096];
while (read(klog_fd, flush_buf, sizeof(flush_buf)) > 0);
}
pldmgr_log("[Autoload] Fallback Mode: Starting %ds countdown (PS Button active)...\n", auto_delay);
} else {
pldmgr_log("[Autoload] Browser Mode: Starting %ds countdown...\n", auto_delay);
}
char klog_buf[2048];
long long remaining_ms;
while ((remaining_ms = countdown_end_time - get_current_time_ms()) > 0) {
remaining_seconds = (int)((remaining_ms + 999) / 1000);
if (abort_flag) {
if (klog_fd >= 0) close(klog_fd);
countdown_end_time = 0;
remaining_seconds = -1;
return NULL;
}
if (klog_fd >= 0) {
ssize_t n = read(klog_fd, klog_buf, sizeof(klog_buf) - 1);
if (n > 0) {
klog_buf[n] = 0;
if (strstr(klog_buf, "onPSButtonPressed")) {
pldmgr_log("[Autoload] ABORTED via PS Button.\n");
pldmgr_notify("Autoload Aborted");
abort_flag = 1;
close(klog_fd);
countdown_end_time = 0;
remaining_seconds = -1;
return NULL;
}
}
}
usleep(100000); /* 100ms */
}
if (klog_fd >= 0) close(klog_fd);
}
countdown_end_time = 0;
remaining_seconds = 0;
is_executing = 1;
FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r");
if (!f) {
pldmgr_log("[Autoload] !!! Failed to open %s\n", AUTOLOAD_CONFIG_PATH);
remaining_seconds = -1;
return NULL;
}
pldmgr_log("[Autoload] Starting sequence...\n");
/* Count total first for UI */
autoload_total_count = 0;
autoload_done_count = 0;
char count_line[256];
while (fgets(count_line, sizeof(count_line), f)) {
if (count_line[0] != '!' && strlen(count_line) > 1) autoload_total_count++;
}
rewind(f);
char line[256];
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = 0;
char *start = line;
while (*start && isspace((unsigned char)*start)) start++;
char *end = start + strlen(start);
while (end > start && isspace((unsigned char)*(end - 1))) end--;
*end = '\0';
if (strlen(start) == 0) continue;
if (start[0] == '!') {
int delay = atoi(start + 1);
if (delay > 0) {
pldmgr_log("[Autoload] Delaying for %d ms...\n", delay);
usleep(delay * 1000);
}
} else {
char full_path[512];
if (is_pldmgr_name(start)) {
pldmgr_log("[Autoload] Skipping self: %s\n", start);
continue;
}
if (slot_resolve(start, full_path, sizeof(full_path)) == 0) {
if (is_pldmgr_name(full_path)) {
pldmgr_log("[Autoload] Skipping self: %s\n", full_path);
continue;
}
strncpy(autoload_current_name, start, sizeof(autoload_current_name) - 1);
pldmgr_log("[Autoload] Launching: %s\n", full_path);
ps5_launch_elf(full_path);
autoload_done_count++;
usleep(500000); /* UI visibility */
} else {
pldmgr_log("[Autoload] !!! Payload not found: %s\n", line);
}
}
}
fclose(f);
pldmgr_log("[Autoload] Sequence complete.\n");
strcpy(autoload_current_name, "DONE");
remaining_seconds = 0;
is_executing = 0;
return NULL;
} }
static int cancelled(void *context) {
(void)context;
pthread_mutex_lock(&state_mutex);
int result=cancel_requested;
pthread_mutex_unlock(&state_mutex);
return result;
}
/* Timed condition waits allow prompt stop without changing countdown deadlines. */
static int wait_until(long long end, int waiting_frontend) {
pthread_mutex_lock(&state_mutex);
while (!cancel_requested && !(waiting_frontend && frontend_ready) && now_ms() < end) {
struct timespec wake;
clock_gettime(CLOCK_REALTIME, &wake);
wake.tv_nsec += 100000000;
if (wake.tv_nsec >= 1000000000) { wake.tv_sec++; wake.tv_nsec -= 1000000000; }
pthread_cond_timedwait(&state_cond, &state_mutex, &wake);
}
int result=cancel_requested;
pthread_mutex_unlock(&state_mutex);
return result;
}
/* Parse the exact sequence once. Counters and UI list derive from these entries,
* not a second pass with different whitespace/delay rules or a newer file. */
static int read_sequence(SequenceEntry **out, size_t *count, char list[4096]) {
*out=NULL; *count=0; list[0]=0;
int fd=open(AUTOLOAD_CONFIG_PATH,O_RDONLY|O_NONBLOCK);
if (fd<0) return errno==ENOENT ? 1 : -1;
struct stat info;
if (fstat(fd,&info) || !S_ISREG(info.st_mode) || info.st_size<0 || info.st_size>MAX_SEQUENCE_TEXT) {
close(fd); return -1;
}
FILE *file=fdopen(fd,"rb");
if (!file) { close(fd); return -1; }
char text[MAX_SEQUENCE_TEXT+1];
size_t size=fread(text,1,sizeof(text),file);
int failed=ferror(file);
if (fclose(file)) failed=1;
if (failed || size>MAX_SEQUENCE_TEXT || memchr(text,0,size)) return -1;
text[size]=0;
size_t capacity=1;
for (size_t i=0;i<size;i++) if (text[i]=='\n') capacity++;
SequenceEntry *entries=calloc(capacity, sizeof(*entries));
if (!entries) return -1;
char *save=NULL; size_t pos=0;
for (char *line=strtok_r(text,"\n",&save);line;line=strtok_r(NULL,"\n",&save)) {
while (*line && isspace((unsigned char)*line)) line++;
char *end=line+strlen(line);
while (end>line && isspace((unsigned char)end[-1])) *--end=0;
size_t length=strlen(line);
if (!length) continue;
if (length > MAX_ENTRY_TEXT || strpbrk(line,"\r,")) goto invalid;
SequenceEntry *entry=&entries[*count];
entry->delay_ms=-1;
if (*line=='!') {
if (length==1) goto invalid;
long delay=0;
for (const char *p=line+1;*p;p++) {
if (*p<'0' || *p>'9' || delay > MAX_ENTRY_DELAY_MS/10) goto invalid;
delay=delay*10+*p-'0';
if (delay > MAX_ENTRY_DELAY_MS) goto invalid;
}
entry->delay_ms=(int)delay;
} else {
size_t needed=length+(pos?1:0);
if (needed >= 4096-pos) goto invalid;
if (pos) list[pos++]=',';
memcpy(list+pos,line,length); pos+=length; list[pos]=0;
}
memcpy(entry->key,line,length+1);
(*count)++;
}
*out=entries; return 0;
invalid:
free(entries); *count=0; list[0]=0; return -1;
}
static void set_current(const char *key) {
size_t length=strlen(key);
if (length>=sizeof(status.current)) {
length=sizeof(status.current)-1;
while (length && ((unsigned char)key[length]&0xc0)==0x80) length--;
}
memcpy(status.current,key,length); status.current[length]=0;
}
static void finish(AutoloadState state) {
pthread_mutex_lock(&state_mutex);
status.state=cancel_requested ? AUTOLOAD_ABORTED : state;
status.remaining=status.state==AUTOLOAD_DONE ? 0 : -1;
status.remaining_ms=(long long)status.remaining*1000;
snprintf(status.current,sizeof(status.current),"%s",status.state==AUTOLOAD_DONE ? "DONE" : "");
countdown_end=0;
running=0;
pthread_cond_broadcast(&state_cond);
pthread_mutex_unlock(&state_mutex);
}
static void *autoload_worker(void *argument) {
(void)argument;
SequenceEntry *entries=NULL; size_t count=0;
char list[4096]={0};
PldmgrConfig cfg; config_read(&cfg);
if (!cfg.autoload_enabled) { finish(AUTOLOAD_DISABLED); return NULL; }
int parsed=read_sequence(&entries,&count,list);
if (parsed) {
if (parsed<0) pldmgr_log("[Autoload] Invalid or unreadable sequence; no payloads launched.\n");
finish(parsed>0 ? AUTOLOAD_DISABLED : AUTOLOAD_FAILED); return NULL;
}
pthread_mutex_lock(&state_mutex);
status.delay=cfg.autoload_delay;
status.scan_usb_payloads=cfg.scan_usb_payloads;
memcpy(status.list,list,sizeof(list));
for (size_t i=0;i<count;i++) if (entries[i].delay_ms<0) status.outcomes[status.total++]='p';
status.state=AUTOLOAD_WAITING;
status.remaining=cfg.autoload_delay;
status.remaining_ms=(long long)cfg.autoload_delay*1000;
pthread_mutex_unlock(&state_mutex);
if (cfg.auto_browser_open) {
pldmgr_log("[Autoload] Browser Mode: Waiting for frontend connection...\n");
if (wait_until(now_ms()+PLDMGR_FRONTEND_WAIT_MS,1)) goto aborted;
}
if (cancelled(NULL)) goto aborted;
pthread_mutex_lock(&state_mutex);
status.state=AUTOLOAD_COUNTDOWN;
countdown_end=now_ms()+(long long)cfg.autoload_delay*1000;
long long end=countdown_end;
pthread_mutex_unlock(&state_mutex);
int pldmgr_autoload_start() { /* Preserve the existing PS-button mode, prompt/drain ordering and per-read
abort_flag = 0; * matching. The user explicitly withdrew the separate detection changes. */
is_executing = 0; int klog_fd=-1;
if (pthread_create(&autoload_thread, NULL, pldmgr_autoload_worker, NULL) != 0) { if (cfg.autoload_delay>0 && !cfg.auto_browser_open) {
pldmgr_log("[Autoload] !!! Failed to create background thread\n"); if (!pldmgr_server_is_active()) {
return -1; char ip[64];
if (pldmgr_get_local_ip(ip,sizeof(ip))!=0) strcpy(ip,"0.0.0.0");
pldmgr_notify("Payload Manager Running\nhttp://%s:%d",ip,MENU_PORT);
pldmgr_notify("Autoloading in %ds\nPress PS Button to Abort",cfg.autoload_delay);
}
klog_fd=open("/dev/klog",O_RDONLY|O_NONBLOCK);
if (klog_fd>=0) {
char flush[4096];
while (read(klog_fd,flush,sizeof(flush))>0) {
if (cancelled(NULL)) { close(klog_fd); goto aborted; }
}
}
pldmgr_log("[Autoload] Fallback Mode: Starting %ds countdown (PS Button active)...\n",cfg.autoload_delay);
} else if (cfg.autoload_delay>0) {
pldmgr_log("[Autoload] Browser Mode: Starting %ds countdown...\n",cfg.autoload_delay);
}
while (now_ms()<end) {
if (cancelled(NULL)) { if(klog_fd>=0)close(klog_fd); goto aborted; }
if (klog_fd>=0) {
char log[2048]; ssize_t n=read(klog_fd,log,sizeof(log)-1);
if (n>0) {
log[n]=0;
if (strstr(log,"onPSButtonPressed")) {
pldmgr_log("[Autoload] ABORTED via PS Button.\n");
pldmgr_notify("Autoload Aborted");
pldmgr_autoload_abort(); close(klog_fd); goto aborted;
}
}
}
long long tick=now_ms()+100;
if (tick>end) tick=end;
if (wait_until(tick,0)) { if(klog_fd>=0)close(klog_fd); goto aborted; }
}
if (klog_fd>=0) close(klog_fd);
if (cancelled(NULL)) goto aborted;
pthread_mutex_lock(&state_mutex);
status.state=AUTOLOAD_EXECUTING; status.remaining=0; status.remaining_ms=0; countdown_end=0;
pthread_mutex_unlock(&state_mutex);
pldmgr_log("[Autoload] Starting sequence...\n");
int payload_index=0;
for (size_t i=0;i<count;i++) {
if (cancelled(NULL)) goto aborted;
SequenceEntry *entry=&entries[i];
if (entry->delay_ms>=0) {
pthread_mutex_lock(&state_mutex);
snprintf(status.current,sizeof(status.current),"DELAY");
pthread_mutex_unlock(&state_mutex);
if (wait_until(now_ms()+entry->delay_ms,0)) goto aborted;
continue;
}
pthread_mutex_lock(&state_mutex);
set_current(entry->key);
pthread_mutex_unlock(&state_mutex);
int skipped=0, failed=0;
char path[512];
if (is_pldmgr_name(entry->key)) skipped=1;
else if (slot_resolve(entry->key,path,sizeof(path))) failed=1;
else if (is_pldmgr_name(path)) skipped=1;
else {
pldmgr_log("[Autoload] Launching: %s\n",path);
if (ps5_launch_elf_cancellable(path,cancelled,NULL)) failed=1;
}
if (skipped) pldmgr_log("[Autoload] Skipping self: %s\n",entry->key);
if (failed) pldmgr_log("[Autoload] Missing, failed or cancelled payload: %s\n",entry->key);
pthread_mutex_lock(&state_mutex);
status.done++;
status.outcomes[payload_index++]=skipped ? 'k' : failed ? 'f' : 's';
status.skipped+=skipped;
status.failed+=failed;
status.successful+=!skipped && !failed;
pthread_mutex_unlock(&state_mutex);
if (cancelled(NULL)) goto aborted;
if (wait_until(now_ms()+500,0)) goto aborted;
} }
pthread_detach(autoload_thread); free(entries); finish(AUTOLOAD_DONE);
return 0; pldmgr_log("[Autoload] Sequence complete.\n"); return NULL;
aborted:
free(entries); finish(AUTOLOAD_ABORTED);
pldmgr_log("[Autoload] Sequence aborted.\n"); return NULL;
}
int pldmgr_autoload_start(void) {
pthread_mutex_lock(&lifecycle_mutex);
if (owned) { pthread_mutex_unlock(&lifecycle_mutex); return 0; }
pthread_mutex_lock(&state_mutex);
status=(AutoloadSnapshot){.state=AUTOLOAD_WAITING,.remaining=-1,.remaining_ms=-1000};
cancel_requested=0; frontend_ready=0; countdown_end=0; running=1;
int result=pthread_create(&worker_thread,NULL,autoload_worker,NULL);
if (result) { running=0;status.state=AUTOLOAD_FAILED; }
else owned=1;
pthread_mutex_unlock(&state_mutex);
pthread_mutex_unlock(&lifecycle_mutex);
if (result) pldmgr_log("[Autoload] Failed to create worker: %s\n",strerror(result));
return result ? -1 : 0;
}
void pldmgr_autoload_abort(void) {
pthread_mutex_lock(&state_mutex);
if (running) { cancel_requested=1; pthread_cond_broadcast(&state_cond); }
pthread_mutex_unlock(&state_mutex);
} }
void pldmgr_autoload_stop(void) {
void pldmgr_autoload_abort() { pthread_mutex_lock(&lifecycle_mutex);
if (!is_executing) { if (owned) {
abort_flag = 1; pldmgr_autoload_abort();
pthread_join(worker_thread,NULL);
owned=0;
} }
pthread_mutex_unlock(&lifecycle_mutex);
} }
void pldmgr_autoload_reset(void) {
void pldmgr_autoload_reset() { pthread_mutex_lock(&state_mutex);
/* If already counting down, don't reset to avoid jumps. if (!running) {
* The browser launch often triggers a system 'resume' which would snap time back. status=(AutoloadSnapshot){.state=AUTOLOAD_DISABLED,.remaining=-1,.remaining_ms=-1000};
* We only reset if the countdown has already finished or hasn't started. */ countdown_end=0;
if (countdown_end_time > 0 && remaining_seconds > 0) {
pldmgr_log("[PLDMGR] Autoload reset ignored (timer active)\n");
return;
} }
pthread_mutex_unlock(&state_mutex);
pldmgr_log("[PLDMGR] Autoload reset triggered\n");
remaining_seconds = -1;
is_executing = 0;
autoload_total_count = 0;
autoload_done_count = 0;
autoload_triggered = 0;
strcpy(autoload_current_name, "");
} }
int pldmgr_autoload_update_config_entry(const char *old_key,const char *new_key) {
int pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename) { int result=config_update_list_entry(AUTOLOAD_CONFIG_PATH,old_key,new_key);
int result = config_update_list_entry(AUTOLOAD_CONFIG_PATH, old_filename, new_filename); if (result) pldmgr_log("[Autoload] Failed to update entry: %s\n",old_key ? old_key : "");
if (result) pldmgr_log("[Autoload] Failed to update entry: %s\n", old_filename ? old_filename : "");
return result; return result;
} }
+12 -21
View File
@@ -962,33 +962,24 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
MHD_RESPMEM_MUST_COPY); MHD_RESPMEM_MUST_COPY);
add_response_header(resp, "Content-Type", "text/plain"); add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, ROUTE_AUTOLOAD_STATUS) == 0) { } else if (strcmp(url, ROUTE_AUTOLOAD_STATUS) == 0) {
int total, done; if (mutation_authorized(conn)) pldmgr_autoload_frontend_ready();
char current[128]; AutoloadSnapshot snapshot;
pldmgr_autoload_get_status(&total, &done, current); pldmgr_autoload_get_snapshot(&snapshot);
int remaining = pldmgr_autoload_get_remaining_seconds(); char current_escaped[6 * 128], list_escaped[6 * 4096];
long long remaining_ms = pldmgr_autoload_get_remaining_ms(); if (pldmgr_json_escape(snapshot.current,current_escaped,sizeof(current_escaped)) ||
pldmgr_json_escape(snapshot.list,list_escaped,sizeof(list_escaped)))
char list_buf[4096]; return send_request_error(conn,MHD_HTTP_INTERNAL_SERVER_ERROR,"Invalid autoload snapshot");
if (read_autoload_list(list_buf, sizeof(list_buf), 0) != 0)
return send_request_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"Autoload list is unreadable or exceeds the size limit");
PldmgrConfig cfg;
config_read(&cfg);
char current_escaped[6 * 128];
char list_escaped[6 * 4096];
pldmgr_json_escape(current, current_escaped, sizeof(current_escaped));
pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped));
char *resp_buf; char *resp_buf;
if (!alloc_response_buffer_small(&resp_buf)) if (!alloc_response_buffer_small(&resp_buf))
return send_out_of_memory(conn); return send_out_of_memory(conn);
json_format(resp_buf, SMALL_RESPONSE_BUFFER_SIZE, json_format(resp_buf, SMALL_RESPONSE_BUFFER_SIZE,
"{\"remaining\":%d,\"remaining_ms\":%lld,\"total\":%d,\"done\":%d,\"current\":\"%s\"," "{\"remaining\":%d,\"remaining_ms\":%lld,\"total\":%d,\"done\":%d,\"current\":\"%s\","
"\"list\":\"%s\",\"delay\":%d,\"SCAN_USB_PAYLOADS\":%s}", "\"list\":\"%s\",\"delay\":%d,\"SCAN_USB_PAYLOADS\":%s,"
remaining, remaining_ms, total, done, current_escaped, list_escaped, cfg.autoload_delay, "\"state\":\"%s\",\"successful\":%d,\"failed\":%d,\"skipped\":%d,\"outcomes\":\"%s\"}",
cfg.scan_usb_payloads ? "true" : "false"); snapshot.remaining, snapshot.remaining_ms, snapshot.total, snapshot.done, current_escaped,
list_escaped, snapshot.delay, snapshot.scan_usb_payloads ? "true" : "false",
pldmgr_autoload_state_name(snapshot.state),snapshot.successful,snapshot.failed,snapshot.skipped,snapshot.outcomes);
if (!resp_buf[0]) return send_serialization_error(conn, resp_buf); if (!resp_buf[0]) return send_serialization_error(conn, resp_buf);
resp = create_buffer_response(strlen(resp_buf), resp = create_buffer_response(strlen(resp_buf),
(void *)resp_buf, (void *)resp_buf,
+2 -1
View File
@@ -5,6 +5,7 @@
#include <pthread.h> #include <pthread.h>
#include <errno.h> #include <errno.h>
#include <time.h> #include <time.h>
#include <stdatomic.h>
#include "log_server.h" #include "log_server.h"
#include "utils.h" #include "utils.h"
@@ -18,7 +19,7 @@ static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t log_cond = PTHREAD_COND_INITIALIZER; static pthread_cond_t log_cond = PTHREAD_COND_INITIALIZER;
static int log_updated = 0; static int log_updated = 0;
static volatile int server_active_flag = 0; static _Atomic int server_active_flag = 0;
void log_server_set_active(void) { server_active_flag = 1; } void log_server_set_active(void) { server_active_flag = 1; }
int pldmgr_server_is_active(void) { return server_active_flag; } int pldmgr_server_is_active(void) { return server_active_flag; }
+2
View File
@@ -264,6 +264,8 @@ int main(int argc, char *argv[]) {
} }
pldmgr_log("[PLDMGR] Shutting down...\n"); pldmgr_log("[PLDMGR] Shutting down...\n");
pldmgr_autoload_stop();
if (daemon) if (daemon)
MHD_stop_daemon(daemon); MHD_stop_daemon(daemon);
+85 -65
View File
@@ -14,73 +14,93 @@
#include <unistd.h> #include <unistd.h>
#include <stdint.h> #include <stdint.h>
#include <errno.h>
#include <time.h>
#include <poll.h>
int ps5_launch_elf(const char *path) { #ifndef PLDMGR_LOADER_CONNECT_MS
char canonical[PATH_MAX]; #define PLDMGR_LOADER_CONNECT_MS 5000
int fd = slot_open_payload(path, canonical, sizeof(canonical)); #endif
if (fd < 0) { #ifndef PLDMGR_LOADER_TRANSFER_MS
pldmgr_log("[PLDMGR] Payload path rejected: %s\n", path ? path : "(null)"); #define PLDMGR_LOADER_TRANSFER_MS 30000
return -1; #endif
} static long long launch_now_ms(void) {
pldmgr_log("[PLDMGR] Sending ELF to local loader: %s\n", canonical); struct timespec time; clock_gettime(CLOCK_MONOTONIC, &time);
size_t total_sent = 0; return (long long)time.tv_sec*1000+time.tv_nsec/1000000;
/* Create socket to local elfldr */
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
close(fd);
return -1;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(ELFLDR_PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
pldmgr_log("[PLDMGR] !!! Connection to elfldr (port %d) failed. Is it "
"running?\n",
ELFLDR_PORT);
close(sock);
close(fd);
return -1;
}
/* Stream the file */
char buffer[8192];
ssize_t read_bytes;
int error = 0;
while ((read_bytes = read(fd, buffer, sizeof(buffer))) > 0) {
size_t offset = 0;
while (offset < (size_t)read_bytes) {
ssize_t sent = send(sock, buffer + offset, (size_t)read_bytes - offset, 0);
if (sent <= 0) {
pldmgr_log("[PLDMGR] !!! send failed while launching payload\n");
error = 1;
break;
}
offset += (size_t)sent;
total_sent += (size_t)sent;
}
if (error)
break;
}
if (read_bytes < 0) {
pldmgr_log("[PLDMGR] !!! read failed while launching payload\n");
error = 1;
}
pldmgr_log("[PLDMGR] Sent %zu bytes to loader.\n", total_sent);
close(sock);
close(fd);
if (!error) {
history_mgr_add(canonical);
}
return error ? -1 : 0;
} }
static int launch_cancelled(PayloadLaunchCancelled callback, void *context) {
if (callback && callback(context)) { errno=ECANCELED; return 1; }
return 0;
}
static int wait_writable(int fd, long long deadline, PayloadLaunchCancelled cancelled, void *context) {
for (;;) {
if (launch_cancelled(cancelled,context)) return -1;
long long left=deadline-launch_now_ms();
if (left<=0) { errno=ETIMEDOUT; return -1; }
struct pollfd socket={.fd=fd,.events=POLLOUT};
int ready=poll(&socket,1,left>100 ? 100 : (int)left);
if (ready>0) {
if (socket.revents & (POLLERR|POLLHUP|POLLNVAL)) { errno=EIO; return -1; }
if (socket.revents & POLLOUT) return 0;
} else if (ready<0 && errno!=EINTR) return -1;
}
}
int ps5_launch_elf_cancellable(const char *path, PayloadLaunchCancelled cancelled, void *context) {
if (launch_cancelled(cancelled,context)) return -1;
char canonical[PATH_MAX];
int fd=slot_open_payload(path,canonical,sizeof(canonical));
if (fd<0) { pldmgr_log("[PLDMGR] Payload path rejected: %s\n",path ? path : "(null)"); return -1; }
int sock=socket(AF_INET,SOCK_STREAM,0);
if (sock<0) { close(fd); return -1; }
int flags=fcntl(sock,F_GETFL,0);
int error=flags<0 || fcntl(sock,F_SETFL,flags|O_NONBLOCK)<0;
struct sockaddr_in address={0};
address.sin_family=AF_INET;
address.sin_port=htons(ELFLDR_PORT);
address.sin_addr.s_addr=inet_addr("127.0.0.1");
long long deadline=launch_now_ms()+PLDMGR_LOADER_CONNECT_MS;
if (!error && !launch_cancelled(cancelled,context)) {
if (connect(sock,(struct sockaddr *)&address,sizeof(address))<0) {
if (errno!=EINPROGRESS && errno!=EWOULDBLOCK && errno!=EINTR) error=1;
else if (wait_writable(sock,deadline,cancelled,context)) error=1;
else {
int socket_error=0; socklen_t length=sizeof(socket_error);
if (getsockopt(sock,SOL_SOCKET,SO_ERROR,&socket_error,&length) || socket_error) error=1;
}
}
} else error=1;
if (error) pldmgr_log("[PLDMGR] Loader connection failed, timed out, or cancelled.\n");
size_t total=0;
deadline=launch_now_ms()+PLDMGR_LOADER_TRANSFER_MS;
char buffer[8192];
while (!error) {
if (launch_cancelled(cancelled,context) || launch_now_ms()>=deadline) { error=1; break; }
ssize_t bytes=read(fd,buffer,sizeof(buffer));
if (bytes==0) break;
if (bytes<0) { if(errno==EINTR)continue; error=1; break; }
size_t offset=0;
while (offset<(size_t)bytes) {
if (launch_cancelled(cancelled,context) || launch_now_ms()>=deadline) { error=1; break; }
#ifdef MSG_NOSIGNAL
int send_flags=MSG_NOSIGNAL;
#else
int send_flags=0; /* main also ignores SIGPIPE on PS5 */
#endif
ssize_t sent=send(sock,buffer+offset,(size_t)bytes-offset,send_flags);
if (sent>0) { offset+=(size_t)sent; total+=(size_t)sent; }
else if (sent<0 && errno==EINTR) continue;
else if (sent<0 && (errno==EAGAIN || errno==EWOULDBLOCK)) {
if (wait_writable(sock,deadline,cancelled,context)) { error=1; break; }
} else { error=1; break; }
}
}
if (!total) error=1;
close(sock); close(fd);
if (!error) history_mgr_add(canonical);
pldmgr_log("[PLDMGR] Loader transfer %s (%zu bytes sent).\n",error ? "failed or cancelled" : "complete",total);
return error ? -1 : 0;
}
int ps5_launch_elf(const char *path) { return ps5_launch_elf_cancellable(path,NULL,NULL); }
extern int sceSystemServiceLaunchWebBrowser(const char *uri); extern int sceSystemServiceLaunchWebBrowser(const char *uri);
+151
View File
@@ -0,0 +1,151 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <pthread.h>
#include <stdatomic.h>
#include "pldmgr.h"
#include "autoload.h"
#include "config.h"
#include "json_helpers.h"
#include "ps5_launcher.h"
#define CHECK(x) do { if (!(x)) { fprintf(stderr,"FAIL %d: %s\n",__LINE__,#x); exit(1); } } while(0)
static _Atomic int launches, blocked, released, entered, fail_thread, fail_alloc, created, joined;
void *autoload_test_calloc(size_t count,size_t size) { return fail_alloc ? NULL : calloc(count,size); }
int autoload_test_pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*fn)(void *),void *arg) {
if(fail_thread) return EAGAIN;
int rc=pthread_create(thread,attr,fn,arg); if(!rc)created++; return rc;
}
int autoload_test_pthread_join(pthread_t thread,void **result) { int rc=pthread_join(thread,result); if(!rc)joined++;return rc; }
void pldmgr_log(const char *fmt,...) { (void)fmt; }
void pldmgr_notify(const char *fmt,...) { (void)fmt; }
int pldmgr_server_is_active(void) { return 1; }
int slot_resolve(const char *key,char *out,size_t size) {
if(!strcmp(key,"local/missing"))return -1;
snprintf(out,size,"/fixture/%s",key);return 0;
}
int ps5_launch_elf_cancellable(const char *path,PayloadLaunchCancelled cancelled,void *context) {
entered++;
while(blocked && !released) { if(cancelled && cancelled(context))return -1;usleep(1000); }
if(cancelled && cancelled(context))return -1;
if(strstr(path,"fail"))return -1;
launches++;return 0;
}
int ps5_launch_elf(const char *path) { return ps5_launch_elf_cancellable(path,NULL,NULL); }
static long long now_ms(void) { struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (long long)t.tv_sec*1000+t.tv_nsec/1000000; }
static void setup(const char *list,int browser,int delay) {
PldmgrConfig cfg={.autoload_enabled=1,.auto_browser_open=browser,.autoload_delay=delay};
CHECK(!config_write(&cfg)); CHECK(!write_file_text(AUTOLOAD_CONFIG_PATH,list,strlen(list)));
}
static AutoloadSnapshot wait_state(AutoloadState wanted) {
long long end=now_ms()+6000; AutoloadSnapshot snapshot;
do { pldmgr_autoload_get_snapshot(&snapshot);if(snapshot.state==wanted)return snapshot;usleep(1000); } while(now_ms()<end);
fprintf(stderr,"wanted state %d, got %d (%s)\n",wanted,snapshot.state,snapshot.current);exit(1);
}
static void wait_entered(void) { long long end=now_ms()+2000;while(!entered && now_ms()<end)usleep(1000);CHECK(entered); }
static void counters(void) {
setup("\r\n !0\r\n\t\nlocal/ok\r\n pldmgr.elf \nlocal/missing\nlocal/fail",0,0);
CHECK(!pldmgr_autoload_start()); AutoloadSnapshot snapshot=wait_state(AUTOLOAD_DONE);
CHECK(snapshot.total==4); CHECK(snapshot.done==4 && snapshot.successful==1 && snapshot.failed==2 && snapshot.skipped==1);
CHECK(!strcmp(snapshot.list,"local/ok,pldmgr.elf,local/missing,local/fail")); CHECK(!strcmp(snapshot.outcomes,"skff")); CHECK(launches==1);
CHECK(!pldmgr_autoload_start()); CHECK(created==1);pldmgr_autoload_reset();CHECK(!pldmgr_autoload_start());CHECK(created==1 && launches==1);
pldmgr_autoload_stop();CHECK(joined==1);
puts("Autoload counters: CRLF/blanks/indented delays/final line, skip/fail/success, repeat start and terminal reset passed");
}
static void active_reset(void) {
setup("local/ok",1,1);CHECK(!pldmgr_autoload_start());
AutoloadSnapshot snapshot=wait_state(AUTOLOAD_WAITING);
/* WAITING is initially visible before config loading; wait for parsed data. */
long long end=now_ms()+2000;while(snapshot.total!=1 && now_ms()<end){usleep(1000);pldmgr_autoload_get_snapshot(&snapshot);}CHECK(snapshot.total==1);
pldmgr_autoload_reset(); pldmgr_autoload_get_snapshot(&snapshot);CHECK(snapshot.state==AUTOLOAD_WAITING && snapshot.total==1);
pldmgr_autoload_frontend_ready(); snapshot=wait_state(AUTOLOAD_COUNTDOWN);CHECK(snapshot.remaining>0);
long long first=snapshot.remaining_ms;usleep(20000);pldmgr_autoload_reset();pldmgr_autoload_get_snapshot(&snapshot);CHECK(snapshot.state==AUTOLOAD_COUNTDOWN && snapshot.remaining_ms<=first);
CHECK(!pldmgr_autoload_start());CHECK(created==1);pldmgr_autoload_abort();wait_state(AUTOLOAD_ABORTED);pldmgr_autoload_stop();CHECK(!launches && joined==1);
puts("Autoload active reset: waiting/countdown survive reset, deadline advances, abort prevents launch");
}
static void cancellable(const char *which) {
if(!strcmp(which,"wait")) { setup("local/ok",1,0);CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_WAITING); }
else if(!strcmp(which,"delay")) { setup("!60000\nlocal/ok",0,0);CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_EXECUTING); }
else { blocked=1;setup("local/ok\nlocal/next",0,0);CHECK(!pldmgr_autoload_start());wait_entered(); }
pldmgr_autoload_reset();
long long begin=now_ms(); pldmgr_autoload_stop();CHECK(now_ms()-begin<1000);AutoloadSnapshot snapshot;pldmgr_autoload_get_snapshot(&snapshot);CHECK(snapshot.state==AUTOLOAD_ABORTED && !launches && joined==1);
CHECK(!pldmgr_autoload_start()); /* Explicit stop/start permits a fresh run. */
pldmgr_autoload_stop();CHECK(created==2 && joined==2);
printf("Autoload stop: %s cancels/joins within 1s and never launches a later entry\n",which);
}
static void snapshot_file(void) {
blocked=1;setup("local/ok\nlocal/next",0,0);CHECK(!pldmgr_autoload_start());wait_entered();
CHECK(!write_file_text(AUTOLOAD_CONFIG_PATH,"local/changed",13));
AutoloadSnapshot snapshot;pldmgr_autoload_get_snapshot(&snapshot);CHECK(snapshot.total==2 && !strcmp(snapshot.list,"local/ok,local/next"));
pldmgr_autoload_reset();pldmgr_autoload_get_snapshot(&snapshot);CHECK(snapshot.state==AUTOLOAD_EXECUTING && snapshot.total==2);
released=1;snapshot=wait_state(AUTOLOAD_DONE);CHECK(launches==2 && snapshot.done==2);pldmgr_autoload_stop();CHECK(joined==1);
puts("Autoload snapshot: execution/reset and sequence edits cannot replace a running snapshot");
}
static void invalid(void) {
const char *bad[]={"!", "!-1", "!1x", "!3600001", "!999999999999999999999999"};
for(size_t i=0;i<sizeof(bad)/sizeof(*bad);i++) { setup(bad[i],0,0);CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_FAILED);pldmgr_autoload_stop(); }
setup("local/ok",0,0);unlink(AUTOLOAD_CONFIG_PATH);CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_DISABLED);pldmgr_autoload_stop();
PldmgrConfig cfg={0};CHECK(!config_write(&cfg));CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_DISABLED);pldmgr_autoload_stop();
char large[4200];memset(large,'a',sizeof(large));CHECK(!write_file_text(AUTOLOAD_CONFIG_PATH,large,sizeof(large)));
cfg.autoload_enabled=1;CHECK(!config_write(&cfg));CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_FAILED);pldmgr_autoload_stop();
CHECK(!write_file_text(AUTOLOAD_CONFIG_PATH,"a\0b",3));CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_FAILED);pldmgr_autoload_stop();
CHECK(!launches && created==joined); puts("Autoload early exits: malformed delays, missing/disabled sequence all join without launches");
}
static _Atomic int polling;
static void *poll_worker(void *unused) {
(void)unused;
while(polling) {
AutoloadSnapshot snapshot;pldmgr_autoload_get_snapshot(&snapshot);
CHECK(snapshot.done<=snapshot.total && snapshot.done==snapshot.successful+snapshot.failed+snapshot.skipped);
CHECK(strlen(snapshot.outcomes)==(size_t)snapshot.total);
if(snapshot.state==AUTOLOAD_DONE)CHECK(snapshot.done==snapshot.total);
pldmgr_autoload_reset();pldmgr_autoload_frontend_ready();CHECK(!pldmgr_autoload_start());
} return NULL;
}
static void concurrency(void) {
blocked=1;setup("local/ok\nlocal/next",0,0);CHECK(!pldmgr_autoload_start());wait_entered();polling=1;
pthread_t readers[6];for(int i=0;i<6;i++)CHECK(!pthread_create(&readers[i],NULL,poll_worker,NULL));
usleep(50000);released=1;
/* Readers can clear terminal display, so completion is evidenced by launches
* and the owned worker being joined, rather than a transient terminal state. */
long long end=now_ms()+3000;while(launches<2 && now_ms()<end)usleep(1000);CHECK(launches==2);
polling=0;for(int i=0;i<6;i++)CHECK(!pthread_join(readers[i],NULL));pldmgr_autoload_stop();CHECK(created==1 && joined==1);
puts("Autoload concurrency: six simultaneous snapshot/reset/readiness/start clients retain coherent counters and one worker");
}
static void unicode_name(void) {
char key[256]="local/";
for(int i=0;i<40;i++)strcat(key,"😀");
blocked=1;setup(key,0,0);CHECK(!pldmgr_autoload_start());wait_entered();
AutoloadSnapshot snapshot;pldmgr_autoload_get_snapshot(&snapshot);
char escaped[1024];CHECK(!pldmgr_json_escape(snapshot.current,escaped,sizeof(escaped)));
CHECK(!strcmp(snapshot.list,key) && strlen(snapshot.current)<=127);
released=1;wait_state(AUTOLOAD_DONE);pldmgr_autoload_stop();
setup("a",0,0);CHECK(!pldmgr_autoload_start());snapshot=wait_state(AUTOLOAD_DONE);CHECK(snapshot.total==1 && snapshot.done==1);pldmgr_autoload_stop();
puts("Autoload bounds: long Unicode current names retain complete scalars, one-character final entries count");
}
static void allocation_failure(void) {
setup("local/ok",0,0);fail_alloc=1;CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_FAILED);pldmgr_autoload_stop();CHECK(!launches && created==1 && joined==1);
fail_alloc=0;CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_DONE);pldmgr_autoload_stop();CHECK(launches==1 && created==2 && joined==2);
puts("Autoload allocation failure: sequence is rejected and joined; a fresh lifecycle can retry");
}
static void frontend_timeout(void) {
setup("local/ok",1,0);long long begin=now_ms();CHECK(!pldmgr_autoload_start());
wait_state(AUTOLOAD_DONE);pldmgr_autoload_stop();CHECK(now_ms()-begin>=2000 && launches==1);
puts("Autoload frontend timeout: one sequence starts without a frontend after the bounded wait");
}
static void thread_failure(void) {
setup("local/ok",0,0);fail_thread=1;CHECK(pldmgr_autoload_start()==-1);wait_state(AUTOLOAD_FAILED);pldmgr_autoload_stop();
fail_thread=0;CHECK(!pldmgr_autoload_start());wait_state(AUTOLOAD_DONE);pldmgr_autoload_stop();CHECK(created==1 && joined==1 && launches==1);
puts("Autoload thread failure: retry succeeds with no leaked or falsely owned thread");
}
int main(int argc,char **argv) {
CHECK(argc==2);CHECK(!ensure_dir_recursive(BASE_DATA_DIR));
if(!strcmp(argv[1],"counters"))counters();else if(!strcmp(argv[1],"reset"))active_reset();
else if(!strcmp(argv[1],"wait")||!strcmp(argv[1],"delay")||!strcmp(argv[1],"send"))cancellable(argv[1]);
else if(!strcmp(argv[1],"snapshot"))snapshot_file();else if(!strcmp(argv[1],"invalid"))invalid();
else if(!strcmp(argv[1],"concurrency"))concurrency();else if(!strcmp(argv[1],"thread"))thread_failure();else if(!strcmp(argv[1],"unicode"))unicode_name();else if(!strcmp(argv[1],"allocation"))allocation_failure();else if(!strcmp(argv[1],"timeout"))frontend_timeout();else return 2;
return 0;
}
+8
View File
@@ -0,0 +1,8 @@
#include <pthread.h>
#include <stddef.h>
void *autoload_test_calloc(size_t, size_t);
#define calloc autoload_test_calloc
int autoload_test_pthread_create(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *);
int autoload_test_pthread_join(pthread_t, void **);
#define pthread_create autoload_test_pthread_create
#define pthread_join autoload_test_pthread_join
+11
View File
@@ -26,6 +26,7 @@ unsigned int test_config_saves, test_source_saves, test_imports;
size_t test_post_size; size_t test_post_size;
void pldmgr_log(const char *fmt, ...) { (void)fmt; } void pldmgr_log(const char *fmt, ...) { (void)fmt; }
void pldmgr_notify(const char *fmt, ...) { (void)fmt; }
void log_server_set_active(void) { test_active_calls++; } void log_server_set_active(void) { test_active_calls++; }
#ifndef PLDMGR_TEST_REAL_CONFIG #ifndef PLDMGR_TEST_REAL_CONFIG
void config_read(PldmgrConfig *cfg) { memset(cfg, 0, sizeof(*cfg)); cfg->autoload_delay = 5; } void config_read(PldmgrConfig *cfg) { memset(cfg, 0, sizeof(*cfg)); cfg->autoload_delay = 5; }
@@ -41,6 +42,13 @@ int config_get_favorites_csv(char *out, size_t size, int *exists) {
return test_json_failure ? -1 : 0; return test_json_failure ? -1 : 0;
} }
#endif #endif
AutoloadSnapshot test_autoload_snapshot={.state=AUTOLOAD_DISABLED,.remaining=-1,.remaining_ms=-1000,.delay=5};
#ifndef PLDMGR_TEST_REAL_AUTOLOAD
void pldmgr_autoload_frontend_ready(void) {}
void pldmgr_autoload_get_snapshot(AutoloadSnapshot *snapshot) {
*snapshot=test_autoload_snapshot;
}
const char *pldmgr_autoload_state_name(AutoloadState state) { (void)state; return "disabled"; }
void pldmgr_autoload_get_status(int *total, int *done, char *current) { void pldmgr_autoload_get_status(int *total, int *done, char *current) {
*total = 0; *done = 0; current[0] = '\0'; *total = 0; *done = 0; current[0] = '\0';
} }
@@ -48,6 +56,8 @@ int pldmgr_autoload_get_remaining_seconds(void) { return -1; }
long long pldmgr_autoload_get_remaining_ms(void) { return -1000; } long long pldmgr_autoload_get_remaining_ms(void) { return -1000; }
void pldmgr_autoload_reset(void) { test_mutation_calls++; } void pldmgr_autoload_reset(void) { test_mutation_calls++; }
void pldmgr_autoload_abort(void) { test_mutation_calls++; } void pldmgr_autoload_abort(void) { test_mutation_calls++; }
#endif
int pldmgr_server_is_active(void) { return test_active_calls != 0; }
int slot_resolve(const char *key, char *out, size_t size) { int slot_resolve(const char *key, char *out, size_t size) {
test_mutation_calls++; (void)key; (void)out; (void)size; return -1; test_mutation_calls++; (void)key; (void)out; (void)size; return -1;
} }
@@ -55,6 +65,7 @@ int slot_local_key(const char *name, char *out, size_t size) {
snprintf(out, size, "%s", name); return 0; snprintf(out, size, "%s", name); return 0;
} }
int ps5_launch_elf(const char *path) { (void)path; abort(); } int ps5_launch_elf(const char *path) { (void)path; abort(); }
int ps5_launch_elf_cancellable(const char *path, PayloadLaunchCancelled cancelled, void *context) { (void)path; (void)cancelled; (void)context; abort(); }
int payload_mgr_delete_payload(const char *key) { (void)key; test_mutation_calls++; return 0; } int payload_mgr_delete_payload(const char *key) { (void)key; test_mutation_calls++; return 0; }
int payload_mgr_import_to_storage_mode(const char *filename, const char *path, int payload_mgr_import_to_storage_mode(const char *filename, const char *path,
const char *mode, const char *origin, const char *detail, const char *mode, const char *origin, const char *detail,
+13 -3
View File
@@ -530,8 +530,11 @@ static void request_tests(void) {
assert(autoload); assert(autoload);
fputs("\r\nlocal/one\r\n!500\nlocal/two\n", autoload); fputs("\r\nlocal/one\r\n!500\nlocal/two\n", autoload);
fclose(autoload); fclose(autoload);
extern AutoloadSnapshot test_autoload_snapshot;
strcpy(test_autoload_snapshot.list,"local/running");
run_request((struct Request){"/autoload_status", "GET", 200, {{0}}}, 0, 0, 0); run_request((struct Request){"/autoload_status", "GET", 200, {{0}}}, 0, 0, 0);
expect(strstr(body, "local/one,local/two") != NULL, "status excludes delays and CRLF blank lines"); expect(strstr(body,"local/running") != NULL && !strstr(body,"local/one"),
"status returns the worker snapshot rather than a newly edited sequence");
run_request((struct Request){"/get_config", "GET", 200, {{0}}}, 0, 0, 0); run_request((struct Request){"/get_config", "GET", 200, {{0}}}, 0, 0, 0);
expect(strstr(body, "local/one,!500,local/two") != NULL, "config retains delay entries"); expect(strstr(body, "local/one,!500,local/two") != NULL, "config retains delay entries");
for (int size = 4095; size <= 4096; size++) { for (int size = 4095; size <= 4096; size++) {
@@ -541,7 +544,7 @@ static void request_tests(void) {
fclose(autoload); fclose(autoload);
for (int config = 0; config < 2; config++) { for (int config = 0; config < 2; config++) {
run_request((struct Request){config ? "/get_config" : "/autoload_status", "GET", run_request((struct Request){config ? "/get_config" : "/autoload_status", "GET",
size == 4095 ? 200 : 500, {{0}}}, 0, 0, 0); !config || size == 4095 ? 200 : 500, {{0}}}, 0, 0, 0);
} }
} }
autoload = fopen(AUTOLOAD_CONFIG_PATH, "w"); autoload = fopen(AUTOLOAD_CONFIG_PATH, "w");
@@ -550,7 +553,8 @@ static void request_tests(void) {
fputc('\0', autoload); fputc('\0', autoload);
fputs("local/two", autoload); fputs("local/two", autoload);
fclose(autoload); fclose(autoload);
run_request((struct Request){"/autoload_status", "GET", 500, {{0}}}, 0, 0, 0); run_request((struct Request){"/get_config", "GET", 500, {{0}}}, 0, 0, 0);
memset(test_autoload_snapshot.list,0,sizeof(test_autoload_snapshot.list));
remove(AUTOLOAD_CONFIG_PATH); remove(AUTOLOAD_CONFIG_PATH);
printf("HTTP request lifecycle: %d additional checks, %d total failures\n", checks - initial_checks, failures); printf("HTTP request lifecycle: %d additional checks, %d total failures\n", checks - initial_checks, failures);
} }
@@ -595,9 +599,15 @@ static int serve(void) {
MHD_OPTION_SOCK_ADDR, &address, MHD_OPTION_END); MHD_OPTION_SOCK_ADDR, &address, MHD_OPTION_END);
if (!daemon) return 1; if (!daemon) return 1;
const union MHD_DaemonInfo *info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_BIND_PORT); const union MHD_DaemonInfo *info = MHD_get_daemon_info(daemon, MHD_DAEMON_INFO_BIND_PORT);
#ifdef PLDMGR_TEST_REAL_AUTOLOAD
if (pldmgr_autoload_start()) { MHD_stop_daemon(daemon); return 1; }
#endif
printf("PORT %u\n", info->port); printf("PORT %u\n", info->port);
fflush(stdout); fflush(stdout);
(void)getchar(); (void)getchar();
#ifdef PLDMGR_TEST_REAL_AUTOLOAD
pldmgr_autoload_stop();
#endif
MHD_stop_daemon(daemon); MHD_stop_daemon(daemon);
printf("Application response references: created=%u released=%u\n", created, destroyed); printf("Application response references: created=%u released=%u\n", created, destroyed);
return created == destroyed ? 0 : 1; return created == destroyed ? 0 : 1;
+84
View File
@@ -0,0 +1,84 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <time.h>
#include <poll.h>
#include <sys/socket.h>
#include "ps5_launcher.h"
#define CHECK(x) do { if(!(x)){fprintf(stderr,"FAIL %d: %s (%s)\n",__LINE__,#x,mode);exit(1);} } while(0)
static const char *mode,*fixture;
static int peer=-1, sock=-1, file=-1, sockets, opened, history, calls;
static size_t sent_bytes, received_bytes, expected_size;
static unsigned char *received;
static pthread_t receiver;
static int has_receiver;
static long long started;
static long long now_ms(void) { struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (long long)t.tv_sec*1000+t.tv_nsec/1000000; }
void pldmgr_log(const char *fmt,...) { (void)fmt; }
void history_mgr_add(const char *path) { CHECK(!strcmp(path,fixture));history++; }
int slot_open_payload(const char *path,char *canonical,size_t size) { CHECK(!strcmp(path,fixture));opened++;snprintf(canonical,size,"%s",path);file=open(path,O_RDONLY);return file; }
static void *receive_bytes(void *unused) {
(void)unused;
for(;;) { ssize_t n=recv(peer,received+received_bytes,expected_size-received_bytes,0); if(n==0)break;CHECK(n>0);received_bytes+=(size_t)n;if(received_bytes==expected_size)break; }
close(peer);return NULL;
}
int launcher_test_socket(int domain,int type,int protocol) {
(void)domain;(void)type;(void)protocol;sockets++;
if(!strcmp(mode,"socket")){errno=EMFILE;return -1;}
int pair[2];CHECK(!socketpair(AF_UNIX,SOCK_STREAM,0,pair));sock=pair[0];peer=pair[1];
int size=4096;CHECK(!setsockopt(sock,SOL_SOCKET,SO_SNDBUF,&size,sizeof(size)));
if(!strcmp(mode,"ok") || !strcmp(mode,"partial") || !strcmp(mode,"interrupt")) { has_receiver=1;CHECK(!pthread_create(&receiver,NULL,receive_bytes,NULL)); }
return sock;
}
int launcher_test_fcntl(int fd,int command,...) { va_list args;va_start(args,command);int arg=va_arg(args,int);va_end(args);if(!strcmp(mode,"fcntl")){errno=EIO;return -1;}return fcntl(fd,command,arg); }
int launcher_test_connect(int fd,const struct sockaddr *address,socklen_t size) {
(void)fd;(void)address;(void)size;
if(!strcmp(mode,"connect")){errno=ECONNREFUSED;return -1;}
if(!strcmp(mode,"connect-stall") || !strcmp(mode,"socket-error")){errno=EINPROGRESS;return -1;}
return 0;
}
int launcher_test_getsockopt(int fd,int level,int name,void *out,socklen_t *size) { if(!strcmp(mode,"socket-error")){errno=EIO;return -1;}return getsockopt(fd,level,name,out,size); }
int launcher_test_poll(struct pollfd *fds,nfds_t count,int timeout) {
if(!strcmp(mode,"connect-stall")){usleep((useconds_t)timeout*1000);return 0;}
return poll(fds,count,timeout);
}
ssize_t launcher_test_send(int fd,const void *buffer,size_t size,int flags) {
calls++;
if(!strcmp(mode,"zero"))return 0;
if(!strcmp(mode,"interrupt") && calls==1){errno=EINTR;return -1;}
if(!strcmp(mode,"interrupt") && calls==2){errno=EAGAIN;return -1;}
if((!strcmp(mode,"partial") || !strcmp(mode,"cancel-send")) && size>17)size=17;
ssize_t n=send(fd,buffer,size,flags);if(n>0)sent_bytes+=(size_t)n;return n;
}
ssize_t launcher_test_read(int fd,void *buffer,size_t size) { if(!strcmp(mode,"read")){errno=EIO;return -1;}return read(fd,buffer,size); }
static int cancelled(void *context) { (void)context;return !strcmp(mode,"cancel-before") || (!strcmp(mode,"cancel-send") && sent_bytes>=68) || (!strcmp(mode,"cancel-wait") && now_ms()-started>=100); }
int main(int argc,char **argv) {
CHECK(argc==3);mode=argv[1];fixture=argv[2];signal(SIGPIPE,SIG_IGN);
FILE *f=fopen(fixture,"rb");CHECK(f);CHECK(!fseek(f,0,SEEK_END));long length=ftell(f);CHECK(length>0);expected_size=(size_t)length;CHECK(!fclose(f));
received=malloc(expected_size);CHECK(received);started=now_ms();
int rc=ps5_launch_elf_cancellable(fixture,cancelled,NULL);
long long elapsed=now_ms()-started;
if(has_receiver)CHECK(!pthread_join(receiver,NULL));else if(peer>=0)close(peer);
int success=!strcmp(mode,"ok") || !strcmp(mode,"partial") || !strcmp(mode,"interrupt");
CHECK(success ? rc==0 && history==1 : rc==-1 && history==0);
if(success) {
CHECK(received_bytes==expected_size && sent_bytes==expected_size);
FILE *original=fopen(fixture,"rb");CHECK(original);unsigned char *expected=malloc(expected_size);CHECK(expected);CHECK(fread(expected,1,expected_size,original)==expected_size);CHECK(!fclose(original));CHECK(!memcmp(expected,received,expected_size));free(expected);
}
if(!strcmp(mode,"cancel-before"))CHECK(!opened && !sockets);
if(!strcmp(mode,"cancel-send"))CHECK(sent_bytes==68);
if(!strcmp(mode,"cancel-wait"))CHECK(elapsed>=100 && elapsed<1000);
if(!strcmp(mode,"connect-stall"))CHECK(elapsed>=200 && elapsed<1500);
if(!strcmp(mode,"send-stall"))CHECK(elapsed>=1000 && elapsed<3000 && sent_bytes<expected_size);
if(file>=0)CHECK(fcntl(file,F_GETFD)==-1 && errno==EBADF);
if(sock>=0)CHECK(fcntl(sock,F_GETFD)==-1 && errno==EBADF);
free(received);
printf("Loader: %s passed (sent=%zu, history=%d, elapsed=%lldms)\n",mode,sent_bytes,history,elapsed);return 0;
}
+17
View File
@@ -0,0 +1,17 @@
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
int launcher_test_socket(int,int,int);
int launcher_test_connect(int,const struct sockaddr *,socklen_t);
ssize_t launcher_test_send(int,const void *,size_t,int);
ssize_t launcher_test_read(int,void *,size_t);
int launcher_test_poll(struct pollfd *,nfds_t,int);
int launcher_test_getsockopt(int,int,int,void *,socklen_t *);
int launcher_test_fcntl(int,int,...);
#define socket launcher_test_socket
#define connect launcher_test_connect
#define send launcher_test_send
#define read launcher_test_read
#define poll launcher_test_poll
#define getsockopt launcher_test_getsockopt
#define fcntl launcher_test_fcntl
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Actual joinable worker, configuration files and concurrent API readers; fake loader only."""
import argparse
import os
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
ROOT=Path(__file__).resolve().parents[1]
parser=argparse.ArgumentParser()
parser.add_argument('--thread-sanitize',action='store_true')
parser.add_argument('--legacy',action='store_true')
args=parser.parse_args()
os.environ.setdefault('ASAN_OPTIONS','detect_leaks=0:halt_on_error=1' if sys.platform=='darwin' else 'detect_leaks=1:halt_on_error=1')
os.environ.setdefault('UBSAN_OPTIONS','halt_on_error=1:print_stacktrace=1')
os.environ.setdefault('TSAN_OPTIONS','halt_on_error=1')
with tempfile.TemporaryDirectory(prefix='pldmgr-autoload-') as temp:
build=Path(temp);storage=build/'data';storage.mkdir()
(build/'pldmgr.h').write_text((ROOT/'include/pldmgr.h').read_text().replace('/data/pldmgr',str(storage)))
flags=shlex.split(os.environ.get('HOST_CC','clang'))+['-std=c11','-D_DEFAULT_SOURCE','-g','-O1','-Wall','-Wextra','-pthread','-ffunction-sections','-fdata-sections','-I'+str(build),'-Iinclude', '-DPLDMGR_FRONTEND_WAIT_MS=2000', '-fno-omit-frame-pointer', '-fsanitize=thread' if args.thread_sanitize else '-fsanitize=address,undefined']
objects=[]
for name in ['autoload','config','json_helpers','utils']:
source=f'src/{name}.c';extra=['-include','tests/autoload_hooks.h'] if name=='autoload' else []
if args.legacy and name=='autoload':
snapshot=build/'autoload_legacy.c'
old=subprocess.check_output(['git','show','356a18a:src/autoload.c'],cwd=ROOT,text=True)
old+='''\nvoid pldmgr_autoload_stop(void) {}
void pldmgr_autoload_frontend_ready(void) { int total,done; pldmgr_autoload_get_status(&total,&done,0); }
void pldmgr_autoload_get_snapshot(AutoloadSnapshot *out) {
memset(out,0,sizeof(*out));
pldmgr_autoload_get_status(&out->total,&out->done,out->current);
out->remaining=pldmgr_autoload_get_remaining_seconds();
out->state=!strcmp(out->current,"DONE") ? AUTOLOAD_DONE : AUTOLOAD_EXECUTING;
}
'''
snapshot.write_text(old);source=str(snapshot)
obj=build/(name+'.o');subprocess.run(flags+extra+['-c',source,'-o',str(obj)],cwd=ROOT,check=True);objects.append(str(obj))
binary=build/'autoload_tests';subprocess.run(flags+objects+['tests/autoload_driver.c','-Wl,-dead_strip' if sys.platform=='darwin' else '-Wl,--gc-sections','-o',str(binary)],cwd=ROOT,check=True)
cases=['counters'] if args.legacy else ['counters','reset','wait','delay','send','snapshot','invalid','concurrency','thread','unicode','allocation','timeout']
for case in cases:
result=subprocess.run([str(binary),case],cwd=ROOT,text=True,capture_output=True,timeout=15)
if args.legacy:
assert result.returncode and ('snapshot.total==4' in result.stderr or (args.thread_sanitize and 'ThreadSanitizer: data race' in result.stderr)),result.stdout+result.stderr
print('Previous autoload reproduces '+('a ThreadSanitizer data race' if 'ThreadSanitizer: data race' in result.stderr else 'inconsistent payload/delay/blank counters'))
else:
assert result.returncode==0,result.stdout+result.stderr
print(result.stdout.strip())
+59 -5
View File
@@ -131,7 +131,7 @@ def lifecycle_tests(port, storage, server):
# Newlines are list separators; other controls remain string content. # Newlines are list separators; other controls remain string content.
sample = 'Zażółć Україна 日本語 😀 "quoted" \\path\t\b\f\x01' sample = 'Zażółć Україна 日本語 😀 "quoted" \\path\t\b\f\x01'
autoload.write_text(sample, encoding="utf-8") autoload.write_text(sample, encoding="utf-8")
for route, key in [("/get_config", "AUTOLOAD_LIST"), ("/autoload_status", "list")]: for route, key in [("/get_config", "AUTOLOAD_LIST")]:
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10) connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try: try:
connection.request("GET", route) connection.request("GET", route)
@@ -142,7 +142,7 @@ def lifecycle_tests(port, storage, server):
finally: finally:
connection.close() connection.close()
autoload.write_bytes(b"\x01" * 4095) autoload.write_bytes(b"\x01" * 4095)
for route, key in [("/get_config", "AUTOLOAD_LIST"), ("/autoload_status", "list")]: for route, key in [("/get_config", "AUTOLOAD_LIST")]:
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10) connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try: try:
connection.request("GET", route) connection.request("GET", route)
@@ -153,7 +153,7 @@ def lifecycle_tests(port, storage, server):
connection.close() connection.close()
for size in [4095, 4096, 20000]: for size in [4095, 4096, 20000]:
autoload.write_text("a" * size) autoload.write_text("a" * size)
for route in ["/get_config", "/autoload_status"]: for route in ["/get_config"]:
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10) connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try: try:
connection.request("GET", route) connection.request("GET", route)
@@ -396,6 +396,51 @@ def config_network_tests(binary, storage):
if server.poll() is None: server.kill(); server.communicate() if server.poll() is None: server.kill(); server.communicate()
def autoload_network_tests(binary, storage):
server = subprocess.Popen([str(binary), "--serve"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
try:
line = server.stdout.readline().strip()
assert line.startswith("PORT "), line
port = int(line.split()[1])
def request(method="GET", route="/autoload_status", headers=None):
connection = http.client.HTTPConnection("127.0.0.1",port,timeout=10)
try:
connection.request(method,route,headers=headers or {})
response = connection.getresponse()
body = response.read()
assert response.status == 200,(response.status,body)
return json.loads(body) if route == "/autoload_status" else body
finally: connection.close()
def state_is(wanted, headers=None):
snapshot = request(headers=headers)
assert snapshot["done"] == snapshot["successful"] + snapshot["failed"] + snapshot["skipped"]
return snapshot["state"] == wanted
eventually(lambda: state_is("waiting"), "worker did not enter frontend wait")
time.sleep(0.1)
assert state_is("waiting", {"Origin":"http://evil.invalid"})
assert state_is("waiting"), "anonymous or foreign status read triggered the worker"
marked = {"X-PLDMGR-Request":"1"}
eventually(lambda: state_is("countdown",marked), "authorized frontend read did not start countdown")
(storage / "autoload.txt").write_text("local/changed\n")
snapshot = request()
assert snapshot["list"] == "local/running" and snapshot["total"] == 1
request("POST","/autoload_clear",marked)
assert state_is("countdown"), "reset erased active countdown"
request("POST","/abort",marked)
eventually(lambda: state_is("aborted"), "abort did not stop countdown")
assert request()["remaining"] == -1
request("POST","/autoload_clear",marked)
snapshot = request()
assert snapshot["state"] == "disabled" and snapshot["total"] == 0
server.stdin.write("\n");server.stdin.flush()
output,_=server.communicate(timeout=10)
assert server.returncode == 0,output
print(output.strip())
print("HTTP actual autoload: foreign/anonymous reads stay passive; authorized readiness, coherent sequence, active reset, abort and terminal clear passed")
finally:
if server.poll() is None: server.kill();server.communicate()
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--ownership-only", action="store_true", parser.add_argument("--ownership-only", action="store_true",
@@ -408,6 +453,7 @@ def main():
parser.add_argument("--legacy-security", action="store_true") parser.add_argument("--legacy-security", action="store_true")
parser.add_argument("--serve-only", action="store_true") parser.add_argument("--serve-only", action="store_true")
parser.add_argument("--config-only", action="store_true") parser.add_argument("--config-only", action="store_true")
parser.add_argument("--autoload-only", action="store_true")
parser.add_argument("--source", default="src/http_server.c", parser.add_argument("--source", default="src/http_server.c",
help="Alternate HTTP source snapshot for regression reproduction") help="Alternate HTTP source snapshot for regression reproduction")
args = parser.parse_args() args = parser.parse_args()
@@ -435,8 +481,10 @@ def main():
f"static const unsigned int assets_{name}_len = sizeof(assets_{name});\n") f"static const unsigned int assets_{name}_len = sizeof(assets_{name});\n")
common = compiler + ["-std=c11", "-D_DEFAULT_SOURCE", "-g", "-O1", "-Wall", "-Wextra", common = compiler + ["-std=c11", "-D_DEFAULT_SOURCE", "-g", "-O1", "-Wall", "-Wextra",
"-I" + str(build), "-Iinclude", "-pthread"] "-I" + str(build), "-Iinclude", "-pthread"]
if args.config_only: if args.config_only or args.autoload_only:
common += ["-DPLDMGR_TEST_REAL_CONFIG"] common += ["-DPLDMGR_TEST_REAL_CONFIG"]
if args.autoload_only:
common += ["-DPLDMGR_TEST_REAL_AUTOLOAD"]
if not args.no_sanitize: if not args.no_sanitize:
common += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] common += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
cflags = [flag for flag in flags if flag.startswith("-I") or flag.startswith("-D")] cflags = [flag for flag in flags if flag.startswith("-I") or flag.startswith("-D")]
@@ -444,10 +492,16 @@ def main():
run(common + cflags + ["-include", "tests/http_test_hooks.h", "-c", run(common + cflags + ["-include", "tests/http_test_hooks.h", "-c",
args.source, "-o", str(object_file)]) args.source, "-o", str(object_file)])
binary = build / "http_tests" binary = build / "http_tests"
extra_sources = ["src/config.c"] if args.config_only else [] extra_sources = ["src/config.c"] if args.config_only or args.autoload_only else []
if args.autoload_only:
extra_sources += ["src/autoload.c"]
(storage / "pldmgr_config.txt").write_text("AUTOLOAD_ENABLED=1\nAUTO_BROWSER_OPEN=1\nAUTOLOAD_DELAY=5\n")
(storage / "autoload.txt").write_text("local/running\n")
run(common + cflags + extra_sources + [str(object_file), "src/utils.c", "src/json_helpers.c", "tests/http_backend_stubs.c", run(common + cflags + extra_sources + [str(object_file), "src/utils.c", "src/json_helpers.c", "tests/http_backend_stubs.c",
"tests/http_responses_test.c", "-o", str(binary)] + flags) "tests/http_responses_test.c", "-o", str(binary)] + flags)
selected = ["--ownership-only"] if args.ownership_only else (["--oom-only"] if args.oom_only else []) selected = ["--ownership-only"] if args.ownership_only else (["--oom-only"] if args.oom_only else [])
if args.autoload_only:
autoload_network_tests(binary, storage); return
if args.config_only: if args.config_only:
config_network_tests(binary, storage); return config_network_tests(binary, storage); return
if args.serve_only: if args.serve_only:
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Actual nonblocking launcher with a socketpair byte recorder; no real ELF loader."""
import os
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
ROOT=Path(__file__).resolve().parents[1]
os.environ.setdefault('ASAN_OPTIONS','detect_leaks=0:halt_on_error=1' if sys.platform=='darwin' else 'detect_leaks=1:halt_on_error=1')
os.environ.setdefault('UBSAN_OPTIONS','halt_on_error=1:print_stacktrace=1')
with tempfile.TemporaryDirectory(prefix='pldmgr-loader-') as temp:
build=Path(temp)
flags=shlex.split(os.environ.get('HOST_CC','clang'))+['-std=c11','-D_DEFAULT_SOURCE','-g','-O1','-Wall','-Wextra','-pthread','-ffunction-sections','-fdata-sections','-Iinclude','-DPLDMGR_LOADER_CONNECT_MS=200','-DPLDMGR_LOADER_TRANSFER_MS=1000','-fsanitize=address,undefined','-fno-omit-frame-pointer']
obj=build/'launcher.o';binary=build/'launcher_tests'
subprocess.run(flags+['-include','tests/launcher_hooks.h','-c','src/ps5_launcher.c','-o',str(obj)],cwd=ROOT,check=True)
subprocess.run(flags+[str(obj),'tests/launcher_driver.c','-Wl,-dead_strip' if sys.platform=='darwin' else '-Wl,--gc-sections','-o',str(binary)],cwd=ROOT,check=True)
small=build/'small.elf';small.write_bytes(bytes(range(256))*80)
big=build/'big.elf';big.write_bytes(bytes(range(256))*4096)
for case in ['ok','partial','interrupt','socket','fcntl','connect','socket-error','read','zero','connect-stall','send-stall','cancel-before','cancel-send','cancel-wait']:
fixture=big if case in ['send-stall','cancel-wait'] else small
result=subprocess.run([str(binary),case,str(fixture)],text=True,capture_output=True,timeout=10)
assert result.returncode==0,result.stdout+result.stderr
print(result.stdout.strip())