@mmmbuto/nexuscrew 0.9.51 → 0.9.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/docs/NODES.md +1 -1
- package/docs/SECURITY.md +1 -1
- package/frontend/dist/assets/index-DBlFdkZn.js +169 -0
- package/frontend/dist/assets/{index-CcZskBmy.css → index-Da4oBb4f.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +63 -14
- package/lib/cli/messages.js +55 -0
- package/lib/cli/url.js +6 -2
- package/lib/notify/ask-answer-service.js +1 -0
- package/lib/notify/asks.js +58 -7
- package/lib/notify/event-feed-client.js +420 -48
- package/lib/notify/event-feed-routes.js +44 -3
- package/lib/notify/persist.js +69 -12
- package/lib/notify/routes.js +19 -1
- package/lib/prefs/routes.js +52 -0
- package/lib/prefs/store.js +112 -0
- package/lib/proxy/federation.js +49 -5
- package/lib/server.js +112 -34
- package/lib/update/core.js +45 -0
- package/lib/update/manager.js +31 -8
- package/lib/update/npm-env.js +109 -0
- package/lib/update/runner.js +54 -14
- package/lib/update/start-lock.js +106 -0
- package/package.json +1 -1
- package/frontend/dist/assets/index-BDdWDcTt.js +0 -169
package/lib/update/runner.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
PACKAGE_NAME, parseVersion, scrubError, adoptUpdateLock, releaseUpdateLock, readState, writeState,
|
|
10
10
|
stableRuntimeDir,
|
|
11
11
|
} = require('./core.js');
|
|
12
|
+
const npmEnv = require('./npm-env.js');
|
|
12
13
|
|
|
13
14
|
// da revisione — rigenerazione delle definizioni di boot dopo l'installazione.
|
|
14
15
|
//
|
|
@@ -233,6 +234,21 @@ function args(argv) {
|
|
|
233
234
|
return out;
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
// Versione del processo che ascolta su `port`, dal suo /api/config. null = non leggibile (non si presume).
|
|
238
|
+
async function readRuntimeVersion(port, token, opts = {}) {
|
|
239
|
+
if (typeof opts.runtimeVersionImpl === 'function') return opts.runtimeVersionImpl(port, token);
|
|
240
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
241
|
+
if (typeof fetchImpl !== 'function') return null;
|
|
242
|
+
try {
|
|
243
|
+
const r = await fetchImpl(`http://127.0.0.1:${port}/api/config`, {
|
|
244
|
+
headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout?.(5000),
|
|
245
|
+
});
|
|
246
|
+
if (r.status !== 200) return null;
|
|
247
|
+
const v = (await r.json()).version;
|
|
248
|
+
return typeof v === 'string' && v ? v : null;
|
|
249
|
+
} catch (_) { return null; }
|
|
250
|
+
}
|
|
251
|
+
|
|
236
252
|
async function restartRuntime(opts = {}) {
|
|
237
253
|
const home = opts.home || os.homedir();
|
|
238
254
|
const commands = opts.commands || require('../cli/commands.js');
|
|
@@ -298,7 +314,15 @@ async function restartRuntime(opts = {}) {
|
|
|
298
314
|
await wait(100);
|
|
299
315
|
}
|
|
300
316
|
if (!released) throw new Error(`restart portatile fallito: porta ${port} non liberata`);
|
|
301
|
-
commands.startPortable({ platform, home });
|
|
317
|
+
const launched = commands.startPortable({ platform, home });
|
|
318
|
+
if (launched && launched.started === false) {
|
|
319
|
+
// «già in esecuzione»/non avviato NON e' un successo: lo e' solo se il processo che risponde e' gia' alla
|
|
320
|
+
// versione attesa (verificato), altrimenti l'update ha installato codice che nessuno esegue.
|
|
321
|
+
const running = opts.expectedVersion ? await readRuntimeVersion(port, token, opts) : null;
|
|
322
|
+
if (!opts.expectedVersion || running !== opts.expectedVersion) {
|
|
323
|
+
throw new Error(`avvio portatile non riuscito (${launched.reason || 'started:false'}); versione in esecuzione ${running || 'non leggibile'}${opts.expectedVersion ? `, attesa ${opts.expectedVersion}` : ''}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
302
326
|
mode = 'portable';
|
|
303
327
|
}
|
|
304
328
|
}
|
|
@@ -309,6 +333,12 @@ async function restartRuntime(opts = {}) {
|
|
|
309
333
|
...(opts.healthProbeImpl ? { probeImpl: opts.healthProbeImpl } : {}),
|
|
310
334
|
});
|
|
311
335
|
if (!healthy) throw new Error(`NexusCrew ${mode} non healthy su 127.0.0.1:${port} dopo il restart`);
|
|
336
|
+
if (opts.expectedVersion) {
|
|
337
|
+
const running = await readRuntimeVersion(port, token, opts);
|
|
338
|
+
if (running !== opts.expectedVersion) {
|
|
339
|
+
throw new Error(`NexusCrew ${mode} risponde ma esegue la versione ${running || 'non leggibile'}, attesa ${opts.expectedVersion}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
312
342
|
}
|
|
313
343
|
return mode;
|
|
314
344
|
}
|
|
@@ -320,10 +350,20 @@ async function runUpdate(opts = {}) {
|
|
|
320
350
|
const statusPath = opts.statusPath || path.join(home, '.nexuscrew', 'npm-update.json');
|
|
321
351
|
const workDir = opts.cwd || stableRuntimeDir(home);
|
|
322
352
|
const execImpl = opts.execImpl || execFileSync;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
353
|
+
const packageRoot = opts.packageRoot || path.resolve(__dirname, '..', '..');
|
|
354
|
+
const log = opts.log || console.log;
|
|
355
|
+
// La versione si legge dal package.json del percorso ESEGUITO (realpath), non con `npm ls`: e' il codice che il
|
|
356
|
+
// riavvio caricherebbe davvero.
|
|
357
|
+
const readInstalledVersion = opts.readInstalledVersion || (() => npmEnv.readInstalledFrom(packageRoot).version);
|
|
358
|
+
const installedPath = () => { try { return npmEnv.redact((opts.readInstalledFromImpl || (() => npmEnv.readInstalledFrom(packageRoot)))().path, home); } catch (_) { return ''; } };
|
|
359
|
+
// npm = STESSO node del demone; la preflight prova, prima di installare, che `npm -g` scriverebbe dove il codice vive.
|
|
360
|
+
const invocation = (opts.npmInvocationImpl || npmEnv.resolveNpmInvocation)({ execPath: opts.execPath || process.execPath });
|
|
361
|
+
const runNpm = (npmArgs) => execImpl(invocation.bin, [...invocation.argvPrefix, ...npmArgs], {
|
|
362
|
+
stdio: 'inherit', timeout: 5 * 60 * 1000, cwd: workDir, ...(invocation.env ? { env: invocation.env } : {}),
|
|
326
363
|
});
|
|
364
|
+
const logEnv = (phase) => log(`env-check[${phase}] ${npmEnv.describeEnv({
|
|
365
|
+
execPath: opts.execPath || process.execPath, envPath: opts.envPath !== undefined ? opts.envPath : process.env.PATH, home,
|
|
366
|
+
})} npm=${invocation.kind}${invocation.npmCli ? `:${npmEnv.redact(invocation.npmCli, home)}` : ''} pkg=${npmEnv.redact(packageRoot, home)}`);
|
|
327
367
|
const preflightImpl = opts.preflightImpl || (({ version: expectedVersion = version } = {}) => {
|
|
328
368
|
const bin = path.resolve(__dirname, '..', '..', 'bin', 'nexuscrew.js');
|
|
329
369
|
const output = execFileSync(process.execPath, [bin, 'version'], {
|
|
@@ -357,11 +397,11 @@ async function runUpdate(opts = {}) {
|
|
|
357
397
|
: undefined;
|
|
358
398
|
try {
|
|
359
399
|
writeState(statusPath, { ...previous, phase: 'installing', targetVersion: version, updaterPid: process.pid, lastError: '' });
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
});
|
|
400
|
+
logEnv('pre-install');
|
|
401
|
+
await (opts.npmPreflightImpl || npmEnv.npmPreflight)({ invocation, packageRoot, home });
|
|
402
|
+
runNpm(['install', '--global', `${PACKAGE_NAME}@${version}`, '--no-audit', '--no-fund']);
|
|
363
403
|
const installed = String(readInstalledVersion() || '');
|
|
364
|
-
if (installed !== version) throw new Error(`verifica installazione fallita: attesa ${version}, trovata ${installed || 'sconosciuta'}`);
|
|
404
|
+
if (installed !== version) throw new Error(`verifica installazione fallita: attesa ${version}, trovata ${installed || 'sconosciuta'}${installedPath() ? ` in ${installedPath()}` : ''}`);
|
|
365
405
|
installedNew = true;
|
|
366
406
|
await preflightImpl({ version, home });
|
|
367
407
|
writeState(statusPath, { ...readState(statusPath), phase: 'restarting', updaterPid: process.pid, lastError: '' });
|
|
@@ -376,7 +416,8 @@ async function runUpdate(opts = {}) {
|
|
|
376
416
|
home, running: wasRunning, execImpl, ...(opts.regenSeams || {}),
|
|
377
417
|
...(opts.regenBootImpl ? { regenImpl: opts.regenBootImpl } : {}),
|
|
378
418
|
});
|
|
379
|
-
const restartMode = await restartImpl({ home, ...(opts.runtimeSeams || {}) });
|
|
419
|
+
const restartMode = await restartImpl({ home, expectedVersion: version, ...(opts.runtimeSeams || {}) });
|
|
420
|
+
logEnv('post-restart');
|
|
380
421
|
writeState(statusPath, {
|
|
381
422
|
...readState(statusPath), phase: 'installed', current: version, latest: version,
|
|
382
423
|
available: false, blockedVersion: '', lastUpdatedAt: new Date().toISOString(), lastError: '',
|
|
@@ -386,9 +427,8 @@ async function runUpdate(opts = {}) {
|
|
|
386
427
|
let rollbackError = null; let rolledBack = false;
|
|
387
428
|
if (installedNew && parseVersion(previousVersion) && previousVersion !== version) {
|
|
388
429
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
});
|
|
430
|
+
logEnv('pre-rollback');
|
|
431
|
+
runNpm(['install', '--global', `${PACKAGE_NAME}@${previousVersion}`, '--no-audit', '--no-fund']);
|
|
392
432
|
if (String(readInstalledVersion() || '') !== previousVersion) throw new Error(`rollback verify: attesa ${previousVersion}`);
|
|
393
433
|
await preflightImpl({ version: previousVersion, home, rollback: true });
|
|
394
434
|
// da revisione: il rollback riparte dalle stesse definizioni sane — il
|
|
@@ -399,7 +439,7 @@ async function runUpdate(opts = {}) {
|
|
|
399
439
|
home, running: wasRunning, execImpl, ...(opts.regenSeams || {}),
|
|
400
440
|
...(opts.regenBootImpl ? { regenImpl: opts.regenBootImpl } : {}),
|
|
401
441
|
});
|
|
402
|
-
await restartImpl({ home, ...(opts.runtimeSeams || {}) });
|
|
442
|
+
await restartImpl({ home, expectedVersion: previousVersion, ...(opts.runtimeSeams || {}) });
|
|
403
443
|
rolledBack = true;
|
|
404
444
|
} catch (rollbackFailure) { rollbackError = rollbackFailure; }
|
|
405
445
|
}
|
|
@@ -425,4 +465,4 @@ if (require.main === module) {
|
|
|
425
465
|
.catch(() => { process.exitCode = 1; });
|
|
426
466
|
}
|
|
427
467
|
|
|
428
|
-
module.exports = { args, restartRuntime, runUpdate, regenBootDefinitions, healBootDefinitions };
|
|
468
|
+
module.exports = { args, readRuntimeVersion, restartRuntime, runUpdate, regenBootDefinitions, healBootDefinitions };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Lock di avvio condiviso: serializza chi EMETTE un avvio/riavvio del nodo (boot via start, `show`, `start`,
|
|
3
|
+
// `restart`, runner dell'update). Due avviatori simultanei creavano un nodo che ne uccideva un altro appena partito.
|
|
4
|
+
//
|
|
5
|
+
// Contro il deadlock:
|
|
6
|
+
// - sezione critica breve: si tiene solo mentre si EMETTE il comando (spawn/systemctl/launchctl), mai mentre si
|
|
7
|
+
// attende la salute del nodo, quindi il processo appena avviato non ha nulla da attendere;
|
|
8
|
+
// - rientrante nello STESSO processo (smartUp -> restart -> startPortable non si blocca da solo);
|
|
9
|
+
// - proprietario VERIFICATO: pid vivo e, se noto, stessa nascita del processo (pid riassegnato = lock orfano);
|
|
10
|
+
// - SCADENZA: oltre ttlMs il lock si considera stantio anche con il proprietario vivo, e si riprende;
|
|
11
|
+
// - attesa limitata: allo scadere errore chiaro `start-lock-timeout` (con chi lo tiene), mai attesa infinita;
|
|
12
|
+
// - si rilascia/toglie solo un lock col PROPRIO token.
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const crypto = require('node:crypto');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_TTL_MS = 60 * 1000;
|
|
19
|
+
const DEFAULT_WAIT_MS = 20 * 1000;
|
|
20
|
+
const POLL_MS = 100;
|
|
21
|
+
const held = new Map(); // file -> { depth, token }
|
|
22
|
+
|
|
23
|
+
const sleepSync = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch (_) { /* best effort */ } };
|
|
24
|
+
|
|
25
|
+
function lockPath(home) { return path.join(home || os.homedir(), '.nexuscrew', 'start.lock'); }
|
|
26
|
+
|
|
27
|
+
function pidAlive(pid) {
|
|
28
|
+
if (!Number.isInteger(pid) || pid < 1) return false;
|
|
29
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function processStartOf(pid, impl) {
|
|
33
|
+
try { return (impl || require('../cli/pidfile.js').readProcessStart)(pid) || ''; } catch (_) { return ''; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readLock(file) {
|
|
37
|
+
try {
|
|
38
|
+
const st = fs.lstatSync(file);
|
|
39
|
+
if (!st.isFile() || st.isSymbolicLink()) return { corrupt: true };
|
|
40
|
+
const v = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
41
|
+
return v && Number.isInteger(v.pid) && typeof v.token === 'string' ? v : { corrupt: true };
|
|
42
|
+
} catch (e) { return e.code === 'ENOENT' ? null : { corrupt: true }; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isStale(meta, now, ttlMs, impl = {}) {
|
|
46
|
+
if (!meta || meta.corrupt) return true;
|
|
47
|
+
if (!pidAlive(meta.pid)) return true;
|
|
48
|
+
if (meta.processStart) {
|
|
49
|
+
const live = processStartOf(meta.pid, impl.readProcessStartImpl);
|
|
50
|
+
if (live && live !== meta.processStart) return true; // pid riassegnato a un altro processo
|
|
51
|
+
}
|
|
52
|
+
return now - Number(meta.createdAt || 0) > ttlMs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unlinkIfToken(file, token) {
|
|
56
|
+
const cur = readLock(file);
|
|
57
|
+
if (cur && !cur.corrupt && cur.token !== token) return false;
|
|
58
|
+
try { fs.unlinkSync(file); return true; } catch (e) { return e.code === 'ENOENT'; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ritorna un handle {release()}. Sincrono: chi lo chiama (spawn, execFileSync) lo e' gia'.
|
|
62
|
+
function acquireStartLock({ home, ttlMs = DEFAULT_TTL_MS, waitMs = DEFAULT_WAIT_MS, label = '', impl = {} } = {}) {
|
|
63
|
+
const file = lockPath(home);
|
|
64
|
+
const mine = held.get(file);
|
|
65
|
+
if (mine) { mine.depth += 1; return { release: () => releaseHeld(file) }; }
|
|
66
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
67
|
+
const deadline = Date.now() + waitMs;
|
|
68
|
+
for (;;) {
|
|
69
|
+
const token = crypto.randomBytes(12).toString('hex');
|
|
70
|
+
try {
|
|
71
|
+
fs.writeFileSync(file, `${JSON.stringify({ pid: process.pid, token, createdAt: Date.now(), label, processStart: processStartOf(process.pid, impl.readProcessStartImpl) })}\n`, { flag: 'wx', mode: 0o600 });
|
|
72
|
+
held.set(file, { depth: 1, token });
|
|
73
|
+
return { release: () => releaseHeld(file) };
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e.code !== 'EEXIST') throw e;
|
|
76
|
+
}
|
|
77
|
+
const meta = readLock(file);
|
|
78
|
+
if (isStale(meta, Date.now(), ttlMs, impl)) {
|
|
79
|
+
// ripresa: solo se il file e' ancora quello che abbiamo giudicato stantio
|
|
80
|
+
const again = readLock(file);
|
|
81
|
+
if (!again || again.corrupt || again.token === (meta && meta.token)) { unlinkIfToken(file, meta && !meta.corrupt ? meta.token : undefined); continue; }
|
|
82
|
+
}
|
|
83
|
+
if (Date.now() >= deadline) {
|
|
84
|
+
const error = new Error(`avvio gia' in corso (${meta && meta.label ? meta.label : 'altro avviatore'}, pid ${meta && meta.pid}): riprova fra poco`);
|
|
85
|
+
error.code = 'start-lock-timeout'; error.holder = meta && !meta.corrupt ? { pid: meta.pid, label: meta.label || '', ageMs: Date.now() - Number(meta.createdAt || 0) } : null;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
sleepSync(POLL_MS);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function releaseHeld(file) {
|
|
93
|
+
const mine = held.get(file);
|
|
94
|
+
if (!mine) return;
|
|
95
|
+
mine.depth -= 1;
|
|
96
|
+
if (mine.depth > 0) return;
|
|
97
|
+
held.delete(file);
|
|
98
|
+
unlinkIfToken(file, mine.token);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function withStartLock(fn, opts = {}) {
|
|
102
|
+
const handle = acquireStartLock(opts);
|
|
103
|
+
try { return fn(); } finally { handle.release(); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { DEFAULT_TTL_MS, DEFAULT_WAIT_MS, lockPath, acquireStartLock, withStartLock, isStale, readLock };
|