Caricamenti di cartelle grandi e servizio sempre aggiornato

- daemon: gli stack dei thread si riusano (pthread_join prima del riuso):
  un thread per file senza riuso esauriva la memoria dopo qualche
  migliaio di file e il caricamento si fermava
- daemon: /v1/ping riporta quando è partito; la UI sostituisce da sola un
  servizio più vecchio del file sul disco, senza riavviare la console
- client: il servizio si avvia con elfldr o, in mancanza, con websrv
- pagina: schermo acceso durante i caricamenti, ripresa immediata quando
  la pagina torna visibile, meno richieste per file
This commit is contained in:
Cristian Laporta committed 2026-10-04 17:15:10 +02:00
1 parent d6defbd67e
commit c4e89f41ff
5 files changed
+71 -21

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@
#endif
// versione dell'app: la usano gli aggiornamenti, va alzata a ogni rilascio.
#ifndef OMEGA_VERSION
#define OMEGA_VERSION "2026.10.04.16"
#define OMEGA_VERSION "2026.10.04.17"
#endif
// Dati dell'app. Il demone omega_redirect legge session.json e ui-active da
+2 -4
View File
@@ -102,15 +102,13 @@ static int send_elfldr(const char *elf) {
}
#endif
// Il servizio di Omega solo con elfldr: avviato da websrv (hbldr daemon=1) finisce
// in un processo dove cade alla prima richiesta. Senza elfldr si aspetta il riavvio.
// Il servizio di Omega: elfldr se c'è, altrimenti websrv (hbldr daemon=1).
int payload_run_service(const char *elf, char *err, size_t en) {
struct stat st;
if (stat(elf, &st) != 0) { snprintf(err, en, "%s", _("File del payload non trovato")); return -1; }
#ifdef PS5
if (send_elfldr(elf) == 0) { omega_log("servizio %s inviato a elfldr :%d", elf, ELFLDR_PORT); return 0; }
snprintf(err, en, "elfldr :%d non disponibile", ELFLDR_PORT);
return -1;
return payload_run(elf, err, en); // websrv come ripiego
#else
omega_log("(desktop) avvio simulato del servizio %s", elf);
return 0;
+16 -4
View File
@@ -675,7 +675,7 @@
function size(b) { return b >= 1073741824 ? (b / 1073741824).toFixed(1) + ' GB' : (b / 1048576).toFixed(1) + ' MB'; }
function fill(t, o) { return t.replace(/\{(\w+)\}/g, function (m, k) { return o[k] !== undefined ? o[k] : m; }); }
function upMsg(ok, err) { $('upok').textContent = ok || ''; $('uperr').textContent = err || ''; }
function upEnd() { upX = null; $('upbox').hidden = true; $('up_file').disabled = $('up_dir').disabled = false; }
function upEnd() { upX = null; $('upbox').hidden = true; $('up_file').disabled = $('up_dir').disabled = false; try { upRelease(); } catch (e) {} }
// list: [{file, path}] — path relativo dentro il lotto.
// Ogni file riprende da dove era arrivato (status + &o=): se la rete cade, la
// console si blocca o il servizio si riavvia, si riprova con attese crescenti.
@@ -706,6 +706,7 @@
if (upStop) { upEnd(); return; }
if (fails >= RETRY.length) { upEnd(); upMsg('', tx('up_fail') + tx('up_gaveup')); return; }
var wait = RETRY[fails++], left = wait;
retryNow = function () { again(); };
var tick = function () {
if (upStop) return;
$('up_txt').textContent = fill(tx('up_retry'), { s: left, k: fails, m: RETRY.length });
@@ -727,8 +728,10 @@
if (upStop) { upEnd(); return; }
if (i >= list.length) { finish(); return; }
var f = list[i].file, path = list[i].path;
// prima si chiede quanto è già arrivato (dopo un errore o un riavvio del servizio)
api('GET', '/v1/library/upload/status?b=' + batch + '&p=' + encodeURIComponent(path)).then(function (st) {
// quanto è già arrivato si chiede solo dopo un errore o alla ripresa di un lotto:
// con migliaia di file piccoli una richiesta in più per file pesa
var ask = fails > 0 || resumed ? api('GET', '/v1/library/upload/status?b=' + batch + '&p=' + encodeURIComponent(path)) : Promise.resolve({ have: 0, done: false });
ask.then(function (st) {
if (st.done && st.have === f.size) { sent += f.size; fails = 0; send(i + 1); return; }
var off = st.have > 0 && st.have <= f.size ? st.have : 0;
var x = new XMLHttpRequest(); upX = x;
@@ -749,11 +752,20 @@
}).catch(function () { retry(function () { send(i); }); });
};
upStopTimer = function () { if (timer) clearTimeout(timer); };
// il telefono non deve spegnere lo schermo: in background il browser ferma la pagina
var lock = null;
try { if (navigator.wakeLock) navigator.wakeLock.request('screen').then(function (l) { lock = l; }).catch(function () {}); } catch (e) {}
upRelease = function () { try { if (lock) lock.release(); } catch (e) {} lock = null; };
// tornando sulla pagina dopo una pausa si riprova subito, senza aspettare l'attesa in corso
upWake = function () { if (timer && !upStop) { clearTimeout(timer); timer = null; fails = Math.max(fails, 1); retryNow(); } };
try { localStorage.setItem(key + '-started', '1'); } catch (e) {}
send(0);
}).catch(function () { upMsg('', tx('up_fail')); });
}
var upStopTimer = function () {};
var upStopTimer = function () {}, upRelease = function () {}, upWake = function () {}, retryNow = function () {};
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') upWake();
});
$('up_cancel').onclick = function () { upStop = true; upStopTimer(); if (upX) upX.abort(); upEnd(); };
$('up_file').onchange = function () { var f = this.files && this.files[0]; if (f) upStart([{ file: f, path: f.name }]); this.value = ''; };
$('up_dir').onchange = function () {
+36 -8
View File
@@ -59,14 +59,40 @@ void player_log(const char *fmt, ...);
// ignorato): bastava formattare una risposta per esaurirlo. Con la memoria data
// da noi (pthread_attr_setstack) lo stack è davvero quello.
#define CTL_STACK (1024 * 1024)
// Gli stack si riusano: un caricamento di una cartella apre un thread per file e
// senza riuso migliaia di file esaurivano la memoria. I thread non sono staccati:
// prima di riusare uno stack si fa pthread_join del thread che lo usava (già finito,
// quindi ritorna subito), così si è certi che non lo stia più toccando.
#define POOL 48
static struct { void *stk; int busy; pthread_t t; } pool[POOL]; // busy: 0 libero, 1 in uso, 2 finito
static pthread_mutex_t poolmx = PTHREAD_MUTEX_INITIALIZER;
typedef struct { void *(*fn)(void *); void *arg; int slot; } ThreadJob;
static void *thread_main(void *a) {
ThreadJob j = *(ThreadJob *)a; free(a);
void *r = j.fn(j.arg);
pthread_mutex_lock(&poolmx); pool[j.slot].busy = 2; pthread_mutex_unlock(&poolmx);
return r;
}
int omega_thread(void *(*fn)(void *), void *arg) {
int slot = -1; pthread_t done_t; int need_join = 0;
pthread_mutex_lock(&poolmx);
for (int i = 0; i < POOL && slot < 0; i++) if (pool[i].busy == 0) slot = i;
for (int i = 0; i < POOL && slot < 0; i++) if (pool[i].busy == 2) { slot = i; done_t = pool[i].t; need_join = 1; }
if (slot >= 0) {
if (!pool[slot].stk && posix_memalign(&pool[slot].stk, 4096, CTL_STACK) != 0) pool[slot].stk = NULL;
if (pool[slot].stk) pool[slot].busy = 1; else slot = -1;
}
pthread_mutex_unlock(&poolmx);
if (slot < 0) return -1; // mai thread con lo stack di default: sulla console è troppo piccolo
if (need_join) pthread_join(done_t, NULL);
ThreadJob *j = malloc(sizeof *j);
if (!j) { pthread_mutex_lock(&poolmx); pool[slot].busy = 0; pthread_mutex_unlock(&poolmx); return -1; }
j->fn = fn; j->arg = arg; j->slot = slot;
pthread_attr_t at; pthread_attr_init(&at);
pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED);
void *stk = NULL;
if (posix_memalign(&stk, 4096, CTL_STACK) == 0) pthread_attr_setstack(&at, stk, CTL_STACK);
pthread_t t; int rc = pthread_create(&t, &at, fn, arg);
pthread_attr_setstack(&at, pool[slot].stk, CTL_STACK);
int rc = pthread_create(&pool[slot].t, &at, thread_main, j);
pthread_attr_destroy(&at);
if (rc && stk) free(stk);
if (rc) { free(j); pthread_mutex_lock(&poolmx); pool[slot].busy = 0; pthread_mutex_unlock(&poolmx); }
return rc;
}
#define spawn omega_thread
@@ -75,6 +101,7 @@ extern const char REMOTE_HTML[]; extern const size_t REMOTE_HTML_LEN;
static char data_dir[200];
static int lsock = -1;
static time_t started_at; // la UI lo confronta con la data del file: se il file è più nuovo, riavvia
static char last_req[160] = "-";
const char *ctl_last_request(void) { return last_req; }
static char pin[8], secret[40];
@@ -118,7 +145,7 @@ static void send_all(int s, const void *p, size_t n) {
}
static void reply(int s, int code, const char *type, const void *body, size_t len) {
const char *msg = code == 200 ? "OK" : code == 400 ? "Bad Request" : code == 404 ? "Not Found" : code == 413 ? "Payload Too Large" : code == 202 ? "Accepted" : code == 401 ? "Unauthorized" : code == 403 ? "Forbidden" : code == 429 ? "Too Many Requests" : code == 409 ? "Conflict" : code == 507 ? "Insufficient Storage" : "Error";
const char *msg = code == 200 ? "OK" : code == 400 ? "Bad Request" : code == 404 ? "Not Found" : code == 413 ? "Payload Too Large" : code == 202 ? "Accepted" : code == 401 ? "Unauthorized" : code == 403 ? "Forbidden" : code == 429 ? "Too Many Requests" : code == 409 ? "Conflict" : code == 507 ? "Insufficient Storage" : code == 503 ? "Service Unavailable" : "Error";
char h[256];
static int trace = 4; // diagnosi: i primi passi delle prime risposte nel log
if (trace > 0) player_log("risposta %d: inizio (stack %p)", code, (void *)h);
@@ -396,7 +423,7 @@ NOINLINE static int start_upload(int s, const char *qs, const char *pre, size_t
u->s = s; u->clen = clen; u->off = off; u->base = base; u->total = total; snprintf(u->dst, sizeof u->dst, "%s", dst); snprintf(u->batch, sizeof u->batch, "%s", b);
if (have > (size_t)clen) have = (size_t)clen;
if (have) { u->pre = malloc(have); if (!u->pre) { free(u); reply_json(s, 500, "{\"error\":\"memory\"}"); return 0; } memcpy(u->pre, pre, have); u->have = have; }
if (spawn(up_thread, u) != 0) { free(u->pre); free(u); reply_json(s, 500, "{\"error\":\"thread\"}"); return 0; }
if (spawn(up_thread, u) != 0) { free(u->pre); free(u); player_log("caricamento: nessun thread libero, il telefono riprova"); reply_json(s, 503, "{\"error\":\"busy\"}"); return 0; }
return 1;
}
@@ -682,7 +709,7 @@ static int handle(int s, int local) {
if (!strcmp(path, "/v1/library/space")) { char up[300], o[96]; up_root(up, sizeof up); snprintf(o, sizeof o, "{\"free\":%lld}", free_bytes(up)); reply_json(s, 200, o); return 0; }
if (!strcmp(path, "/v1/library/file")) { upload_file(s, qcopy); return 0; }
if (!strcmp(path, "/v1/library/upload/status")) { upload_status(s, qcopy); return 0; }
if (!strcmp(path, "/v1/ping")) { reply_json(s, 200, "{\"ok\":true,\"service\":\"omega\"}"); return 0; }
if (!strcmp(path, "/v1/ping")) { char o[96]; snprintf(o, sizeof o, "{\"ok\":true,\"service\":\"omega\",\"started\":%ld}", (long)started_at); reply_json(s, 200, o); return 0; }
reply_json(s, 404, "{\"error\":\"not_found\"}"); return 0;
}
if (strcmp(method, "POST")) { reply_json(s, 400, "{\"error\":\"method\"}"); return 0; }
@@ -754,6 +781,7 @@ static void *ctl_thread(void *arg) {
}
int ctl_start(int port, const char *dir) {
started_at = time(NULL);
snprintf(data_dir, sizeof data_dir, "%s", dir ? dir : ".");
remote_load();
lsock = socket(AF_INET, SOCK_STREAM, 0);
+16 -4
View File
@@ -678,7 +678,7 @@ const char REMOTE_HTML[] =
" function size(b) { return b >= 1073741824 ? (b / 1073741824).toFixed(1) + ' GB' : (b / 1048576).toFixed(1) + ' MB'; }\n"
" function fill(t, o) { return t.replace(/\\{(\\w+)\\}/g, function (m, k) { return o[k] !== undefined ? o[k] : m; }); }\n"
" function upMsg(ok, err) { $('upok').textContent = ok || ''; $('uperr').textContent = err || ''; }\n"
" function upEnd() { upX = null; $('upbox').hidden = true; $('up_file').disabled = $('up_dir').disabled = false; }\n"
" function upEnd() { upX = null; $('upbox').hidden = true; $('up_file').disabled = $('up_dir').disabled = false; try { upRelease(); } catch (e) {} }\n"
" // list: [{file, path}] — path relativo dentro il lotto.\n"
" // Ogni file riprende da dove era arrivato (status + &o=): se la rete cade, la\n"
" // console si blocca o il servizio si riavvia, si riprova con attese crescenti.\n"
@@ -709,6 +709,7 @@ const char REMOTE_HTML[] =
" if (upStop) { upEnd(); return; }\n"
" if (fails >= RETRY.length) { upEnd(); upMsg('', tx('up_fail') + tx('up_gaveup')); return; }\n"
" var wait = RETRY[fails++], left = wait;\n"
" retryNow = function () { again(); };\n"
" var tick = function () {\n"
" if (upStop) return;\n"
" $('up_txt').textContent = fill(tx('up_retry'), { s: left, k: fails, m: RETRY.length });\n"
@@ -730,8 +731,10 @@ const char REMOTE_HTML[] =
" if (upStop) { upEnd(); return; }\n"
" if (i >= list.length) { finish(); return; }\n"
" var f = list[i].file, path = list[i].path;\n"
" // prima si chiede quanto è già arrivato (dopo un errore o un riavvio del servizio)\n"
" api('GET', '/v1/library/upload/status?b=' + batch + '&p=' + encodeURIComponent(path)).then(function (st) {\n"
" // quanto è già arrivato si chiede solo dopo un errore o alla ripresa di un lotto:\n"
" // con migliaia di file piccoli una richiesta in più per file pesa\n"
" var ask = fails > 0 || resumed ? api('GET', '/v1/library/upload/status?b=' + batch + '&p=' + encodeURIComponent(path)) : Promise.resolve({ have: 0, done: false });\n"
" ask.then(function (st) {\n"
" if (st.done && st.have === f.size) { sent += f.size; fails = 0; send(i + 1); return; }\n"
" var off = st.have > 0 && st.have <= f.size ? st.have : 0;\n"
" var x = new XMLHttpRequest(); upX = x;\n"
@@ -752,11 +755,20 @@ const char REMOTE_HTML[] =
" }).catch(function () { retry(function () { send(i); }); });\n"
" };\n"
" upStopTimer = function () { if (timer) clearTimeout(timer); };\n"
" // il telefono non deve spegnere lo schermo: in background il browser ferma la pagina\n"
" var lock = null;\n"
" try { if (navigator.wakeLock) navigator.wakeLock.request('screen').then(function (l) { lock = l; }).catch(function () {}); } catch (e) {}\n"
" upRelease = function () { try { if (lock) lock.release(); } catch (e) {} lock = null; };\n"
" // tornando sulla pagina dopo una pausa si riprova subito, senza aspettare l'attesa in corso\n"
" upWake = function () { if (timer && !upStop) { clearTimeout(timer); timer = null; fails = Math.max(fails, 1); retryNow(); } };\n"
" try { localStorage.setItem(key + '-started', '1'); } catch (e) {}\n"
" send(0);\n"
" }).catch(function () { upMsg('', tx('up_fail')); });\n"
" }\n"
" var upStopTimer = function () {};\n"
" var upStopTimer = function () {}, upRelease = function () {}, upWake = function () {}, retryNow = function () {};\n"
" document.addEventListener('visibilitychange', function () {\n"
" if (document.visibilityState === 'visible') upWake();\n"
" });\n"
" $('up_cancel').onclick = function () { upStop = true; upStopTimer(); if (upX) upX.abort(); upEnd(); };\n"
" $('up_file').onchange = function () { var f = this.files && this.files[0]; if (f) upStart([{ file: f, path: f.name }]); this.value = ''; };\n"
" $('up_dir').onchange = function () {\n"