Fix launcher install and progress reporting

This commit is contained in:
ItsBlurf committed 2026-05-29 23:50:21 +01:00
1 parent 7186e2d66a
commit 30d89ecafa
5 files changed
+86 -27

No files matched your search

+10 -3
View File
@@ -28,6 +28,10 @@ When the launcher is newly installed or updated, the payload also asks the PS5
to open that local browser URL once. Later loads keep the tile available without
forcing the browser open again.
Launcher state is stored in `/data/BS5fm`. The PS5 app tile itself still lives
under `/user/app/BS5F00001`, which is the console app location used by the app
install API.
The payload also shows a PS5 notification with the external browser URL when
the web server is ready. Port `5905` is fixed so this payload can run next to
older builds that may still be listening on another port.
@@ -59,9 +63,10 @@ At a high level the payload has five parts:
notifications, installs the launcher tile if needed, and starts the HTTP
server on port `5905`.
- `src/app_installer.c` writes the embedded launcher metadata/icon under
`/user/app/BS5F00001` and registers the PS5 home-screen tile. The installer
resolves the title-directory install function from `libSceAppInstUtil.sprx`
at runtime and falls back to the broader app install call when needed.
`/user/app/BS5F00001`, stores its update marker under `/data/BS5fm`, and
registers the PS5 home-screen tile. The installer resolves the title-directory
install function from `libSceAppInstUtil.sprx` at runtime and falls back to
the broader app install call when needed.
- `src/websrv_lite.c` is a compact HTTP server. It serves the embedded UI,
static downloads under `/fs`, and JSON APIs under `/api/fs`.
- `src/transfer.c` implements the file-manager API: list, stat, folder size,
@@ -114,3 +119,5 @@ port `5905` instead.
Recursive folder size, copy, move, and delete can still take time on very large
external drives. Progress polling is tolerant of short hiccups, and active jobs
continue on the PS5 side even if the browser temporarily loses contact.
Copy, move, and delete jobs report PS5-side throughput in `MB/s` plus ETA from
the server-side job counters.
+18 -5
View File
@@ -727,6 +727,12 @@
return s + 's';
}
function speedMBps(bytesPerSecond) {
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return '';
const mb = bytesPerSecond / (1024 * 1024);
return mb.toFixed(mb >= 10 ? 1 : 2) + ' MB/s';
}
function formatDate(seconds) {
if (!Number.isFinite(seconds) || seconds <= 0) return 'Unknown';
return new Date(seconds * 1000).toLocaleString();
@@ -739,7 +745,10 @@
const totalFiles = Number(job.totalFiles || 0);
const failedFiles = Number(job.failedFiles || 0);
const elapsed = Math.max(0, (Date.now() / 1000) - Number(job.startedAt || 0));
const speed = elapsed > 0 && copied > 0 ? copied / elapsed : 0;
const reportedSpeed = Number(job.speedBytesPerSec || 0);
const speed = reportedSpeed > 0 ? reportedSpeed :
(elapsed > 0 && copied > 0 ? copied / elapsed : 0);
const reportedEta = Number(job.etaSeconds || 0);
const pct = total > 0 ? Math.max(0, Math.min(100, (copied / total) * 100)) : 0;
const parts = [];
@@ -755,8 +764,12 @@
}
if (speed > 0) {
parts.push(shortSize(speed) + '/s');
if (total > copied) parts.push('ETA ' + shortDuration((total - copied) / speed));
parts.push(speedMBps(speed));
if (reportedEta > 0) {
parts.push('ETA ' + shortDuration(reportedEta));
} else if (total > copied) {
parts.push('ETA ' + shortDuration((total - copied) / speed));
}
}
if (failedFiles > 0) parts.push(failedFiles + ' failed');
@@ -1027,7 +1040,7 @@
if (!job.busy) return job;
} catch (err) {
missed++;
if (missed >= 120) throw err;
if (missed >= 600) throw err;
}
await new Promise((resolve) => setTimeout(resolve, 700));
}
@@ -1171,7 +1184,7 @@
} catch (err) {
missed++;
text.textContent = 'Waiting for status...';
if (missed >= 120) {
if (missed >= 600) {
clearInterval(state.jobTimer);
state.jobTimer = 0;
box.classList.remove('show');
+25 -7
View File
@@ -18,7 +18,9 @@
#define BS5FM_APP_TITLE_ID "BS5F00001"
#define BS5FM_APP_ROOT "/user/app"
#define BS5FM_APP_PARENT BS5FM_APP_ROOT "/"
#define BS5FM_INSTALL_MARKER "bs5filemanager-launcher-v2\n"
#define BS5FM_DATA_DIR "/data/BS5fm"
#define BS5FM_MARKER_PATH BS5FM_DATA_DIR "/launcher.ok"
#define BS5FM_INSTALL_MARKER "bs5filemanager-launcher-v3\n"
#define INCASSET(name, file) \
__asm__(".section .rodata\n" \
@@ -37,6 +39,7 @@ INCASSET(bs5fm_icon0_png, "assets-app/icon0.png");
int sceAppInstUtilInitialize(void);
int sceAppInstUtilAppInstallAll(void *);
int sceAppInstUtilAppUnInstall(const char *);
typedef int (*app_install_title_dir_fn)(const char *, const char *, void *);
@@ -106,27 +109,33 @@ mkdir_if_needed(const char *path) {
}
static int
ensure_data_dir(void) {
if(mkdir_if_needed("/data") != 0) return -1;
return mkdir_if_needed(BS5FM_DATA_DIR);
}
int
bs5fm_install_app_if_needed(void) {
char app_dir[256];
char sce_sys_dir[256];
char param_path[256];
char icon_path[256];
char marker_path[256];
struct stat st;
snprintf(app_dir, sizeof(app_dir), BS5FM_APP_ROOT "/%s", BS5FM_APP_TITLE_ID);
snprintf(sce_sys_dir, sizeof(sce_sys_dir), "%s/sce_sys", app_dir);
snprintf(param_path, sizeof(param_path), "%s/param.json", sce_sys_dir);
snprintf(icon_path, sizeof(icon_path), "%s/icon0.png", sce_sys_dir);
snprintf(marker_path, sizeof(marker_path), "%s/bs5fm.ok", app_dir);
int needs_install = stat(app_dir, &st) != 0 ||
int app_exists = stat(app_dir, &st) == 0;
int needs_install = !app_exists ||
file_differs(param_path, bs5fm_param_json,
bs5fm_param_json_size) ||
file_differs(icon_path, bs5fm_icon0_png,
bs5fm_icon0_png_size) ||
file_differs(marker_path, g_install_marker,
file_differs(BS5FM_MARKER_PATH, g_install_marker,
sizeof(g_install_marker) - 1);
if(!needs_install) return 0;
@@ -143,6 +152,11 @@ bs5fm_install_app_if_needed(void) {
return -1;
}
if(app_exists) {
int uninstall_err = sceAppInstUtilAppUnInstall(BS5FM_APP_TITLE_ID);
printf(" launcher install: refresh old tile 0x%08x\n", uninstall_err);
}
if(mkdir_if_needed(app_dir) != 0 || mkdir_if_needed(sce_sys_dir) != 0) {
printf(" launcher install: mkdir failed errno %d\n", errno);
return -1;
@@ -164,9 +178,13 @@ bs5fm_install_app_if_needed(void) {
return -1;
}
if(write_file(marker_path, g_install_marker,
if(ensure_data_dir() != 0) {
printf(" launcher install: warning, failed creating %s errno %d\n",
BS5FM_DATA_DIR, errno);
} else if(write_file(BS5FM_MARKER_PATH, g_install_marker,
sizeof(g_install_marker) - 1) != 0) {
printf(" launcher install: warning, failed writing %s\n", marker_path);
printf(" launcher install: warning, failed writing %s\n",
BS5FM_MARKER_PATH);
}
bs5fm_notify("BS5FileManager app ready",
+2 -2
View File
@@ -25,7 +25,7 @@
#define BS5FM_WEB_PORT 5905
int sceNetCtlInit(void);
int sceSystemServiceLaunchWebBrowser(const char *uri);
int sceSystemServiceLaunchWebBrowser(const char *uri, void *);
int sceUserServiceInitialize(void *);
@@ -76,7 +76,7 @@ static void
launch_browser_once(ready_state_t *state) {
if(!state->launch_browser || state->browser_launched) return;
int err = sceSystemServiceLaunchWebBrowser("http://127.0.0.1:5905/");
int err = sceSystemServiceLaunchWebBrowser("http://127.0.0.1:5905/", NULL);
if(err == 0) {
puts(" ps5 browser: opened http://127.0.0.1:5905/");
} else {
+31 -10
View File
@@ -388,7 +388,7 @@ copy_recursive(const char *src, const char *dst) {
static int
delete_recursive(const char *path) {
delete_recursive(const char *path, int count_progress) {
if(job_cancelled()) {
errno = ECANCELED;
return -1;
@@ -405,7 +405,7 @@ delete_recursive(const char *path) {
if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) continue;
char child[1024];
join_path(child, sizeof(child), path, ent->d_name);
if(delete_recursive(child) != 0) {
if(delete_recursive(child, count_progress) != 0) {
rc = -1;
atomic_fetch_add(&g_job.failed_files, 1);
if(job_cancelled()) break;
@@ -415,16 +415,24 @@ delete_recursive(const char *path) {
if(rc == 0) {
job_set_current(path);
if(rmdir(path) != 0) return -1;
if(st.st_size > 0) atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size);
atomic_fetch_add(&g_job.done_files, 1);
if(count_progress) {
if(st.st_size > 0) {
atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size);
}
atomic_fetch_add(&g_job.done_files, 1);
}
}
return rc;
}
job_set_current(path);
if(unlink(path) != 0) return -1;
if(st.st_size > 0) atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size);
atomic_fetch_add(&g_job.done_files, 1);
if(count_progress) {
if(st.st_size > 0) {
atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size);
}
atomic_fetch_add(&g_job.done_files, 1);
}
return 0;
}
@@ -492,7 +500,7 @@ copy_worker(void *arg) {
free(a);
return NULL;
}
if(a->is_move && delete_recursive(a->src) != 0) {
if(a->is_move && delete_recursive(a->src, 0) != 0) {
char err[160];
snprintf(err, sizeof(err), "post-move cleanup: %s", strerror(errno));
job_end(-1, err);
@@ -520,7 +528,7 @@ delete_worker(void *arg) {
atomic_store(&g_job.total_files, (int)(items > 0 ? items : 1));
atomic_store(&g_job.total_bytes, bytes);
if(delete_recursive(a->path) != 0) {
if(delete_recursive(a->path, 1) != 0) {
char err[160];
snprintf(err, sizeof(err), "delete: %s", strerror(errno));
job_end(-1, err);
@@ -882,6 +890,16 @@ status_handler(const http_request_t *req) {
ended_at = g_job.ended_at;
pthread_mutex_unlock(&g_job.lock);
time_t now = time(NULL);
time_t ref_at = busy ? now : (ended_at ? ended_at : now);
long elapsed = started_at > 0 && ref_at > started_at
? (long)(ref_at - started_at)
: 0;
long speed = elapsed > 0 && cb > 0 ? cb / elapsed : 0;
long eta = busy && speed > 0 && tb > cb
? (tb - cb + speed - 1) / speed
: 0;
json_buf_t b = {0};
if(json_appendf(&b,
"{\"ok\":true,\"busy\":%s,\"cancelling\":%s,\"verb\":",
@@ -894,8 +912,11 @@ status_handler(const http_request_t *req) {
json_appendf(&b,
",\"totalBytes\":%ld,\"copiedBytes\":%ld,"
"\"totalFiles\":%d,\"doneFiles\":%d,\"failedFiles\":%d,"
"\"startedAt\":%ld,\"endedAt\":%ld}",
tb, cb, tf, df, ff, (long)started_at, (long)ended_at) != 0) {
"\"startedAt\":%ld,\"endedAt\":%ld,"
"\"elapsedSeconds\":%ld,\"speedBytesPerSec\":%ld,"
"\"etaSeconds\":%ld}",
tb, cb, tf, df, ff, (long)started_at, (long)ended_at,
elapsed, speed, eta) != 0) {
free(b.data);
return -1;
}