mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
fix: synchronize autoload lifecycle and bound loader transfers
This commit is contained in:
1 parent
ab82dbf05e
commit
c47b725344
22 files changed
+925
-347
No files matched your search
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in new issue
Block a user