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
steps:
- uses: actions/checkout@v4
- name: Check concurrent config, sources and history with ThreadSanitizer
run: python3 tests/run_config_tests.py --thread-sanitize
- name: Check concurrent config, sources, history and autoload with ThreadSanitizer
run: |
python3 tests/run_config_tests.py --thread-sanitize
python3 tests/run_autoload_tests.py --thread-sanitize
frontend:
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
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
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;
@@ -182,7 +198,7 @@ build a candidate under `scratch/` rather than run `make clean`:
```bash
mkdir -p scratch
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
+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_integrity_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_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
@@ -3,12 +3,14 @@ import { CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '../../utils/helpers'
import PayloadName from '../ui/PayloadName'
import { autoloadPhase, autoloadItemProgress } from '../../utils/autoloadProgress'
const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
const { t } = useTranslation()
const isCountdown = status.remaining > 0 || (status.remaining === 0 && !status.current);
const isExecuting = status.remaining === 0 && !!status.current && status.current !== 'DONE';
const isDone = status.current === 'DONE';
const phase = autoloadPhase(status);
const isCountdown = phase === 'waiting' || phase === 'countdown';
const isExecuting = phase === 'executing';
const isDone = phase === 'done';
const payloadList = (typeof status.list === 'string') ? status.list.split(',').filter(p => p.trim() !== '') : [];
const listRef = useRef(null);
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")}
</button>
) : isCountdown ? (
) : (isCountdown || isExecuting) ? (
<button
onClick={onCancel}
autoFocus
@@ -178,8 +180,7 @@ const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
<div className="space-y-3">
{payloadList.map((name, i) => {
const active = !isDone && isExecuting && i === status.done;
const done = isDone || i < status.done;
const { active, processed: done, failed, skipped } = autoloadItemProgress(status, i);
return (
<div
key={i}
@@ -187,10 +188,13 @@ const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
className={cn(
"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' :
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">
{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" /> :
<div className="w-6 h-6 rounded-full border-2 border-white/10" />}
<PayloadName
@@ -200,8 +204,8 @@ const AutoloadOverlay = ({ status, onCancel, onFinish, isPS5 }) => {
/>
</div>
{done && (
<span className="text-emerald-500 text-[10px] font-black uppercase tracking-widest italic">
{t("autoload_overlay.success", "Success")}
<span className={cn("text-[10px] font-black uppercase tracking-widest italic", failed ? "text-red-400" : skipped ? "text-zinc-400" : "text-emerald-500")}>
{failed ? t("autoload_overlay.failed", "Failed") : skipped ? t("autoload_overlay.skipped", "Skipped") : t("autoload_overlay.success", "Success")}
</span>
)}
</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
#include <stddef.h>
/*
* Start the autoload sequence in a background thread.
* Returns 0 on success, -1 on failure.
*/
int pldmgr_autoload_start();
void pldmgr_autoload_abort();
void pldmgr_autoload_reset();
int pldmgr_autoload_get_remaining_seconds();
long long pldmgr_autoload_get_remaining_ms();
typedef enum {
AUTOLOAD_DISABLED, AUTOLOAD_WAITING, AUTOLOAD_COUNTDOWN, AUTOLOAD_EXECUTING,
AUTOLOAD_DONE, AUTOLOAD_ABORTED, AUTOLOAD_FAILED
} AutoloadState;
typedef struct {
AutoloadState state;
int remaining, total, done, successful, failed, skipped;
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);
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);
/* 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.
* @param uri The URL to open.
+296 -230
View File
@@ -4,256 +4,322 @@
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
static volatile long long countdown_end_time = 0;
long long get_current_time_ms() {
struct timeval tv;
gettimeofday(&tv, NULL);
return (long long)(tv.tv_sec) * 1000 + (tv.tv_usec) / 1000;
}
#include <time.h>
#include <errno.h>
#include <sys/stat.h>
#include "pldmgr.h"
#include "autoload.h"
#include "config.h"
#include "payload_mgr.h"
#include "json_helpers.h"
#include "slot.h"
#include "ps5_launcher.h"
static volatile int abort_flag = 0;
static volatile int remaining_seconds = -1;
static volatile int is_executing = 0;
static pthread_t autoload_thread;
#ifndef PLDMGR_FRONTEND_WAIT_MS
#define PLDMGR_FRONTEND_WAIT_MS 5000
#endif
#define MAX_SEQUENCE_TEXT 4095
#define MAX_ENTRY_TEXT 511
#define MAX_ENTRY_DELAY_MS 3600000
static char autoload_current_name[128] = "";
static int autoload_total_count = 0;
static int autoload_done_count = 0;
static int autoload_triggered = 0; // Starts at 0, becomes 1 when frontend connects
/* Ownership lock serializes start/stop/join; worker uses only state_mutex.
* Never hold state_mutex during join, filesystem access, notifications or launch. */
static pthread_mutex_t lifecycle_mutex = PTHREAD_MUTEX_INITIALIZER;
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() {
return remaining_seconds;
typedef struct { char key[MAX_ENTRY_TEXT + 1]; int delay_ms; } SequenceEntry;
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;
}
long long pldmgr_autoload_get_remaining_ms() {
if (countdown_end_time == 0) return (long long)remaining_seconds * 1000;
long long now = get_current_time_ms();
if (now >= countdown_end_time) return 0;
return countdown_end_time - now;
const char *pldmgr_autoload_state_name(AutoloadState state) {
const char *names[] = {"disabled", "waiting", "countdown", "executing", "done", "aborted", "failed"};
return state >= AUTOLOAD_DISABLED && state <= AUTOLOAD_FAILED ? names[state] : "failed";
}
void pldmgr_autoload_get_snapshot(AutoloadSnapshot *out) {
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) {
autoload_triggered = 1; // Signal that frontend is ready/active
*total = autoload_total_count;
*done = autoload_done_count;
if (current) strcpy(current, autoload_current_name);
AutoloadSnapshot snapshot; pldmgr_autoload_get_snapshot(&snapshot);
if (total) *total=snapshot.total;
if (done) *done=snapshot.done;
if (current) memcpy(current, snapshot.current, sizeof(snapshot.current));
}
void* pldmgr_autoload_worker(void* arg) {
struct stat st;
int has_config = (stat(AUTOLOAD_CONFIG_PATH, &st) == 0);
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;
void pldmgr_autoload_frontend_ready(void) {
pthread_mutex_lock(&state_mutex);
frontend_ready=1;
pthread_cond_broadcast(&state_cond);
pthread_mutex_unlock(&state_mutex);
}
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() {
abort_flag = 0;
is_executing = 0;
if (pthread_create(&autoload_thread, NULL, pldmgr_autoload_worker, NULL) != 0) {
pldmgr_log("[Autoload] !!! Failed to create background thread\n");
return -1;
/* Preserve the existing PS-button mode, prompt/drain ordering and per-read
* matching. The user explicitly withdrew the separate detection changes. */
int klog_fd=-1;
if (cfg.autoload_delay>0 && !cfg.auto_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",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);
return 0;
free(entries); finish(AUTOLOAD_DONE);
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_abort() {
if (!is_executing) {
abort_flag = 1;
void pldmgr_autoload_stop(void) {
pthread_mutex_lock(&lifecycle_mutex);
if (owned) {
pldmgr_autoload_abort();
pthread_join(worker_thread,NULL);
owned=0;
}
pthread_mutex_unlock(&lifecycle_mutex);
}
void pldmgr_autoload_reset() {
/* If already counting down, don't reset to avoid jumps.
* The browser launch often triggers a system 'resume' which would snap time back.
* We only reset if the countdown has already finished or hasn't started. */
if (countdown_end_time > 0 && remaining_seconds > 0) {
pldmgr_log("[PLDMGR] Autoload reset ignored (timer active)\n");
return;
void pldmgr_autoload_reset(void) {
pthread_mutex_lock(&state_mutex);
if (!running) {
status=(AutoloadSnapshot){.state=AUTOLOAD_DISABLED,.remaining=-1,.remaining_ms=-1000};
countdown_end=0;
}
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, "");
pthread_mutex_unlock(&state_mutex);
}
int pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename) {
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_filename ? old_filename : "");
int pldmgr_autoload_update_config_entry(const char *old_key,const char *new_key) {
int result=config_update_list_entry(AUTOLOAD_CONFIG_PATH,old_key,new_key);
if (result) pldmgr_log("[Autoload] Failed to update entry: %s\n",old_key ? old_key : "");
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);
add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, ROUTE_AUTOLOAD_STATUS) == 0) {
int total, done;
char current[128];
pldmgr_autoload_get_status(&total, &done, current);
int remaining = pldmgr_autoload_get_remaining_seconds();
long long remaining_ms = pldmgr_autoload_get_remaining_ms();
char list_buf[4096];
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));
if (mutation_authorized(conn)) pldmgr_autoload_frontend_ready();
AutoloadSnapshot snapshot;
pldmgr_autoload_get_snapshot(&snapshot);
char current_escaped[6 * 128], list_escaped[6 * 4096];
if (pldmgr_json_escape(snapshot.current,current_escaped,sizeof(current_escaped)) ||
pldmgr_json_escape(snapshot.list,list_escaped,sizeof(list_escaped)))
return send_request_error(conn,MHD_HTTP_INTERNAL_SERVER_ERROR,"Invalid autoload snapshot");
char *resp_buf;
if (!alloc_response_buffer_small(&resp_buf))
return send_out_of_memory(conn);
json_format(resp_buf, SMALL_RESPONSE_BUFFER_SIZE,
"{\"remaining\":%d,\"remaining_ms\":%lld,\"total\":%d,\"done\":%d,\"current\":\"%s\","
"\"list\":\"%s\",\"delay\":%d,\"SCAN_USB_PAYLOADS\":%s}",
remaining, remaining_ms, total, done, current_escaped, list_escaped, cfg.autoload_delay,
cfg.scan_usb_payloads ? "true" : "false");
"\"list\":\"%s\",\"delay\":%d,\"SCAN_USB_PAYLOADS\":%s,"
"\"state\":\"%s\",\"successful\":%d,\"failed\":%d,\"skipped\":%d,\"outcomes\":\"%s\"}",
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);
resp = create_buffer_response(strlen(resp_buf),
(void *)resp_buf,
+2 -1
View File
@@ -5,6 +5,7 @@
#include <pthread.h>
#include <errno.h>
#include <time.h>
#include <stdatomic.h>
#include "log_server.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 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; }
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_autoload_stop();
if (daemon)
MHD_stop_daemon(daemon);
+85 -65
View File
@@ -14,73 +14,93 @@
#include <unistd.h>
#include <stdint.h>
#include <errno.h>
#include <time.h>
#include <poll.h>
int ps5_launch_elf(const char *path) {
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;
}
pldmgr_log("[PLDMGR] Sending ELF to local loader: %s\n", canonical);
size_t total_sent = 0;
/* 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;
#ifndef PLDMGR_LOADER_CONNECT_MS
#define PLDMGR_LOADER_CONNECT_MS 5000
#endif
#ifndef PLDMGR_LOADER_TRANSFER_MS
#define PLDMGR_LOADER_TRANSFER_MS 30000
#endif
static long long launch_now_ms(void) {
struct timespec time; clock_gettime(CLOCK_MONOTONIC, &time);
return (long long)time.tv_sec*1000+time.tv_nsec/1000000;
}
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);
+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;
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++; }
#ifndef PLDMGR_TEST_REAL_CONFIG
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;
}
#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) {
*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; }
void pldmgr_autoload_reset(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) {
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;
}
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_import_to_storage_mode(const char *filename, const char *path,
const char *mode, const char *origin, const char *detail,
+13 -3
View File
@@ -530,8 +530,11 @@ static void request_tests(void) {
assert(autoload);
fputs("\r\nlocal/one\r\n!500\nlocal/two\n", 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);
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);
expect(strstr(body, "local/one,!500,local/two") != NULL, "config retains delay entries");
for (int size = 4095; size <= 4096; size++) {
@@ -541,7 +544,7 @@ static void request_tests(void) {
fclose(autoload);
for (int config = 0; config < 2; config++) {
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");
@@ -550,7 +553,8 @@ static void request_tests(void) {
fputc('\0', autoload);
fputs("local/two", 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);
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);
if (!daemon) return 1;
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);
fflush(stdout);
(void)getchar();
#ifdef PLDMGR_TEST_REAL_AUTOLOAD
pldmgr_autoload_stop();
#endif
MHD_stop_daemon(daemon);
printf("Application response references: created=%u released=%u\n", created, destroyed);
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.
sample = 'Zażółć Україна 日本語 😀 "quoted" \\path\t\b\f\x01'
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)
try:
connection.request("GET", route)
@@ -142,7 +142,7 @@ def lifecycle_tests(port, storage, server):
finally:
connection.close()
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)
try:
connection.request("GET", route)
@@ -153,7 +153,7 @@ def lifecycle_tests(port, storage, server):
connection.close()
for size in [4095, 4096, 20000]:
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)
try:
connection.request("GET", route)
@@ -396,6 +396,51 @@ def config_network_tests(binary, storage):
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():
parser = argparse.ArgumentParser()
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("--serve-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",
help="Alternate HTTP source snapshot for regression reproduction")
args = parser.parse_args()
@@ -435,8 +481,10 @@ def main():
f"static const unsigned int assets_{name}_len = sizeof(assets_{name});\n")
common = compiler + ["-std=c11", "-D_DEFAULT_SOURCE", "-g", "-O1", "-Wall", "-Wextra",
"-I" + str(build), "-Iinclude", "-pthread"]
if args.config_only:
if args.config_only or args.autoload_only:
common += ["-DPLDMGR_TEST_REAL_CONFIG"]
if args.autoload_only:
common += ["-DPLDMGR_TEST_REAL_AUTOLOAD"]
if not args.no_sanitize:
common += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
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",
args.source, "-o", str(object_file)])
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",
"tests/http_responses_test.c", "-o", str(binary)] + flags)
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:
config_network_tests(binary, storage); return
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())