diff --git a/client/source/omega.h b/client/source/omega.h index deeca1f..b0f494d 100644 --- a/client/source/omega.h +++ b/client/source/omega.h @@ -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 diff --git a/client/source/payload.c b/client/source/payload.c index 4531801..da816e0 100644 --- a/client/source/payload.c +++ b/client/source/payload.c @@ -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; diff --git a/daemon/remote/remote.html b/daemon/remote/remote.html index d11b641..6b633a7 100644 --- a/daemon/remote/remote.html +++ b/daemon/remote/remote.html @@ -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 () { diff --git a/daemon/source/ctl.c b/daemon/source/ctl.c index b3ce080..cd9d0d8 100644 --- a/daemon/source/ctl.c +++ b/daemon/source/ctl.c @@ -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); diff --git a/daemon/source/remote_page.c b/daemon/source/remote_page.c index 78a5103..3cc45cd 100644 --- a/daemon/source/remote_page.c +++ b/daemon/source/remote_page.c @@ -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"