livedesk 0.1.591 → 0.1.593
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/README.md +8 -8
- package/bin/livedesk.js +115 -112
- package/bootstrap/client-process-identity.js +1 -1
- package/bootstrap/device-role.js +1 -1
- package/bootstrap/legacy-client-update.js +15 -15
- package/bootstrap/role-bootstrap-server.js +2 -2
- package/bootstrap/runtime-lock.js +4 -4
- package/bootstrap/runtime-shutdown.js +3 -3
- package/bootstrap/update-host-client.mjs +9 -9
- package/bootstrap/update-host.mjs +6 -6
- package/client/README.md +10 -10
- package/client/THIRD_PARTY_NOTICES.md +1 -1
- package/client/bin/client-version.js +1 -1
- package/client/bin/livedesk-client-node.js +102 -102
- package/client/bin/livedesk-client-update-bootstrap.cjs +12 -12
- package/client/bin/livedesk-client.js +136 -122
- package/client/package.json +7 -7
- package/client/src/runtime/agent-process-lifecycle.js +10 -10
- package/client/src/runtime/agent-shell.js +66 -66
- package/client/src/runtime/client-runtime-server.js +12 -11
- package/client/src/runtime/linux-video-acceleration.js +1 -1
- package/client/src/runtime/windows-owned-process-manifest.js +9 -9
- package/client/tests/client-version.test.mjs +2 -2
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +5 -5
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +7 -7
- package/diagnostics/livedesk-transport-physical.mjs +3 -3
- package/electron/auth-session-owner.mjs +24 -24
- package/electron/desktop-clipboard-owner.mjs +382 -382
- package/electron/electron-builder.e2e.yml +1 -1
- package/electron/electron-builder.mac-arm64.yml +1 -1
- package/electron/electron-builder.mac-x64.yml +1 -1
- package/electron/electron-builder.win-dev.yml +1 -1
- package/electron/electron-builder.win.yml +2 -2
- package/electron/electron-builder.yml +2 -2
- package/electron/main.mjs +175 -174
- package/electron/preload.cjs +13 -13
- package/electron/runtime-role-transition.mjs +17 -17
- package/hub/package.json +4 -4
- package/hub/src/agents/agent-manager.js +2 -2
- package/hub/src/agents/agent-permissions.js +2 -2
- package/hub/src/agents/agent-tool-registry.js +36 -36
- package/hub/src/agents/codex-agent-runtime.js +16 -16
- package/hub/src/agents/codex-mcp-server.js +5 -5
- package/hub/src/filesystem/roots.js +20 -20
- package/hub/src/filesystem/shared-folders.js +2 -2
- package/hub/src/live-desk-update.js +9 -9
- package/hub/src/remote-clipboard-contract.mjs +482 -482
- package/hub/src/remote-hub.js +1025 -1025
- package/hub/src/server.js +146 -146
- package/hub/src/settings/effective-device-policy.js +31 -31
- package/hub/src/settings/settings-schema.js +2 -2
- package/package.json +7 -7
- package/runtime-core/package.json +2 -2
- package/runtime-core/src/auth-config.js +1 -1
- package/scripts/sync-web-dist.js +9 -9
- package/web/dist/app.webmanifest +4 -4
- package/web/dist/assets/LiveDeskApp-Djx1CsIp.js +178 -0
- package/web/dist/assets/{icons-CebEqIp7.js → icons-DwJmccVC.js} +1 -1
- package/web/dist/assets/{index-BPgqiKB-.js → index-DjIbOQxl.js} +6 -6
- package/web/dist/assets/{react-BHQWCi9c.js → react-DOZUPHF8.js} +1 -1
- package/web/dist/assets/supabase-C7qjtLN3.js +29 -0
- package/web/dist/desktop-auth-callback.html +4 -4
- package/web/dist/desktop-auth-callback.js +13 -13
- package/web/dist/index.html +11 -10
- package/web/dist/livedesk-build-evidence.json +30 -30
- package/web/dist/offline.html +2 -2
- package/web/dist/sw.js +11 -11
- package/web/dist/assets/LiveDeskApp-BnYbdi9v.js +0 -178
- package/web/dist/assets/supabase-C0Z0FS3K.js +0 -29
package/bin/livedesk.js
CHANGED
|
@@ -101,7 +101,7 @@ function prepareUpdateNeutralCwd(kind, operationId) {
|
|
|
101
101
|
if (unexpectedEntry) {
|
|
102
102
|
const shadowPath = join(neutralCwd, unexpectedEntry);
|
|
103
103
|
throw new Error(
|
|
104
|
-
`
|
|
104
|
+
`VuvoDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
return neutralCwd;
|
|
@@ -135,7 +135,7 @@ function restoreUpdateOriginalCwd() {
|
|
|
135
135
|
process.chdir(UPDATE_ORIGINAL_CWD);
|
|
136
136
|
} catch (error) {
|
|
137
137
|
throw new Error(
|
|
138
|
-
`
|
|
138
|
+
`VuvoDesk could not restore the pre-update working directory ${UPDATE_ORIGINAL_CWD}: `
|
|
139
139
|
+ `${error instanceof Error ? error.message : String(error)}`
|
|
140
140
|
);
|
|
141
141
|
}
|
|
@@ -190,12 +190,12 @@ async function assertNoActiveMacPhysicalDiagnosticLease() {
|
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
192
192
|
throw new Error(
|
|
193
|
-
`Mac physical diagnostic is running (pid ${lease.pid}); normal
|
|
193
|
+
`Mac physical diagnostic is running (pid ${lease.pid}); normal VuvoDesk startup was not started. `
|
|
194
194
|
+ 'Wait for its owned cleanup to finish or stop the diagnostic with Ctrl+C.'
|
|
195
195
|
);
|
|
196
196
|
}
|
|
197
197
|
throw new Error(
|
|
198
|
-
`Mac physical diagnostic ownership is unreadable at ${lease.path}; normal
|
|
198
|
+
`Mac physical diagnostic ownership is unreadable at ${lease.path}; normal VuvoDesk startup was not started `
|
|
199
199
|
+ 'to avoid overlapping a capture runtime.'
|
|
200
200
|
);
|
|
201
201
|
}
|
|
@@ -254,11 +254,11 @@ async function waitForHubRestartUpdateHostClaim(updateHost, jobId, expected) {
|
|
|
254
254
|
timeoutMs: Math.min(2_000, Math.max(250, claimDeadline - Date.now()))
|
|
255
255
|
});
|
|
256
256
|
if (!job) {
|
|
257
|
-
throw new Error(`
|
|
257
|
+
throw new Error(`VuvoDesk Update Host lost job ${jobId} before handoff.`);
|
|
258
258
|
}
|
|
259
259
|
if (['failed', 'cancelled'].includes(String(job.state || ''))) {
|
|
260
260
|
throw new Error(
|
|
261
|
-
`
|
|
261
|
+
`VuvoDesk Update Host job ${jobId} failed before handoff: ${job.error || `state=${job.state}`}`
|
|
262
262
|
);
|
|
263
263
|
}
|
|
264
264
|
workerPid = Number(job.workerPid || 0);
|
|
@@ -300,7 +300,7 @@ async function waitForHubRestartUpdateHostClaim(updateHost, jobId, expected) {
|
|
|
300
300
|
await delay(Math.max(50, HUB_UPDATE_SUPERVISOR_HANDSHAKE_POLL_MS));
|
|
301
301
|
}
|
|
302
302
|
throw new Error(
|
|
303
|
-
`
|
|
303
|
+
`VuvoDesk Update Host job ${jobId} did not publish an atomic handoff for `
|
|
304
304
|
+ `operation ${expected.operationId} within ${expected.timeoutMs}ms.`
|
|
305
305
|
);
|
|
306
306
|
}
|
|
@@ -339,7 +339,7 @@ function writeJsonAtomic(filePath, value) {
|
|
|
339
339
|
|
|
340
340
|
function printHelp() {
|
|
341
341
|
process.stdout.write(`
|
|
342
|
-
|
|
342
|
+
VuvoDesk
|
|
343
343
|
|
|
344
344
|
Usage:
|
|
345
345
|
npx -y --prefer-online livedesk@latest
|
|
@@ -364,10 +364,10 @@ Hub options:
|
|
|
364
364
|
--port <port> Hub HTTP port. Default: 5179
|
|
365
365
|
--remote-port <port>
|
|
366
366
|
Client connection port. Default: 5197.
|
|
367
|
-
--clean Proactively replace verified stale
|
|
367
|
+
--clean Proactively replace verified stale VuvoDesk owners on Hub ports.
|
|
368
368
|
--no-clean Do not attempt automatic port-conflict recovery.
|
|
369
369
|
--no-restart-existing
|
|
370
|
-
Keep an existing same-role
|
|
370
|
+
Keep an existing same-role VuvoDesk runtime and open its page.
|
|
371
371
|
|
|
372
372
|
Common:
|
|
373
373
|
--force-role <hub|client>
|
|
@@ -382,7 +382,7 @@ Common:
|
|
|
382
382
|
|
|
383
383
|
function printMacPhysicalDiagnosticHelp() {
|
|
384
384
|
process.stdout.write(`
|
|
385
|
-
|
|
385
|
+
VuvoDesk macOS physical diagnostic
|
|
386
386
|
|
|
387
387
|
Usage:
|
|
388
388
|
npx -y --prefer-online livedesk@latest diagnose mac-physical --confirm-active-motion
|
|
@@ -390,16 +390,16 @@ Usage:
|
|
|
390
390
|
|
|
391
391
|
Requirements:
|
|
392
392
|
macOS with two connected displays showing visibly different, continuously moving content.
|
|
393
|
-
Screen Recording permission for the terminal/
|
|
394
|
-
Accessibility permission for the terminal/
|
|
395
|
-
Stop every existing
|
|
393
|
+
Screen Recording permission for the terminal/VuvoDesk capture helper.
|
|
394
|
+
Accessibility permission for the terminal/VuvoDesk input helper.
|
|
395
|
+
Stop every existing VuvoDesk Hub, Client, and desktop runtime first.
|
|
396
396
|
--confirm-active-motion attests that motion is already running on both displays;
|
|
397
397
|
measured decoded-pixel change and native cadence must still prove it.
|
|
398
398
|
|
|
399
399
|
Behavior:
|
|
400
400
|
Starts one production unified Hub root and one production unified Client root
|
|
401
401
|
with isolated loopback state; the Client starts its normal RemoteFast child.
|
|
402
|
-
Refuses to overlap a running
|
|
402
|
+
Refuses to overlap a running VuvoDesk runtime or capture helper.
|
|
403
403
|
Stops only those exact launcher roots after their descendants drain.
|
|
404
404
|
Writes the default report under ~/.livedesk/diagnostics.
|
|
405
405
|
|
|
@@ -425,14 +425,14 @@ async function runDiagnosticCommand(args) {
|
|
|
425
425
|
'livedesk-transport-physical.mjs'
|
|
426
426
|
);
|
|
427
427
|
if (!existsSync(diagnosticEntry)) {
|
|
428
|
-
throw new Error(`
|
|
428
|
+
throw new Error(`VuvoDesk transport-physical diagnostic is missing from this package: ${diagnosticEntry}`);
|
|
429
429
|
}
|
|
430
430
|
const diagnostic = await import(pathToFileURL(diagnosticEntry).href);
|
|
431
431
|
await diagnostic.runTransportPhysicalDiagnosticCli(args.slice(1));
|
|
432
432
|
return;
|
|
433
433
|
}
|
|
434
434
|
if (name !== 'mac-physical') {
|
|
435
|
-
throw new Error(`Unknown
|
|
435
|
+
throw new Error(`Unknown VuvoDesk diagnostic: ${name}. Supported diagnostics: mac-physical, transport-physical.`);
|
|
436
436
|
}
|
|
437
437
|
if (args.slice(1).some(argument => ['--help', '-h', 'help'].includes(String(argument).toLowerCase()))) {
|
|
438
438
|
printMacPhysicalDiagnosticHelp();
|
|
@@ -444,21 +444,21 @@ async function runDiagnosticCommand(args) {
|
|
|
444
444
|
'livedesk-mode3-capture-smoke.mjs'
|
|
445
445
|
);
|
|
446
446
|
if (!existsSync(diagnosticEntry)) {
|
|
447
|
-
throw new Error(`
|
|
447
|
+
throw new Error(`VuvoDesk mac-physical diagnostic is missing from this package: ${diagnosticEntry}`);
|
|
448
448
|
}
|
|
449
449
|
await import(pathToFileURL(diagnosticEntry).href);
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
function printClientHelp() {
|
|
453
453
|
process.stdout.write(`
|
|
454
|
-
|
|
454
|
+
VuvoDesk Client
|
|
455
455
|
|
|
456
456
|
Usage:
|
|
457
457
|
npx -y --prefer-online livedesk@latest client
|
|
458
458
|
npx -y --prefer-online livedesk@latest client 3
|
|
459
459
|
|
|
460
460
|
Default flow:
|
|
461
|
-
Opens a
|
|
461
|
+
Opens a VuvoDesk connection page with Google sign-in or a 6-digit PIN.
|
|
462
462
|
Omit the number for first-available placement, or pass 1-999 to pin a slot.
|
|
463
463
|
On Windows, enable auto-start from the connection page checkbox.
|
|
464
464
|
|
|
@@ -665,7 +665,7 @@ async function waitForRoleTransitionParentExit(pid, timeoutMs = 120_000) {
|
|
|
665
665
|
if (!alive) return;
|
|
666
666
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
667
667
|
}
|
|
668
|
-
throw new Error(`Previous
|
|
668
|
+
throw new Error(`Previous VuvoDesk runtime ${processId} did not exit before the role transition deadline.`);
|
|
669
669
|
}
|
|
670
670
|
|
|
671
671
|
async function isManagerAlreadyRunning(url = DEFAULT_MANAGER_URL) {
|
|
@@ -743,7 +743,7 @@ async function probeExistingRuntimeIdentity(existing) {
|
|
|
743
743
|
};
|
|
744
744
|
}
|
|
745
745
|
} catch {
|
|
746
|
-
// A non-
|
|
746
|
+
// A non-VuvoDesk, stopping, or unreachable owner is not runtime proof.
|
|
747
747
|
} finally {
|
|
748
748
|
clearTimeout(timer);
|
|
749
749
|
}
|
|
@@ -904,7 +904,7 @@ async function waitForProcessExit(pid, timeoutMs = EXISTING_RUNTIME_STOP_TIMEOUT
|
|
|
904
904
|
if (!alive) return;
|
|
905
905
|
await waitMilliseconds(Math.min(PORT_CLEANUP_POLL_MS, Math.max(1, deadline - Date.now())));
|
|
906
906
|
}
|
|
907
|
-
throw new Error(`Existing
|
|
907
|
+
throw new Error(`Existing VuvoDesk runtime ${processId} did not exit before the restart deadline.`);
|
|
908
908
|
}
|
|
909
909
|
|
|
910
910
|
function normalizeExactWindowsProcessRecord(value, depth = 0) {
|
|
@@ -958,7 +958,7 @@ async function queryExactWindowsProcessSnapshot() {
|
|
|
958
958
|
);
|
|
959
959
|
if (!result.ok || !result.stdout) {
|
|
960
960
|
throw new Error(
|
|
961
|
-
'
|
|
961
|
+
'VuvoDesk could not establish an immutable Windows process snapshot: '
|
|
962
962
|
+ `${result.stderr || result.error?.message || 'CIM query failed'}`
|
|
963
963
|
);
|
|
964
964
|
}
|
|
@@ -966,7 +966,7 @@ async function queryExactWindowsProcessSnapshot() {
|
|
|
966
966
|
try {
|
|
967
967
|
parsed = JSON.parse(result.stdout);
|
|
968
968
|
} catch (error) {
|
|
969
|
-
throw new Error(`
|
|
969
|
+
throw new Error(`VuvoDesk could not parse the immutable Windows process snapshot: ${error.message}`);
|
|
970
970
|
}
|
|
971
971
|
const records = parsed?.Records == null
|
|
972
972
|
? []
|
|
@@ -977,7 +977,7 @@ async function queryExactWindowsProcessSnapshot() {
|
|
|
977
977
|
async function captureExactWindowsProcessTree(rootDetails, { rootExitedAtMs = null } = {}) {
|
|
978
978
|
const rootRecord = normalizeExactWindowsProcessRecord(rootDetails, 0);
|
|
979
979
|
if (!rootRecord) {
|
|
980
|
-
throw new Error('
|
|
980
|
+
throw new Error('VuvoDesk could not establish the launcher CreationDate identity; it was preserved.');
|
|
981
981
|
}
|
|
982
982
|
const snapshot = await queryExactWindowsProcessSnapshot();
|
|
983
983
|
const currentByPid = new Map(snapshot.map(record => [record.pid, record]));
|
|
@@ -1056,7 +1056,7 @@ async function exactReclaimedWindowsRecordsStillRunning(records) {
|
|
|
1056
1056
|
);
|
|
1057
1057
|
if (!result.ok || !result.stdout) {
|
|
1058
1058
|
throw new Error(
|
|
1059
|
-
'
|
|
1059
|
+
'VuvoDesk could not revalidate the reclaimed Windows process identities: '
|
|
1060
1060
|
+ `${result.stderr || result.error?.message || 'targeted CIM query failed'}`
|
|
1061
1061
|
);
|
|
1062
1062
|
}
|
|
@@ -1064,7 +1064,7 @@ async function exactReclaimedWindowsRecordsStillRunning(records) {
|
|
|
1064
1064
|
try {
|
|
1065
1065
|
parsed = JSON.parse(result.stdout);
|
|
1066
1066
|
} catch (error) {
|
|
1067
|
-
throw new Error(`
|
|
1067
|
+
throw new Error(`VuvoDesk could not parse reclaimed Windows process identities: ${error.message}`);
|
|
1068
1068
|
}
|
|
1069
1069
|
const values = parsed?.Records == null
|
|
1070
1070
|
? []
|
|
@@ -1094,7 +1094,7 @@ async function terminateExactWindowsProcesses(recordOrRecords) {
|
|
|
1094
1094
|
.map(record => normalizeExactWindowsProcessRecord(record, record?.depth))
|
|
1095
1095
|
.filter(Boolean);
|
|
1096
1096
|
if (targets.length === 0) {
|
|
1097
|
-
throw new Error('
|
|
1097
|
+
throw new Error('VuvoDesk refused to terminate Windows processes without immutable CreationDates.');
|
|
1098
1098
|
}
|
|
1099
1099
|
const targetsJson = JSON.stringify(targets.map(target => ({
|
|
1100
1100
|
pid: target.pid,
|
|
@@ -1175,7 +1175,7 @@ async function drainReclaimedWindowsClientManifest(reclaimedOwner, {
|
|
|
1175
1175
|
if (inheritedManifest.missing === true) {
|
|
1176
1176
|
if (process.env.LIVEDESK_DEBUG === '1') {
|
|
1177
1177
|
console.log(
|
|
1178
|
-
`[
|
|
1178
|
+
`[VuvoDesk] Reclaimed Windows Client generation ${exactOwner.pid} `
|
|
1179
1179
|
+ 'has no exact process manifest; continuing legacy cleanup.'
|
|
1180
1180
|
);
|
|
1181
1181
|
}
|
|
@@ -1219,7 +1219,7 @@ async function drainReclaimedWindowsClientManifest(reclaimedOwner, {
|
|
|
1219
1219
|
}
|
|
1220
1220
|
if (process.env.LIVEDESK_DEBUG === '1') {
|
|
1221
1221
|
console.log(
|
|
1222
|
-
`[
|
|
1222
|
+
`[VuvoDesk] Reclaimed Windows Client generation ${exactOwner.pid} `
|
|
1223
1223
|
+ `drained ${tracked.length} exact process record(s) and consumed its manifest.`
|
|
1224
1224
|
);
|
|
1225
1225
|
}
|
|
@@ -1246,7 +1246,7 @@ async function drainReclaimedWindowsClientManifest(reclaimedOwner, {
|
|
|
1246
1246
|
}
|
|
1247
1247
|
|
|
1248
1248
|
async function terminateExactWindowsProcessTree(rootDetails, {
|
|
1249
|
-
label = '
|
|
1249
|
+
label = 'VuvoDesk process tree',
|
|
1250
1250
|
maxAttempts = 4,
|
|
1251
1251
|
additionalRecords = []
|
|
1252
1252
|
} = {}) {
|
|
@@ -1314,7 +1314,7 @@ async function terminateExactWindowsProcessTree(rootDetails, {
|
|
|
1314
1314
|
async function stopExistingRuntime(existing) {
|
|
1315
1315
|
const processId = Number(existing?.pid);
|
|
1316
1316
|
if (!Number.isInteger(processId) || processId <= 1) {
|
|
1317
|
-
throw new Error('Existing
|
|
1317
|
+
throw new Error('Existing VuvoDesk runtime has an invalid process id; it was preserved.');
|
|
1318
1318
|
}
|
|
1319
1319
|
const storedOwnerToken = String(existing?.ownerToken || '').trim();
|
|
1320
1320
|
const storedOwnerInstanceMarker = String(existing?.ownerInstanceMarker || '').trim();
|
|
@@ -1334,7 +1334,7 @@ async function stopExistingRuntime(existing) {
|
|
|
1334
1334
|
}
|
|
1335
1335
|
: null;
|
|
1336
1336
|
if (processId === process.pid) {
|
|
1337
|
-
throw new Error('Existing
|
|
1337
|
+
throw new Error('Existing VuvoDesk runtime resolves to this launcher without an exact stale-owner proof; it was preserved.');
|
|
1338
1338
|
}
|
|
1339
1339
|
|
|
1340
1340
|
// The mailbox is non-destructive and authenticates the current lock token.
|
|
@@ -1369,7 +1369,7 @@ async function stopExistingRuntime(existing) {
|
|
|
1369
1369
|
const cooperativeStop = await cooperativeStopPromise;
|
|
1370
1370
|
if (cooperativeStop.exited) {
|
|
1371
1371
|
console.log(
|
|
1372
|
-
`[
|
|
1372
|
+
`[VuvoDesk] Existing ${existing?.role || 'runtime'} launcher pid ${processId} `
|
|
1373
1373
|
+ 'accepted cooperative cleanup and exited before replacement (identity=exact-owner-mailbox).'
|
|
1374
1374
|
);
|
|
1375
1375
|
return { runtimeProof: null, staleOwnerProof: null };
|
|
@@ -1390,7 +1390,7 @@ async function stopExistingRuntime(existing) {
|
|
|
1390
1390
|
&& (processInstanceMarker(fallbackDetails) !== processInstanceMarker(details)
|
|
1391
1391
|
|| String(fallbackDetails.startOrder || '') !== String(details.startOrder || ''))) {
|
|
1392
1392
|
console.warn(
|
|
1393
|
-
`[
|
|
1393
|
+
`[VuvoDesk] Launcher pid ${processId} changed identity while cooperative cleanup was pending; `
|
|
1394
1394
|
+ 'the reused process was preserved.'
|
|
1395
1395
|
);
|
|
1396
1396
|
if (exactStaleOwnerProof) {
|
|
@@ -1404,7 +1404,7 @@ async function stopExistingRuntime(existing) {
|
|
|
1404
1404
|
&& currentOwnerStartOrder
|
|
1405
1405
|
&& storedOwnerStartOrder !== currentOwnerStartOrder) {
|
|
1406
1406
|
console.warn(
|
|
1407
|
-
`[
|
|
1407
|
+
`[VuvoDesk] Runtime lock pid ${processId} was reused by a different process instance; `
|
|
1408
1408
|
+ 'the current process was preserved and only the exact stale lock may be reclaimed.'
|
|
1409
1409
|
);
|
|
1410
1410
|
return { runtimeProof: null, staleOwnerProof: exactStaleOwnerProof };
|
|
@@ -1429,7 +1429,7 @@ async function stopExistingRuntime(existing) {
|
|
|
1429
1429
|
&& !legacyClientTreeConfirmed
|
|
1430
1430
|
&& !runtimeProof
|
|
1431
1431
|
&& !cooperativeOwnerConfirmed) {
|
|
1432
|
-
throw new Error(`Existing runtime pid ${processId} is not a confirmed
|
|
1432
|
+
throw new Error(`Existing runtime pid ${processId} is not a confirmed VuvoDesk launcher; it was preserved.`);
|
|
1433
1433
|
}
|
|
1434
1434
|
|
|
1435
1435
|
const identityProof = cooperativeOwnerConfirmed
|
|
@@ -1459,7 +1459,7 @@ async function stopExistingRuntime(existing) {
|
|
|
1459
1459
|
|| currentLock.ownerInstanceMarker !== storedOwnerInstanceMarker
|
|
1460
1460
|
|| currentLock.ownerStartOrder !== storedOwnerStartOrder) {
|
|
1461
1461
|
throw new Error(
|
|
1462
|
-
`
|
|
1462
|
+
`VuvoDesk runtime-lock ownership changed before fallback for pid ${processId}; `
|
|
1463
1463
|
+ 'the current process and newer lock generation were preserved.'
|
|
1464
1464
|
);
|
|
1465
1465
|
}
|
|
@@ -1488,16 +1488,16 @@ async function stopExistingRuntime(existing) {
|
|
|
1488
1488
|
? `ack=${cooperativeStop.ackStatus || 'accepted'}`
|
|
1489
1489
|
: 'no-ack';
|
|
1490
1490
|
console.warn(
|
|
1491
|
-
`[
|
|
1491
|
+
`[VuvoDesk] Existing ${existing?.role || 'runtime'} launcher pid ${processId} `
|
|
1492
1492
|
+ `did not finish cooperative cleanup within its ${cooperativeTimeoutMs / 1000}-second `
|
|
1493
1493
|
+ `maximum window (${status}); `
|
|
1494
1494
|
+ 'continuing with exact process-identity fallback.'
|
|
1495
1495
|
);
|
|
1496
1496
|
}
|
|
1497
|
-
console.log(`[
|
|
1497
|
+
console.log(`[VuvoDesk] Stopping existing ${existing?.role || 'runtime'} launcher pid ${processId} before starting a fresh runtime (identity=${identityProof}).`);
|
|
1498
1498
|
if (os.platform() === 'win32') {
|
|
1499
1499
|
await terminateExactWindowsProcessTree(details, {
|
|
1500
|
-
label: `existing
|
|
1500
|
+
label: `existing VuvoDesk ${existing?.role || 'runtime'} launcher pid=${processId}`,
|
|
1501
1501
|
additionalRecords: inheritedWindowsProcessRecords
|
|
1502
1502
|
});
|
|
1503
1503
|
} else {
|
|
@@ -1508,7 +1508,7 @@ async function stopExistingRuntime(existing) {
|
|
|
1508
1508
|
await waitForProcessExit(processId);
|
|
1509
1509
|
} catch (error) {
|
|
1510
1510
|
console.warn(
|
|
1511
|
-
`[
|
|
1511
|
+
`[VuvoDesk] Confirmed launcher pid ${processId} did not finish graceful Client cleanup; `
|
|
1512
1512
|
+ 'force-stopping that launcher only. Verified orphan agents/helpers will be audited before restart.'
|
|
1513
1513
|
);
|
|
1514
1514
|
try { process.kill(processId, 'SIGKILL'); } catch (killError) {
|
|
@@ -1543,7 +1543,7 @@ function startOwnedRuntimeShutdownWatcher(runtimeOwner, onShutdown) {
|
|
|
1543
1543
|
if (!exactOwnerIsComplete) {
|
|
1544
1544
|
if (runtimeOwner?.ownerToken) {
|
|
1545
1545
|
console.warn(
|
|
1546
|
-
'[
|
|
1546
|
+
'[VuvoDesk] Cooperative shutdown watcher is unavailable because the launcher '
|
|
1547
1547
|
+ 'could not establish a complete process-start identity. Exact fallback remains enabled.'
|
|
1548
1548
|
);
|
|
1549
1549
|
}
|
|
@@ -1632,7 +1632,7 @@ function assertPortCleanupReady(finalRecords, portStates) {
|
|
|
1632
1632
|
.map(formatPortOwner);
|
|
1633
1633
|
return `TCP ${port.Port}[available=${port.PortAvailable === true},bindable=${port.PortBindable === true},bind-error=${port.BindError || 'none'},owners=${owners.length ? owners.join('; ') : 'owner not visible'}`;
|
|
1634
1634
|
}).join('], ');
|
|
1635
|
-
throw new Error(`
|
|
1635
|
+
throw new Error(`VuvoDesk Hub startup cleanup could not release the configured port(s): ${summary}]. Unrelated processes are preserved; stop the owning process manually or choose an explicit --port/--remote-port value.`);
|
|
1636
1636
|
}
|
|
1637
1637
|
|
|
1638
1638
|
async function collectWindowsPortRecords(ports) {
|
|
@@ -1680,7 +1680,7 @@ async function collectWindowsPortRecords(ports) {
|
|
|
1680
1680
|
);
|
|
1681
1681
|
if (!result.ok && !result.stdout) {
|
|
1682
1682
|
throw new Error(
|
|
1683
|
-
`Could not inspect existing
|
|
1683
|
+
`Could not inspect existing VuvoDesk Windows ports: `
|
|
1684
1684
|
+ `${result.stderr || result.error?.message || 'PowerShell failed'}`
|
|
1685
1685
|
);
|
|
1686
1686
|
}
|
|
@@ -1689,7 +1689,7 @@ async function collectWindowsPortRecords(ports) {
|
|
|
1689
1689
|
try {
|
|
1690
1690
|
parsed = JSON.parse(result.stdout);
|
|
1691
1691
|
} catch {
|
|
1692
|
-
throw new Error(`Could not parse existing
|
|
1692
|
+
throw new Error(`Could not parse existing VuvoDesk Windows port state: ${result.stdout}`);
|
|
1693
1693
|
}
|
|
1694
1694
|
const records = parsed?.Records == null
|
|
1695
1695
|
? []
|
|
@@ -1834,7 +1834,7 @@ async function terminateExactWindowsPortOwners(records) {
|
|
|
1834
1834
|
);
|
|
1835
1835
|
if (!result.ok) {
|
|
1836
1836
|
throw new Error(
|
|
1837
|
-
`Could not stop exact
|
|
1837
|
+
`Could not stop exact VuvoDesk Windows port owner tree: `
|
|
1838
1838
|
+ `${result.stderr || result.error?.message || 'PowerShell failed'}`
|
|
1839
1839
|
);
|
|
1840
1840
|
}
|
|
@@ -1874,7 +1874,7 @@ async function stopWindowsProcessesOnPorts(ports) {
|
|
|
1874
1874
|
const summary = [...stoppedOwners.values()]
|
|
1875
1875
|
.map(record => `${record.Pid}:${record.ProcessName || 'node'}`)
|
|
1876
1876
|
.join(', ');
|
|
1877
|
-
console.log(`Restarting
|
|
1877
|
+
console.log(`Restarting VuvoDesk Hub. Stopped stale VuvoDesk owner(s) once: ${summary}`);
|
|
1878
1878
|
}
|
|
1879
1879
|
assertPortCleanupReady(finalRecords, portStates);
|
|
1880
1880
|
}
|
|
@@ -1950,7 +1950,7 @@ async function stopUnixProcessesOnPorts(ports) {
|
|
|
1950
1950
|
if (!stoppedOwners.has(String(record.Pid))) stoppedOwners.set(String(record.Pid), record);
|
|
1951
1951
|
}
|
|
1952
1952
|
const summary = [...stoppedOwners.values()].map(record => `${record.Pid}:${record.ProcessName || 'node'}`).join(', ');
|
|
1953
|
-
console.log(`Restarting
|
|
1953
|
+
console.log(`Restarting VuvoDesk Hub. Stopped stale VuvoDesk owner(s) once: ${summary}`);
|
|
1954
1954
|
}
|
|
1955
1955
|
assertPortCleanupReady(finalRecords, portStates);
|
|
1956
1956
|
}
|
|
@@ -1993,7 +1993,7 @@ async function stopStaleClientAgents() {
|
|
|
1993
1993
|
);
|
|
1994
1994
|
if (!result.ok && !result.stdout) {
|
|
1995
1995
|
throw new Error(
|
|
1996
|
-
`
|
|
1996
|
+
`VuvoDesk Client lifecycle audit could not enumerate Windows Agent processes: `
|
|
1997
1997
|
+ `${result.stderr || result.error?.message || 'CIM query failed'}`
|
|
1998
1998
|
);
|
|
1999
1999
|
}
|
|
@@ -2002,7 +2002,7 @@ async function stopStaleClientAgents() {
|
|
|
2002
2002
|
|
|
2003
2003
|
// One complete ps snapshot avoids treating pgrep execution errors as
|
|
2004
2004
|
// "nothing is running". The strict identity classifier below decides
|
|
2005
|
-
// whether each textual candidate is really a packaged
|
|
2005
|
+
// whether each textual candidate is really a packaged VuvoDesk process.
|
|
2006
2006
|
const result = await runQuiet(
|
|
2007
2007
|
'ps',
|
|
2008
2008
|
['-axo', 'pid=,args='],
|
|
@@ -2010,7 +2010,7 @@ async function stopStaleClientAgents() {
|
|
|
2010
2010
|
);
|
|
2011
2011
|
if (!result.ok && !result.stdout) {
|
|
2012
2012
|
throw new Error(
|
|
2013
|
-
`
|
|
2013
|
+
`VuvoDesk Client lifecycle audit could not enumerate Unix processes: `
|
|
2014
2014
|
+ `${result.stderr || result.error?.message || 'ps failed'}`
|
|
2015
2015
|
);
|
|
2016
2016
|
}
|
|
@@ -2039,7 +2039,7 @@ async function stopStaleClientAgents() {
|
|
|
2039
2039
|
if (!details) {
|
|
2040
2040
|
if (isPidAlive(pid)) {
|
|
2041
2041
|
throw new Error(
|
|
2042
|
-
`
|
|
2042
|
+
`VuvoDesk Client lifecycle audit could not establish the identity of candidate pid=${pid}; `
|
|
2043
2043
|
+ 'startup was preserved instead of signaling an unknown process.'
|
|
2044
2044
|
);
|
|
2045
2045
|
}
|
|
@@ -2051,7 +2051,7 @@ async function stopStaleClientAgents() {
|
|
|
2051
2051
|
const instanceMarker = processInstanceMarker(details);
|
|
2052
2052
|
if (!instanceMarker) {
|
|
2053
2053
|
throw new Error(
|
|
2054
|
-
`
|
|
2054
|
+
`VuvoDesk Client lifecycle audit could not establish a start marker for verified ${type} pid=${pid}.`
|
|
2055
2055
|
);
|
|
2056
2056
|
}
|
|
2057
2057
|
found.push({
|
|
@@ -2189,7 +2189,7 @@ async function stopStaleClientAgents() {
|
|
|
2189
2189
|
const remainingProcesses = await findStaleProcesses();
|
|
2190
2190
|
if (remainingProcesses.length > 0) {
|
|
2191
2191
|
throw new Error(
|
|
2192
|
-
'
|
|
2192
|
+
'VuvoDesk Client startup was stopped because new stale capture processes appeared during cleanup: '
|
|
2193
2193
|
+ remainingProcesses.map(record => `${record.type} pid=${record.pid}`).join('; ')
|
|
2194
2194
|
);
|
|
2195
2195
|
}
|
|
@@ -2197,14 +2197,14 @@ async function stopStaleClientAgents() {
|
|
|
2197
2197
|
const staleProcesses = [...stoppedProcesses.values()];
|
|
2198
2198
|
if (staleProcesses.length === 0) {
|
|
2199
2199
|
if (process.env.LIVEDESK_DEBUG === '1') {
|
|
2200
|
-
console.log('[
|
|
2200
|
+
console.log('[VuvoDesk] Client lifecycle audit: verified stale agents=0 capture-helpers=0.');
|
|
2201
2201
|
}
|
|
2202
2202
|
return;
|
|
2203
2203
|
}
|
|
2204
2204
|
const agentPids = staleProcesses.filter(item => item.type === 'agent').map(item => item.pid);
|
|
2205
2205
|
const helperPids = staleProcesses.filter(item => item.type === 'capture-helper').map(item => item.pid);
|
|
2206
2206
|
console.log(
|
|
2207
|
-
`[
|
|
2207
|
+
`[VuvoDesk] Client lifecycle audit: verified stale agents=${agentPids.length} `
|
|
2208
2208
|
+ `capture-helpers=${helperPids.length}; confirmed stopped pids=${staleProcesses.map(item => item.pid).join(', ')}.`
|
|
2209
2209
|
);
|
|
2210
2210
|
}
|
|
@@ -2320,7 +2320,7 @@ function buildHubRestartBootstrapScript() {
|
|
|
2320
2320
|
'const captureExactWindowsTrees = (identities, snapshot) => { const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const childrenByParent = new Map(); for (const item of snapshot) { if (!childrenByParent.has(item.parentPid)) childrenByParent.set(item.parentPid, []); childrenByParent.get(item.parentPid).push(item); } const tracked = new Map(); for (const root of identities) { const currentRoot = currentByPid.get(Number(root?.pid || 0)); if (!sameWindowsIdentity(currentRoot, root)) continue; const queue = [{ record: currentRoot, depth: 0 }]; const seen = new Set(); while (queue.length > 0) { const current = queue.shift(); if (seen.has(current.record.pid)) continue; seen.add(current.record.pid); const key = `${current.record.pid}:${current.record.startOrder}`; const previous = tracked.get(key); if (!previous || current.depth > previous.depth) tracked.set(key, { ...current.record, depth: current.depth }); for (const child of childrenByParent.get(current.record.pid) || []) { try { if (BigInt(child.startOrder) < BigInt(current.record.startOrder)) continue; } catch { continue; } queue.push({ record: child, depth: current.depth + 1 }); } } } return [...tracked.values()].sort((left, right) => Number(right.depth || 0) - Number(left.depth || 0)); };',
|
|
2321
2321
|
'const stopExactWindowsRecords = async records => { if (!Array.isArray(records) || records.length === 0) return; const encoded = Buffer.from(JSON.stringify(records.map(record => ({ pid: record.pid, startOrder: record.startOrder }))), "utf8").toString("base64"); const script = `$ErrorActionPreference = "Stop"; $targetsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${encoded}")); $targets = ConvertFrom-Json -InputObject $targetsJson; $failures = [System.Collections.Generic.List[string]]::new(); foreach ($item in $targets) { $targetPid = [int]$item.pid; $expectedStartTicks = [string]$item.startOrder; try { $target = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1; if (-not $target) { continue }; $targetStartTicks = if ($target.CreationDate) { [string]$target.CreationDate.ToUniversalTime().Ticks } else { "" }; if ($targetStartTicks -ne $expectedStartTicks) { continue }; Stop-Process -Id $targetPid -Force -ErrorAction Stop } catch { $failures.Add("pid=$targetPid $($_.Exception.Message)") } }; if ($failures.Count -gt 0) { [Console]::Error.WriteLine(($failures -join "; ")); exit 1 }`; await runWindowsPowerShell(script); };',
|
|
2322
2322
|
'const terminateExactWindowsOwnedTrees = async (identities, { skipStop = false } = {}) => { const roots = (Array.isArray(identities) ? identities : []).filter(identity => Number(identity?.pid || 0) > 1 && /^\\d+$/.test(String(identity?.startOrder || ""))); if (roots.length === 0) throw new Error("No immutable Windows replacement identity is available"); const snapshot = await queryWindowsProcessTable(); const targets = captureExactWindowsTrees(roots, snapshot); if (!skipStop) await stopExactWindowsRecords(targets); const finalSnapshot = await queryWindowsProcessTable(); const currentByPid = new Map(finalSnapshot.map(item => [item.pid, item])); const remaining = targets.filter(target => sameWindowsIdentity(currentByPid.get(target.pid), target)); if (remaining.length > 0) throw new Error(`${skipStop ? "simulated exact stop no-op; " : ""}exact pid(s) still alive: ${remaining.map(item => item.pid).join(", ")}`); };',
|
|
2323
|
-
'const waitForExit = async (pid, timeoutMs) => { const deadline = Date.now() + timeoutMs; while (pid > 1 && Date.now() < deadline) { if (!isAlive(pid)) return; await sleep(250); } if (pid > 1 && isAlive(pid)) throw new Error(`Previous
|
|
2323
|
+
'const waitForExit = async (pid, timeoutMs) => { const deadline = Date.now() + timeoutMs; while (pid > 1 && Date.now() < deadline) { if (!isAlive(pid)) return; await sleep(250); } if (pid > 1 && isAlive(pid)) throw new Error(`Previous VuvoDesk Hub launcher pid=${pid} did not exit within ${timeoutMs}ms.`); };',
|
|
2324
2324
|
'const decodeArgs = name => JSON.parse(Buffer.from(process.env[name] || "", "base64").toString("utf8"));',
|
|
2325
2325
|
'const primary = { command: process.env.LIVEDESK_RESTART_COMMAND || "", args: decodeArgs("LIVEDESK_RESTART_ARGS_BASE64") };',
|
|
2326
2326
|
'const fallback = { command: process.env.LIVEDESK_RESTART_FALLBACK_COMMAND || "", args: decodeArgs("LIVEDESK_RESTART_FALLBACK_ARGS_BASE64") };',
|
|
@@ -2336,7 +2336,7 @@ function buildHubRestartBootstrapScript() {
|
|
|
2336
2336
|
'const healthTimeoutMs = terminationFailureOperationId === operationId ? numberFromEnv("LIVEDESK_TEST_HUB_RESTART_TERMINATION_HEALTH_TIMEOUT_MS", 1000, 250) : configuredHealthTimeoutMs;',
|
|
2337
2337
|
'const pollMs = numberFromEnv("LIVEDESK_RESTART_HEALTH_POLL_MS", 250, 50);',
|
|
2338
2338
|
'const stabilityMs = numberFromEnv("LIVEDESK_RESTART_STABILITY_MS", 5000, 5000);',
|
|
2339
|
-
'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("
|
|
2339
|
+
'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("VuvoDesk Hub update neutral working directory is unavailable"); mkdirSync(neutralCwd, { recursive: true }); const unexpectedEntry = readdirSync(neutralCwd)[0]; if (unexpectedEntry) { const shadow = join(neutralCwd, unexpectedEntry); throw new Error(`VuvoDesk Hub update neutral working directory is shadowed by ${shadow}: ${neutralCwd}`); } };',
|
|
2340
2340
|
'const buildLaunchEnv = () => { const env = { ...process.env }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_workspaces = "false"; env.npm_config_include_workspace_root = "false"; return env; };',
|
|
2341
2341
|
'const requestJson = async path => { const response = await fetch(new URL(path, healthBaseUrl), { cache: "no-store", signal: AbortSignal.timeout(Math.min(2000, healthTimeoutMs)) }); if (!response.ok) throw new Error(`HTTP ${response.status} from ${path}`); return response.json(); };',
|
|
2342
2342
|
'const probeRemotePort = () => new Promise((resolve, reject) => { const socket = net.createConnection({ host: remoteHost, port: remotePort }); const timer = setTimeout(() => socket.destroy(new Error("TCP probe timed out")), Math.min(1500, healthTimeoutMs)); timer.unref?.(); socket.once("connect", () => { clearTimeout(timer); socket.destroy(); resolve(); }); socket.once("error", error => { clearTimeout(timer); reject(error); }); });',
|
|
@@ -2447,7 +2447,7 @@ function buildHubRestartBootstrapScript() {
|
|
|
2447
2447
|
' await terminateTree(child, lastRuntimePid);',
|
|
2448
2448
|
' throw new Error(`${label} attempt=${attempt} did not remain healthy for ${stabilityMs}ms: ${lastError?.message || (exit ? `command exited code=${exit.code ?? "none"} signal=${exit.signal || "none"}` : "health deadline expired")}`);',
|
|
2449
2449
|
'};',
|
|
2450
|
-
'(async () => { const targetErrors = []; const fallbackErrors = []; if (exitBeforeHandshakeOperationId === operationId) { log("stage=test-exit-before-handshake"); process.exitCode = 86; return; } if (delayHandshakeOperationId === operationId) { const claimDelayMs = numberFromEnv("LIVEDESK_TEST_HUB_RESTART_HANDSHAKE_DELAY_MS", 2000, 1); log(`stage=test-delay-before-handshake delayMs=${claimDelayMs}`); await sleep(claimDelayMs); } writeResult("waiting-for-old-launcher", { outcome: "in-progress" }); log(`stage=waiting-for-old-launcher pid=${waitPid} supervisorPid=${process.pid}`); if (exitAfterHandshakeOperationId === operationId) { log("stage=test-exit-after-handshake"); process.exitCode = 87; return; } await waitForExit(waitPid, 60000); writeResult("old-launcher-exited", { outcome: "in-progress" }); log("stage=old-launcher-exited"); for (let attempt = 1; attempt <= attemptLimit; attempt += 1) { writeResult("target-attempt", { outcome: "in-progress", attempt }); try { const status = await launchAndVerify(primary, "target", expectedVersion, attempt); writeResult("updated", { outcome: "updated", attempt, runtimePid: status.runtimePid, launcherPid: status.launcherPid, appVersion: status.appVersion, completedAt: new Date().toISOString(), error: undefined }); log("stage=completed"); return; } catch (error) { if (["LIVEDESK_HUB_UPDATE_SUPERSEDED", "LIVEDESK_HUB_RESTART_TERMINATION_FAILED"].includes(error?.code)) throw error; targetErrors.push(error.message); writeResult("target-failed", { outcome: "in-progress", attempt, error: error.message }); log(`target attempt=${attempt} stage=failed error=${error.message}`); } } log(`stage=fallback-start target=${fallbackVersion || "current"}`); for (let attempt = 1; attempt <= 2; attempt += 1) { writeResult("fallback-attempt", { outcome: "in-progress", attempt, error: targetErrors.join(" | ") }); try { const status = await launchAndVerify(fallback, "fallback", fallbackVersion, attempt); writeResult("restored", { outcome: "restored", attempt, runtimePid: status.runtimePid, launcherPid: status.launcherPid, appVersion: status.appVersion, completedAt: new Date().toISOString(), error: `Target Hub ${expectedVersion} failed verification: ${targetErrors.join(" | ")}` }); log("stage=completed-with-fallback"); return; } catch (error) { if (["LIVEDESK_HUB_UPDATE_SUPERSEDED", "LIVEDESK_HUB_RESTART_TERMINATION_FAILED"].includes(error?.code)) throw error; fallbackErrors.push(error.message); writeResult("fallback-failed", { outcome: "in-progress", attempt, error: [...targetErrors, ...fallbackErrors].join(" | ") }); log(`fallback attempt=${attempt} stage=failed error=${error.message}`); } } throw new Error(`No verified
|
|
2450
|
+
'(async () => { const targetErrors = []; const fallbackErrors = []; if (exitBeforeHandshakeOperationId === operationId) { log("stage=test-exit-before-handshake"); process.exitCode = 86; return; } if (delayHandshakeOperationId === operationId) { const claimDelayMs = numberFromEnv("LIVEDESK_TEST_HUB_RESTART_HANDSHAKE_DELAY_MS", 2000, 1); log(`stage=test-delay-before-handshake delayMs=${claimDelayMs}`); await sleep(claimDelayMs); } writeResult("waiting-for-old-launcher", { outcome: "in-progress" }); log(`stage=waiting-for-old-launcher pid=${waitPid} supervisorPid=${process.pid}`); if (exitAfterHandshakeOperationId === operationId) { log("stage=test-exit-after-handshake"); process.exitCode = 87; return; } await waitForExit(waitPid, 60000); writeResult("old-launcher-exited", { outcome: "in-progress" }); log("stage=old-launcher-exited"); for (let attempt = 1; attempt <= attemptLimit; attempt += 1) { writeResult("target-attempt", { outcome: "in-progress", attempt }); try { const status = await launchAndVerify(primary, "target", expectedVersion, attempt); writeResult("updated", { outcome: "updated", attempt, runtimePid: status.runtimePid, launcherPid: status.launcherPid, appVersion: status.appVersion, completedAt: new Date().toISOString(), error: undefined }); log("stage=completed"); return; } catch (error) { if (["LIVEDESK_HUB_UPDATE_SUPERSEDED", "LIVEDESK_HUB_RESTART_TERMINATION_FAILED"].includes(error?.code)) throw error; targetErrors.push(error.message); writeResult("target-failed", { outcome: "in-progress", attempt, error: error.message }); log(`target attempt=${attempt} stage=failed error=${error.message}`); } } log(`stage=fallback-start target=${fallbackVersion || "current"}`); for (let attempt = 1; attempt <= 2; attempt += 1) { writeResult("fallback-attempt", { outcome: "in-progress", attempt, error: targetErrors.join(" | ") }); try { const status = await launchAndVerify(fallback, "fallback", fallbackVersion, attempt); writeResult("restored", { outcome: "restored", attempt, runtimePid: status.runtimePid, launcherPid: status.launcherPid, appVersion: status.appVersion, completedAt: new Date().toISOString(), error: `Target Hub ${expectedVersion} failed verification: ${targetErrors.join(" | ")}` }); log("stage=completed-with-fallback"); return; } catch (error) { if (["LIVEDESK_HUB_UPDATE_SUPERSEDED", "LIVEDESK_HUB_RESTART_TERMINATION_FAILED"].includes(error?.code)) throw error; fallbackErrors.push(error.message); writeResult("fallback-failed", { outcome: "in-progress", attempt, error: [...targetErrors, ...fallbackErrors].join(" | ") }); log(`fallback attempt=${attempt} stage=failed error=${error.message}`); } } throw new Error(`No verified VuvoDesk Hub replacement could be started. Target failures: ${targetErrors.join(" | ") || "none"}. Fallback failures: ${fallbackErrors.join(" | ") || "none"}. Inspect ${logPath || "the Hub restart log"} and start the frozen local VuvoDesk Hub manually.`); })().catch(error => { if (error?.code === "LIVEDESK_HUB_UPDATE_SUPERSEDED") { log("stage=bootstrap-superseded error=" + error.message); return; } try { writeResult("failed", { outcome: "failed", completedAt: new Date().toISOString(), error: error.message }); } catch (writeError) { log("stage=result-write-failed resultStage=failed error=" + writeError.message); } log("stage=bootstrap-failed error=" + error.message); process.exitCode = 1; });',
|
|
2451
2451
|
''
|
|
2452
2452
|
].join('\n');
|
|
2453
2453
|
}
|
|
@@ -2524,7 +2524,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2524
2524
|
hubLogStream.on('error', () => {
|
|
2525
2525
|
hubLogStream = null;
|
|
2526
2526
|
});
|
|
2527
|
-
hubLogStream.write(`\n---
|
|
2527
|
+
hubLogStream.write(`\n--- VuvoDesk Hub ${readVersion()} pid=${process.pid} started ${new Date().toISOString()} debug=${debugConsole} ---\n`);
|
|
2528
2528
|
} catch {
|
|
2529
2529
|
// Console output remains available when the optional diagnostic log cannot be written.
|
|
2530
2530
|
}
|
|
@@ -2570,7 +2570,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2570
2570
|
});
|
|
2571
2571
|
} catch (error) {
|
|
2572
2572
|
reportLauncherUpdate(
|
|
2573
|
-
`[
|
|
2573
|
+
`[VuvoDesk Hub] Self Agent is unavailable: ${error?.message || error}. Remote Clients remain available.`,
|
|
2574
2574
|
true
|
|
2575
2575
|
);
|
|
2576
2576
|
}
|
|
@@ -2618,7 +2618,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2618
2618
|
return;
|
|
2619
2619
|
}
|
|
2620
2620
|
reportLauncherUpdate(
|
|
2621
|
-
`[
|
|
2621
|
+
`[VuvoDesk Hub] Launcher received ${signal}; requesting graceful Hub shutdown for pid=${child.pid || 'unknown'}.`
|
|
2622
2622
|
);
|
|
2623
2623
|
void (async () => {
|
|
2624
2624
|
const gracefulDeadline = Date.now() + 2_000;
|
|
@@ -2633,7 +2633,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2633
2633
|
});
|
|
2634
2634
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2635
2635
|
reportLauncherUpdate(
|
|
2636
|
-
`[
|
|
2636
|
+
`[VuvoDesk Hub] Owned Hub pid=${child.pid || 'unknown'} acknowledged graceful shutdown.`
|
|
2637
2637
|
);
|
|
2638
2638
|
return;
|
|
2639
2639
|
} catch (error) {
|
|
@@ -2643,7 +2643,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2643
2643
|
}
|
|
2644
2644
|
if (child.exitCode !== null || child.signalCode) return;
|
|
2645
2645
|
reportLauncherUpdate(
|
|
2646
|
-
`[
|
|
2646
|
+
`[VuvoDesk Hub] Graceful shutdown API remained unavailable for pid=${child.pid || 'unknown'}`
|
|
2647
2647
|
+ `${lastError?.message ? ` (${lastError.message})` : ''}; exact child fallback remains armed.`,
|
|
2648
2648
|
true
|
|
2649
2649
|
);
|
|
@@ -2654,7 +2654,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2654
2654
|
launcherShutdownHardStopTimer = setTimeout(() => {
|
|
2655
2655
|
if (child.exitCode !== null || child.signalCode) return;
|
|
2656
2656
|
reportLauncherUpdate(
|
|
2657
|
-
`[
|
|
2657
|
+
`[VuvoDesk Hub] Owned Hub pid=${child.pid || 'unknown'} exceeded graceful shutdown; `
|
|
2658
2658
|
+ 'force-stopping that exact child root.',
|
|
2659
2659
|
true
|
|
2660
2660
|
);
|
|
@@ -2671,7 +2671,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2671
2671
|
|
|
2672
2672
|
const restartWithLatest = async request => {
|
|
2673
2673
|
if (!updateHost?.available) {
|
|
2674
|
-
throw new Error('The independent
|
|
2674
|
+
throw new Error('The independent VuvoDesk Update Host is unavailable. The unchanged Hub remains running.');
|
|
2675
2675
|
}
|
|
2676
2676
|
const requestedVersion = normalizeExactUpdateVersion(request?.latestVersion);
|
|
2677
2677
|
if (!requestedVersion) {
|
|
@@ -2726,7 +2726,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2726
2726
|
updatedAt: restartStartedAt
|
|
2727
2727
|
});
|
|
2728
2728
|
} catch (error) {
|
|
2729
|
-
reportLauncherUpdate(`[
|
|
2729
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Could not persist the restart supervisor state: ${error instanceof Error ? error.message : String(error)}.`, true);
|
|
2730
2730
|
throw error;
|
|
2731
2731
|
}
|
|
2732
2732
|
const workerEnvironment = buildUpdateHostWorkerEnvironment(process.env, {
|
|
@@ -2767,10 +2767,10 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2767
2767
|
}, { timeoutMs: handshakeTimeoutMs });
|
|
2768
2768
|
hostJob = submitted?.job || null;
|
|
2769
2769
|
if (!hostJob || Number(hostJob.workerPid || 0) <= 1) {
|
|
2770
|
-
throw new Error('
|
|
2770
|
+
throw new Error('VuvoDesk Update Host accepted the request without a worker identity.');
|
|
2771
2771
|
}
|
|
2772
2772
|
} catch (error) {
|
|
2773
|
-
throw new Error(`
|
|
2773
|
+
throw new Error(`VuvoDesk Update Host rejected the Hub replacement: ${error instanceof Error ? error.message : String(error)}`);
|
|
2774
2774
|
}
|
|
2775
2775
|
try {
|
|
2776
2776
|
await waitForHubRestartUpdateHostClaim(updateHost, hostJobId, {
|
|
@@ -2801,7 +2801,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2801
2801
|
throw error;
|
|
2802
2802
|
}
|
|
2803
2803
|
reportLauncherUpdate(
|
|
2804
|
-
`[
|
|
2804
|
+
`[VuvoDesk Hub] Update handoff claimed target=${requestedVersion} `
|
|
2805
2805
|
+ `updateHostPid=${updateHost.host?.pid || 0} workerPid=${hostJob.workerPid || 0} operation=${operationId}. `
|
|
2806
2806
|
+ `The current launcher will now exit. Restart diagnostics: ${hubLogPath}`
|
|
2807
2807
|
);
|
|
@@ -2823,14 +2823,14 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2823
2823
|
const requestPid = Number(request?.pid || 0);
|
|
2824
2824
|
const activeChildPid = Number(activeChild?.pid || 0);
|
|
2825
2825
|
if (!Number.isInteger(requestPid) || requestPid <= 1) {
|
|
2826
|
-
reportLauncherUpdate('[
|
|
2826
|
+
reportLauncherUpdate('[VuvoDesk Hub] Ignored an invalid update restart request without a Hub PID.', true);
|
|
2827
2827
|
} else if (requestPid !== activeChildPid) {
|
|
2828
|
-
reportLauncherUpdate(`[
|
|
2828
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Ignored stale update restart request for pid ${requestPid}; active Hub pid is ${activeChildPid || 'unavailable'}.`, true);
|
|
2829
2829
|
} else {
|
|
2830
2830
|
const operationId = String(request?.operationId || `hub-update-${Date.now()}`).trim();
|
|
2831
2831
|
const requestedVersion = normalizeExactUpdateVersion(request?.latestVersion);
|
|
2832
2832
|
const failPreflight = message => {
|
|
2833
|
-
reportLauncherUpdate(`[
|
|
2833
|
+
reportLauncherUpdate(`[VuvoDesk Hub] ${message}. The current Hub remains running.`, true);
|
|
2834
2834
|
try {
|
|
2835
2835
|
const now = new Date().toISOString();
|
|
2836
2836
|
writeJsonAtomic(hubUpdateResultPath, {
|
|
@@ -2847,7 +2847,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2847
2847
|
error: message
|
|
2848
2848
|
});
|
|
2849
2849
|
} catch (persistError) {
|
|
2850
|
-
reportLauncherUpdate(`[
|
|
2850
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Could not persist the update preflight failure: ${persistError instanceof Error ? persistError.message : String(persistError)}.`, true);
|
|
2851
2851
|
}
|
|
2852
2852
|
};
|
|
2853
2853
|
if (!requestedVersion) {
|
|
@@ -2870,7 +2870,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2870
2870
|
_neutralCwd: neutralCwd,
|
|
2871
2871
|
_originalCwd: UPDATE_ORIGINAL_CWD
|
|
2872
2872
|
};
|
|
2873
|
-
reportLauncherUpdate(`[
|
|
2873
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Restart requested for update ${request.operationId || 'unknown'} target=${requestedVersion}.`);
|
|
2874
2874
|
void stopHubSelfAgent('hub-update');
|
|
2875
2875
|
const updateChild = activeChild;
|
|
2876
2876
|
let shutdownFallbackStarted = false;
|
|
@@ -2878,18 +2878,18 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2878
2878
|
if (!updateChild || updateChild.exitCode !== null || updateChild.signalCode) return;
|
|
2879
2879
|
if (shutdownFallbackStarted) return;
|
|
2880
2880
|
shutdownFallbackStarted = true;
|
|
2881
|
-
reportLauncherUpdate(`[
|
|
2881
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Update shutdown fallback: ${reason}; terminating Hub pid=${requestPid}.`, true);
|
|
2882
2882
|
try { updateChild.kill('SIGTERM'); } catch { /* child already exited */ }
|
|
2883
2883
|
updateHardStopTimer = setTimeout(() => {
|
|
2884
2884
|
if (!updateChild || updateChild.exitCode !== null || updateChild.signalCode) return;
|
|
2885
|
-
reportLauncherUpdate(`[
|
|
2885
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Update shutdown hard fallback: Hub pid=${requestPid} ignored graceful termination; force-stopping the owned Hub process.`, true);
|
|
2886
2886
|
if (process.platform === 'win32') {
|
|
2887
2887
|
// updateChild is the exact ChildProcess instance spawned by this
|
|
2888
2888
|
// launcher. Stop only that owned Hub root; never let taskkill /T
|
|
2889
2889
|
// follow unverified PPID descendants.
|
|
2890
2890
|
try { updateChild.kill('SIGKILL'); } catch (error) {
|
|
2891
2891
|
if (updateChild.exitCode === null && !updateChild.signalCode) {
|
|
2892
|
-
reportLauncherUpdate(`[
|
|
2892
|
+
reportLauncherUpdate(`[VuvoDesk Hub] hard-stop fallback failed for pid=${requestPid}: ${error.message}`, true);
|
|
2893
2893
|
}
|
|
2894
2894
|
}
|
|
2895
2895
|
} else {
|
|
@@ -2905,7 +2905,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2905
2905
|
signal: AbortSignal.timeout(2_500)
|
|
2906
2906
|
}).then(response => {
|
|
2907
2907
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2908
|
-
reportLauncherUpdate(`[
|
|
2908
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Update shutdown acknowledged by Hub pid=${requestPid}.`);
|
|
2909
2909
|
}).catch(error => forceStop(`shutdown API failed (${error?.message || error})`));
|
|
2910
2910
|
}
|
|
2911
2911
|
} catch {
|
|
@@ -2933,7 +2933,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2933
2933
|
hubSelfAgent?.start();
|
|
2934
2934
|
}).catch(error => {
|
|
2935
2935
|
reportLauncherUpdate(
|
|
2936
|
-
`[
|
|
2936
|
+
`[VuvoDesk Hub] Self Agent startup wait failed: ${error?.message || error}.`,
|
|
2937
2937
|
true
|
|
2938
2938
|
);
|
|
2939
2939
|
});
|
|
@@ -2955,7 +2955,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2955
2955
|
});
|
|
2956
2956
|
|
|
2957
2957
|
child.once('error', error => {
|
|
2958
|
-
console.error(`Failed to start
|
|
2958
|
+
console.error(`Failed to start VuvoDesk Hub: ${error.message}`);
|
|
2959
2959
|
process.exitCode = 1;
|
|
2960
2960
|
});
|
|
2961
2961
|
child.once('exit', (code, signal) => {
|
|
@@ -2990,7 +2990,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2990
2990
|
}
|
|
2991
2991
|
void stopHubSelfAgent('hub-update').then(() => restartWithLatest(request)).catch(error => {
|
|
2992
2992
|
const message = `The Hub restart handoff failed after shutdown: ${error instanceof Error ? error.message : String(error)}`;
|
|
2993
|
-
reportLauncherUpdate(`[
|
|
2993
|
+
reportLauncherUpdate(`[VuvoDesk Hub] ${message}. Restarting the unchanged Hub.`, true);
|
|
2994
2994
|
try {
|
|
2995
2995
|
const now = new Date().toISOString();
|
|
2996
2996
|
const previous = readJsonFile(hubUpdateResultPath);
|
|
@@ -3033,7 +3033,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
3033
3033
|
runtimeLock?.updateRole?.('client');
|
|
3034
3034
|
} catch (error) {
|
|
3035
3035
|
console.error(
|
|
3036
|
-
`[
|
|
3036
|
+
`[VuvoDesk Hub] The shared runtime lock could not transition to Client: ${error?.message || error}. `
|
|
3037
3037
|
+ 'The launcher will exit so a clean invocation can recover.'
|
|
3038
3038
|
);
|
|
3039
3039
|
runtimeShutdownWatcher?.stop();
|
|
@@ -3049,15 +3049,15 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
3049
3049
|
source: 'supabase',
|
|
3050
3050
|
isOfflineFallback: false
|
|
3051
3051
|
}, { stateDir: MANAGER_STATE_DIR });
|
|
3052
|
-
console.log('[
|
|
3052
|
+
console.log('[VuvoDesk Hub] Role transition accepted. Starting the Client runtime.');
|
|
3053
3053
|
runtimeShutdownWatcher?.stop();
|
|
3054
3054
|
removeLauncherSignalHandlers();
|
|
3055
3055
|
void runClient([], { ...resolvedRole, role: 'client', source: 'supabase', roleTransition: true }, runtimeLock).catch(error => {
|
|
3056
|
-
console.error(`Failed to start
|
|
3056
|
+
console.error(`Failed to start VuvoDesk Client runtime: ${error?.message || error}`);
|
|
3057
3057
|
process.exitCode = 1;
|
|
3058
3058
|
});
|
|
3059
3059
|
}).catch(error => {
|
|
3060
|
-
console.error(`[
|
|
3060
|
+
console.error(`[VuvoDesk Hub] Self Agent did not stop for role transition: ${error?.message || error}`);
|
|
3061
3061
|
process.exit(1);
|
|
3062
3062
|
});
|
|
3063
3063
|
return;
|
|
@@ -3068,7 +3068,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
3068
3068
|
&& startupRetryCount < HUB_STARTUP_RETRY_LIMIT
|
|
3069
3069
|
&& isPortConflictOutput(startupOutput)) {
|
|
3070
3070
|
startupRetryCount += 1;
|
|
3071
|
-
console.warn('[
|
|
3071
|
+
console.warn('[VuvoDesk Hub] Startup port conflict detected. Cleaning Hub ports and retrying once.');
|
|
3072
3072
|
void stopProcessesOnPorts([httpPort, remotePort])
|
|
3073
3073
|
.then(() => startHubProcess())
|
|
3074
3074
|
.catch(error => {
|
|
@@ -3092,7 +3092,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
3092
3092
|
|
|
3093
3093
|
runtimeShutdownWatcher = startOwnedRuntimeShutdownWatcher(runtimeOwner, async request => {
|
|
3094
3094
|
reportLauncherUpdate(
|
|
3095
|
-
`[
|
|
3095
|
+
`[VuvoDesk Hub] Cooperative replacement request accepted `
|
|
3096
3096
|
+ `request=${request.requestId} reason=${request.reason}.`
|
|
3097
3097
|
);
|
|
3098
3098
|
requestLauncherShutdown('COOPERATIVE_REPLACEMENT');
|
|
@@ -3111,15 +3111,15 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
3111
3111
|
}
|
|
3112
3112
|
void transferSavedClientSessionToHub(hubProbeBaseUrl, MANAGER_STATE_DIR).then(handoff => {
|
|
3113
3113
|
if (handoff.ok) {
|
|
3114
|
-
reportLauncherUpdate('[
|
|
3114
|
+
reportLauncherUpdate('[VuvoDesk Hub] Saved Client sign-in adopted by the Hub.');
|
|
3115
3115
|
if (!handoff.hostTargetOk) {
|
|
3116
|
-
reportLauncherUpdate('[
|
|
3116
|
+
reportLauncherUpdate('[VuvoDesk Hub] Sign-in was adopted; host publication will continue through lease renewal.', true);
|
|
3117
3117
|
}
|
|
3118
3118
|
} else if (!handoff.skipped) {
|
|
3119
|
-
reportLauncherUpdate(`[
|
|
3119
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Saved Client sign-in could not be adopted: ${handoff.reason}. Continue sign-in in the Hub page.`, true);
|
|
3120
3120
|
}
|
|
3121
3121
|
}).catch(error => {
|
|
3122
|
-
reportLauncherUpdate(`[
|
|
3122
|
+
reportLauncherUpdate(`[VuvoDesk Hub] Saved Client sign-in could not be adopted: ${error instanceof Error ? error.message : String(error)}. Continue sign-in in the Hub page.`, true);
|
|
3123
3123
|
});
|
|
3124
3124
|
return true;
|
|
3125
3125
|
});
|
|
@@ -3129,15 +3129,16 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
3129
3129
|
if (ok) {
|
|
3130
3130
|
openBrowser(openUrl);
|
|
3131
3131
|
} else {
|
|
3132
|
-
console.warn(`
|
|
3132
|
+
console.warn(`VuvoDesk Hub did not answer at ${openUrl} yet. Open it manually when it is ready.`);
|
|
3133
3133
|
}
|
|
3134
3134
|
});
|
|
3135
3135
|
}
|
|
3136
3136
|
}
|
|
3137
3137
|
|
|
3138
|
-
async function runClient(args, resolvedRole = null, runtimeLock = null) {
|
|
3139
|
-
const runtimeOwner = runtimeLock?.payload || null;
|
|
3140
|
-
const internalClientEntry = resolve(packageRoot, 'client', 'bin', 'livedesk-client.js');
|
|
3138
|
+
async function runClient(args, resolvedRole = null, runtimeLock = null) {
|
|
3139
|
+
const runtimeOwner = runtimeLock?.payload || null;
|
|
3140
|
+
const internalClientEntry = resolve(packageRoot, 'client', 'bin', 'livedesk-client.js');
|
|
3141
|
+
const packagedWebDist = resolve(packageRoot, 'web', 'dist');
|
|
3141
3142
|
if (!existsSync(internalClientEntry)) {
|
|
3142
3143
|
throw new Error('The published livedesk package is missing its internal Client runtime. Rebuild the package with npm run build.');
|
|
3143
3144
|
}
|
|
@@ -3151,7 +3152,9 @@ async function runClient(args, resolvedRole = null, runtimeLock = null) {
|
|
|
3151
3152
|
LIVEDESK_NPM_LAUNCHER_VERSION: readVersion(),
|
|
3152
3153
|
LIVEDESK_RUNTIME_ROLE: 'client',
|
|
3153
3154
|
LIVEDESK_UNIFIED_RUNTIME: '1',
|
|
3154
|
-
LIVEDESK_WEB_DIST: resolve(
|
|
3155
|
+
LIVEDESK_WEB_DIST: existsSync(resolve(packagedWebDist, 'index.html'))
|
|
3156
|
+
? packagedWebDist
|
|
3157
|
+
: String(process.env.LIVEDESK_WEB_DIST || ''),
|
|
3155
3158
|
LIVEDESK_CLIENT_RUNTIME_PORT: String(process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_LOCAL_RUNTIME_PORT || DEFAULT_MANAGER_HTTP_PORT),
|
|
3156
3159
|
LIVEDESK_UNIFIED_LAUNCHER_ENTRY: String(process.argv[1] || ''),
|
|
3157
3160
|
LIVEDESK_ROLE_TRANSITION: resolvedRole?.roleTransition === true || process.env.LIVEDESK_ROLE_TRANSITION === '1'
|
|
@@ -3177,7 +3180,7 @@ async function runClient(args, resolvedRole = null, runtimeLock = null) {
|
|
|
3177
3180
|
}
|
|
3178
3181
|
const runtimeShutdownWatcher = startOwnedRuntimeShutdownWatcher(runtimeOwner, async request => {
|
|
3179
3182
|
console.log(
|
|
3180
|
-
`[
|
|
3183
|
+
`[VuvoDesk Client] Cooperative replacement request accepted `
|
|
3181
3184
|
+ `request=${request.requestId} reason=${request.reason}. Draining the owned capture tree.`
|
|
3182
3185
|
);
|
|
3183
3186
|
if (typeof clientModule.requestClientRuntimeShutdown !== 'function') {
|
|
@@ -3193,7 +3196,7 @@ async function runClient(args, resolvedRole = null, runtimeLock = null) {
|
|
|
3193
3196
|
recoverHubClientPort: async port => {
|
|
3194
3197
|
const normalizedPort = normalizePort(port, 0);
|
|
3195
3198
|
if (!normalizedPort) {
|
|
3196
|
-
throw new Error(`
|
|
3199
|
+
throw new Error(`VuvoDesk cannot recover an invalid Hub client port: ${port}.`);
|
|
3197
3200
|
}
|
|
3198
3201
|
await stopProcessesOnPorts([normalizedPort]);
|
|
3199
3202
|
}
|
|
@@ -3217,7 +3220,7 @@ async function runDesktopRoleBootstrap() {
|
|
|
3217
3220
|
onShutdown: () => process.exit(0)
|
|
3218
3221
|
});
|
|
3219
3222
|
await bootstrap.start();
|
|
3220
|
-
console.log(`[
|
|
3223
|
+
console.log(`[VuvoDesk] Role bootstrap is ready at ${bootstrap.url}. Sign in and choose Hub or Client.`);
|
|
3221
3224
|
await new Promise(() => undefined);
|
|
3222
3225
|
}
|
|
3223
3226
|
|
|
@@ -3253,7 +3256,7 @@ async function main() {
|
|
|
3253
3256
|
const commandArgs = args.slice(1).map(value => String(value || '').trim().toLowerCase());
|
|
3254
3257
|
if (commandArgs.some(value => value === '--help' || value === '-h' || value === 'help')) {
|
|
3255
3258
|
if (command === 'client') {
|
|
3256
|
-
console.warn('[
|
|
3259
|
+
console.warn('[VuvoDesk] Legacy client command detected. Use `npx -y --prefer-online livedesk@latest` for automatic role selection.');
|
|
3257
3260
|
printClientHelp();
|
|
3258
3261
|
} else {
|
|
3259
3262
|
printHelp();
|
|
@@ -3262,7 +3265,7 @@ async function main() {
|
|
|
3262
3265
|
}
|
|
3263
3266
|
if (commandArgs.some(value => value === '--version' || value === '-v' || value === 'version')) {
|
|
3264
3267
|
if (command === 'client') {
|
|
3265
|
-
console.warn('[
|
|
3268
|
+
console.warn('[VuvoDesk] Legacy client command detected. Use `npx -y --prefer-online livedesk@latest` for automatic role selection.');
|
|
3266
3269
|
}
|
|
3267
3270
|
console.log(readVersion());
|
|
3268
3271
|
return;
|
|
@@ -3285,7 +3288,7 @@ async function main() {
|
|
|
3285
3288
|
writeRoleCache({ ...resolvedRole, source: 'local-cache', isOfflineFallback: false }, roleStateOptions);
|
|
3286
3289
|
runtimeArgs = args.slice(1);
|
|
3287
3290
|
if (command === 'client') {
|
|
3288
|
-
console.warn('[
|
|
3291
|
+
console.warn('[VuvoDesk] Legacy client command detected. Use `npx -y --prefer-online livedesk@latest` for automatic role selection.');
|
|
3289
3292
|
}
|
|
3290
3293
|
} else {
|
|
3291
3294
|
const forceDesktopBootstrap = process.env.LIVEDESK_DESKTOP_HOST === '1'
|
|
@@ -3298,7 +3301,7 @@ async function main() {
|
|
|
3298
3301
|
});
|
|
3299
3302
|
if (!resolvedRole.role) {
|
|
3300
3303
|
if (resolvedRole.disabled) {
|
|
3301
|
-
throw new Error(`
|
|
3304
|
+
throw new Error(`VuvoDesk device ${resolvedRole.deviceId} is disabled for this account.`);
|
|
3302
3305
|
}
|
|
3303
3306
|
if (forceDesktopBootstrap) {
|
|
3304
3307
|
await runDesktopRoleBootstrap();
|
|
@@ -3319,7 +3322,7 @@ async function main() {
|
|
|
3319
3322
|
}
|
|
3320
3323
|
|
|
3321
3324
|
if (topLevel.noSingleInstance || process.env.LIVEDESK_DISABLE_SINGLE_INSTANCE === '1') {
|
|
3322
|
-
console.warn('[
|
|
3325
|
+
console.warn('[VuvoDesk] Single-instance lock disabled for this run.');
|
|
3323
3326
|
}
|
|
3324
3327
|
const launcherProcessDetails = await getProcessDetails(process.pid);
|
|
3325
3328
|
const launcherInstanceMarker = processInstanceMarker(launcherProcessDetails);
|
|
@@ -3327,7 +3330,7 @@ async function main() {
|
|
|
3327
3330
|
const reportExistingRuntime = existing => {
|
|
3328
3331
|
const port = DEFAULT_MANAGER_HTTP_PORT;
|
|
3329
3332
|
openBrowser(`http://127.0.0.1:${port}`);
|
|
3330
|
-
console.log(`[
|
|
3333
|
+
console.log(`[VuvoDesk] An existing ${existing.role || 'runtime'} is already running (pid ${existing.pid}). Opened its local page. Hub diagnostics: ${join(MANAGER_STATE_DIR, 'hub.log')}`);
|
|
3331
3334
|
};
|
|
3332
3335
|
let lock = topLevel.noSingleInstance || process.env.LIVEDESK_DISABLE_SINGLE_INSTANCE === '1'
|
|
3333
3336
|
? { acquired: true, release: () => undefined }
|
|
@@ -3382,12 +3385,12 @@ async function main() {
|
|
|
3382
3385
|
Object.assign(process.env, updateHostEnvironment(updateHost));
|
|
3383
3386
|
if (updateHost.available) {
|
|
3384
3387
|
console.log(
|
|
3385
|
-
`[
|
|
3388
|
+
`[VuvoDesk] Independent Update Host ready pid=${updateHost.host?.pid || 'unknown'} `
|
|
3386
3389
|
+ `source=${updateHost.host?.sourceVersion || readVersion()}.`
|
|
3387
3390
|
);
|
|
3388
3391
|
} else if (!updateHost.disabled) {
|
|
3389
3392
|
console.warn(
|
|
3390
|
-
`[
|
|
3393
|
+
`[VuvoDesk] Independent Update Host is unavailable; updates are disabled for this run: `
|
|
3391
3394
|
+ `${updateHost.error || 'unknown error'}`
|
|
3392
3395
|
);
|
|
3393
3396
|
}
|
|
@@ -3413,7 +3416,7 @@ async function main() {
|
|
|
3413
3416
|
&& process.env.LIVEDESK_DEBUG === '1'
|
|
3414
3417
|
&& Number.isFinite(testHoldAfterDrainMs)
|
|
3415
3418
|
&& testHoldAfterDrainMs > 0) {
|
|
3416
|
-
console.log('[
|
|
3419
|
+
console.log('[VuvoDesk test] Holding after reclaimed manifest drain before Agent startup.');
|
|
3417
3420
|
await waitMilliseconds(Math.min(60_000, Math.round(testHoldAfterDrainMs)));
|
|
3418
3421
|
}
|
|
3419
3422
|
}
|
|
@@ -3429,7 +3432,7 @@ async function main() {
|
|
|
3429
3432
|
if (!explicitMultiInstance) {
|
|
3430
3433
|
await stopStaleClientAgents();
|
|
3431
3434
|
} else if (process.env.LIVEDESK_DEBUG === '1') {
|
|
3432
|
-
console.log('[
|
|
3435
|
+
console.log('[VuvoDesk] Client lifecycle audit skipped for explicit multi-instance mode.');
|
|
3433
3436
|
}
|
|
3434
3437
|
}
|
|
3435
3438
|
|