livedesk 0.1.383 → 0.1.385
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/bin/livedesk.js +221 -70
- package/client/bin/livedesk-client-node.js +285 -79
- package/client/bin/livedesk-client.js +102 -12
- package/client/package.json +5 -5
- package/hub/src/live-desk-update.js +403 -186
- package/hub/src/remote-hub.js +8 -6
- package/hub/src/server.js +244 -44
- package/package.json +8 -7
- package/web/dist/assets/index-CXcKGcRR.js +16 -0
- package/web/dist/assets/{index-C09kUT0m.css → index-knuZbyFM.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-C2mYNpPt.js +0 -16
package/bin/livedesk.js
CHANGED
|
@@ -5,7 +5,7 @@ import net from 'node:net';
|
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
import { execFile, spawn } from 'node:child_process';
|
|
8
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import { resolveDeviceRole, writeRoleCache } from '../bootstrap/device-role.js';
|
|
@@ -27,10 +27,29 @@ const PORT_CLEANUP_POLL_MS = 100;
|
|
|
27
27
|
const EXISTING_RUNTIME_STOP_TIMEOUT_MS = 10000;
|
|
28
28
|
const HUB_STARTUP_RETRY_LIMIT = 1;
|
|
29
29
|
const HUB_UPDATE_POLL_MS = 250;
|
|
30
|
-
const MANAGER_STATE_DIR = process.env.LIVEDESK_STATE_DIR || join(os.homedir(), '.livedesk');
|
|
31
|
-
const MANAGER_STATE_PATH = join(MANAGER_STATE_DIR, 'manager.json');
|
|
32
|
-
|
|
33
|
-
function
|
|
30
|
+
const MANAGER_STATE_DIR = process.env.LIVEDESK_STATE_DIR || join(os.homedir(), '.livedesk');
|
|
31
|
+
const MANAGER_STATE_PATH = join(MANAGER_STATE_DIR, 'manager.json');
|
|
32
|
+
|
|
33
|
+
function writeJsonAtomic(filePath, value) {
|
|
34
|
+
if (!filePath) return;
|
|
35
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
36
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
37
|
+
let renamed = false;
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(temporaryPath, JSON.stringify(value, null, 2), {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
mode: 0o600
|
|
42
|
+
});
|
|
43
|
+
renameSync(temporaryPath, filePath);
|
|
44
|
+
renamed = true;
|
|
45
|
+
} finally {
|
|
46
|
+
if (!renamed) {
|
|
47
|
+
try { rmSync(temporaryPath, { force: true }); } catch { /* best effort cleanup */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function printHelp() {
|
|
34
53
|
process.stdout.write(`
|
|
35
54
|
LiveDesk
|
|
36
55
|
|
|
@@ -772,20 +791,44 @@ function resolveNpxInvocation(npxArgs) {
|
|
|
772
791
|
|
|
773
792
|
function buildHubRestartBootstrapScript() {
|
|
774
793
|
return [
|
|
775
|
-
'const { spawn } = require("node:child_process");',
|
|
776
|
-
'const { appendFileSync } = require("node:fs");',
|
|
794
|
+
'const { execFile, spawn } = require("node:child_process");',
|
|
795
|
+
'const { appendFileSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } = require("node:fs");',
|
|
796
|
+
'const { dirname } = require("node:path");',
|
|
797
|
+
'const net = require("node:net");',
|
|
777
798
|
'const waitPid = Number(process.env.LIVEDESK_RESTART_WAIT_PID || 0);',
|
|
799
|
+
'const previousRuntimePid = Number(process.env.LIVEDESK_RESTART_PREVIOUS_RUNTIME_PID || 0);',
|
|
778
800
|
'const logPath = process.env.LIVEDESK_RESTART_LOG_PATH || "";',
|
|
801
|
+
'const resultPath = process.env.LIVEDESK_RESTART_RESULT_PATH || "";',
|
|
802
|
+
'const operationId = process.env.LIVEDESK_RESTART_OPERATION_ID || `hub-update-${Date.now()}`;',
|
|
803
|
+
'const startedAt = process.env.LIVEDESK_RESTART_STARTED_AT || new Date().toISOString();',
|
|
779
804
|
'const log = message => { if (!logPath) return; try { appendFileSync(logPath, `[Hub restart] ${new Date().toISOString()} ${message}\\n`, "utf8"); } catch {} };',
|
|
805
|
+
'const readPreviousResult = () => { if (!resultPath) return null; try { const value = JSON.parse(readFileSync(resultPath, "utf8")); return value?.operationId === operationId ? value : null; } catch { return null; } };',
|
|
806
|
+
'const writeResult = (stage, details = {}) => { if (!resultPath) return; const now = new Date().toISOString(); const previous = readPreviousResult(); const value = { ...(previous || {}), operationId, stage, targetVersion: expectedVersion, fallbackVersion, previousRuntimePid, supervisorPid: process.pid, startedAt: previous?.startedAt || startedAt, updatedAt: now, ...details }; const temporaryPath = `${resultPath}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`; let renamed = false; try { mkdirSync(dirname(resultPath), { recursive: true }); writeFileSync(temporaryPath, JSON.stringify(value, null, 2), { encoding: "utf8", mode: 0o600 }); renameSync(temporaryPath, resultPath); renamed = true; } catch (error) { log(`stage=result-write-failed resultStage=${stage} error=${error.message}`); } finally { if (!renamed) { try { rmSync(temporaryPath, { force: true }); } catch {} } } };',
|
|
780
807
|
'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; } };',
|
|
781
|
-
'const
|
|
782
|
-
'const
|
|
783
|
-
'const
|
|
784
|
-
'const
|
|
785
|
-
'
|
|
786
|
-
''
|
|
787
|
-
|
|
788
|
-
|
|
808
|
+
'const isGroupAlive = pid => { try { process.kill(-pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; } };',
|
|
809
|
+
'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
|
|
810
|
+
'const numberFromEnv = (name, fallback, minimum) => { const value = Number(process.env[name]); return Number.isFinite(value) && value >= minimum ? Math.round(value) : fallback; };',
|
|
811
|
+
'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 LiveDesk Hub launcher pid=${pid} did not exit within ${timeoutMs}ms.`); };',
|
|
812
|
+
'const decodeArgs = name => JSON.parse(Buffer.from(process.env[name] || "", "base64").toString("utf8"));',
|
|
813
|
+
'const primary = { command: process.env.LIVEDESK_RESTART_COMMAND || "", args: decodeArgs("LIVEDESK_RESTART_ARGS_BASE64") };',
|
|
814
|
+
'const fallback = { command: process.env.LIVEDESK_RESTART_FALLBACK_COMMAND || "", args: decodeArgs("LIVEDESK_RESTART_FALLBACK_ARGS_BASE64") };',
|
|
815
|
+
'const expectedVersion = process.env.LIVEDESK_RESTART_EXPECTED_VERSION || "";',
|
|
816
|
+
'const fallbackVersion = process.env.LIVEDESK_RESTART_FALLBACK_VERSION || "";',
|
|
817
|
+
'const healthBaseUrl = process.env.LIVEDESK_RESTART_HEALTH_URL || "http://127.0.0.1:5179";',
|
|
818
|
+
'const remoteHost = process.env.LIVEDESK_RESTART_REMOTE_HOST || "127.0.0.1";',
|
|
819
|
+
'const remotePort = Number(process.env.LIVEDESK_RESTART_REMOTE_PORT || 5197);',
|
|
820
|
+
'const attemptLimit = numberFromEnv("LIVEDESK_RESTART_ATTEMPTS", 3, 1);',
|
|
821
|
+
'const healthTimeoutMs = numberFromEnv("LIVEDESK_RESTART_HEALTH_TIMEOUT_MS", 30000, 1000);',
|
|
822
|
+
'const pollMs = numberFromEnv("LIVEDESK_RESTART_HEALTH_POLL_MS", 250, 50);',
|
|
823
|
+
'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(); };',
|
|
824
|
+
'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); }); });',
|
|
825
|
+
'const probeReplacement = async requiredVersion => { const [status, health] = await Promise.all([requestJson("/api/runtime/status"), requestJson("/api/health")]); const runtimePid = Number(status?.runtimePid || 0); if (status?.role !== "hub" || status?.runtimeStarted !== true || runtimePid <= 1) throw new Error("replacement runtime status is not ready"); if (previousRuntimePid > 1 && runtimePid === previousRuntimePid) throw new Error(`old runtime pid=${runtimePid} is still answering`); if (requiredVersion && String(status?.appVersion || "") !== requiredVersion) throw new Error(`version mismatch expected=${requiredVersion} actual=${status?.appVersion || "unknown"}`); if (health?.ok !== true || health?.product !== "LiveDesk") throw new Error("replacement health response is invalid"); await probeRemotePort(); return { runtimePid, appVersion: String(status?.appVersion || "") }; };',
|
|
826
|
+
'const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; log(`terminating unverified replacement pid=${pid}`); if (process.platform === "win32") { if (isAlive(pid)) await new Promise(resolve => execFile("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }, () => resolve())); await sleep(250); return; } try { process.kill(-pid, "SIGTERM"); } catch { try { child.kill("SIGTERM"); } catch {} } const gracefulDeadline = Date.now() + 2000; while (Date.now() < gracefulDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); if (isGroupAlive(pid) || isAlive(pid)) { log(`unverified replacement pid=${pid} ignored SIGTERM; sending SIGKILL to process group`); try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } const hardDeadline = Date.now() + 1000; while (Date.now() < hardDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); } };',
|
|
827
|
+
'const launchAndVerify = async (invocation, label, requiredVersion, attempt) => { if (!invocation.command || !Array.isArray(invocation.args)) throw new Error(`${label} restart command is unavailable`); log(`${label} attempt=${attempt} stage=spawning command=${invocation.command} target=${requiredVersion || "latest"}`); let exit = null; let exitedAt = 0; const child = spawn(invocation.command, invocation.args, { cwd: process.env.LIVEDESK_RESTART_CWD || process.cwd(), env: process.env, detached: true, stdio: "ignore", windowsHide: true }); await new Promise((resolve, reject) => { child.once("error", reject); child.once("spawn", resolve); }); log(`${label} attempt=${attempt} stage=spawned pid=${child.pid || 0}`); child.once("exit", (code, signal) => { exit = { code, signal }; exitedAt = Date.now(); log(`${label} attempt=${attempt} stage=exited code=${code ?? "none"} signal=${signal || "none"}`); }); const deadline = Date.now() + healthTimeoutMs; let lastError = null; while (Date.now() < deadline) { try { const status = await probeReplacement(requiredVersion); log(`${label} attempt=${attempt} stage=verified launcherPid=${child.pid || 0} runtimePid=${status.runtimePid} version=${status.appVersion || "unknown"} http=${healthBaseUrl} remote=${remoteHost}:${remotePort}`); child.unref(); return { ...status, launcherPid: Number(child.pid || 0) }; } catch (error) { lastError = error; } if (exit && Date.now() - exitedAt >= 1500) break; await sleep(pollMs); } await terminateTree(child); throw new Error(`${label} attempt=${attempt} did not become healthy: ${lastError?.message || (exit ? `command exited code=${exit.code ?? "none"} signal=${exit.signal || "none"}` : "health deadline expired")}`); };',
|
|
828
|
+
'(async () => { const targetErrors = []; const fallbackErrors = []; writeResult("waiting-for-old-launcher", { outcome: "in-progress" }); log(`stage=waiting-for-old-launcher pid=${waitPid}`); 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) { 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 || "latest"} failed verification: ${targetErrors.join(" | ")}` }); log("stage=completed-with-fallback"); return; } catch (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 LiveDesk Hub replacement could be started. Target failures: ${targetErrors.join(" | ") || "none"}. Fallback failures: ${fallbackErrors.join(" | ") || "none"}. Inspect ${logPath || "the Hub restart log"} and start livedesk@latest manually.`); })().catch(error => { writeResult("failed", { outcome: "failed", completedAt: new Date().toISOString(), error: error.message }); log("stage=bootstrap-failed error=" + error.message); process.exitCode = 1; });',
|
|
829
|
+
''
|
|
830
|
+
].join('\n');
|
|
831
|
+
}
|
|
789
832
|
|
|
790
833
|
async function runManager(args, resolvedRole = null) {
|
|
791
834
|
const options = parseManagerArgs(args);
|
|
@@ -800,30 +843,42 @@ async function runManager(args, resolvedRole = null) {
|
|
|
800
843
|
const openUrl = options.openUrlExplicit
|
|
801
844
|
? (options.openUrl || `http://127.0.0.1:${httpPort}`)
|
|
802
845
|
: `http://127.0.0.1:${httpPort}`;
|
|
803
|
-
const pairToken = process.env.REMOTE_HUB_PAIR_TOKEN
|
|
804
|
-
|| process.env.LIVEDESK_CLIENT_PAIR_TOKEN
|
|
805
|
-
|| process.env.MINDEXEC_REMOTE_PAIR_TOKEN
|
|
806
|
-
|| getStablePairToken();
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
846
|
+
const pairToken = process.env.REMOTE_HUB_PAIR_TOKEN
|
|
847
|
+
|| process.env.LIVEDESK_CLIENT_PAIR_TOKEN
|
|
848
|
+
|| process.env.MINDEXEC_REMOTE_PAIR_TOKEN
|
|
849
|
+
|| getStablePairToken();
|
|
850
|
+
const configuredHttpHost = options.host || process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1';
|
|
851
|
+
const normalizedHttpHost = String(configuredHttpHost).trim().replace(/^\[|\]$/g, '');
|
|
852
|
+
const hubProbeHost = normalizedHttpHost === '0.0.0.0'
|
|
853
|
+
? '127.0.0.1'
|
|
854
|
+
: (normalizedHttpHost === '::' ? '::1' : normalizedHttpHost);
|
|
855
|
+
const hubProbeUrlHost = hubProbeHost.includes(':') ? `[${hubProbeHost}]` : hubProbeHost;
|
|
856
|
+
const hubProbeBaseUrl = `http://${hubProbeUrlHost}:${httpPort}`;
|
|
857
|
+
mkdirSync(MANAGER_STATE_DIR, { recursive: true });
|
|
858
|
+
const updateRequestPath = join(MANAGER_STATE_DIR, `hub-update-${httpPort}-${remotePort}.json`);
|
|
859
|
+
const hubUpdateResultPath = join(MANAGER_STATE_DIR, `hub-update-result-${httpPort}-${remotePort}.json`);
|
|
860
|
+
rmSync(updateRequestPath, { force: true });
|
|
810
861
|
|
|
811
862
|
if (options.cleanPortsOnStart) {
|
|
812
863
|
await stopProcessesOnPorts([httpPort, remotePort]);
|
|
813
864
|
}
|
|
814
865
|
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
866
|
+
const configuredHubEntry = String(process.env.LIVEDESK_HUB_ENTRY || '').trim();
|
|
867
|
+
const internalHubEntry = resolve(packageRoot, 'hub', 'src', 'server.js');
|
|
868
|
+
const hubEntry = configuredHubEntry && existsSync(configuredHubEntry)
|
|
869
|
+
? configuredHubEntry
|
|
870
|
+
: (existsSync(internalHubEntry)
|
|
871
|
+
? internalHubEntry
|
|
872
|
+
: resolvePackageEntry('@livedesk/hub', '@livedesk/hub/src/server.js'));
|
|
819
873
|
const packagedWebDist = resolve(packageRoot, 'web', 'dist');
|
|
820
|
-
const env = {
|
|
821
|
-
...process.env,
|
|
822
|
-
LIVEDESK_HUB_HTTP_HOST:
|
|
874
|
+
const env = {
|
|
875
|
+
...process.env,
|
|
876
|
+
LIVEDESK_HUB_HTTP_HOST: configuredHttpHost,
|
|
823
877
|
LIVEDESK_HUB_HTTP_PORT: String(httpPort),
|
|
824
|
-
LIVEDESK_MANAGER_VERSION: readVersion(),
|
|
825
|
-
LIVEDESK_CLIENT_PACKAGE_VERSION: readClientVersion(),
|
|
826
|
-
LIVEDESK_HUB_UPDATE_REQUEST_PATH: updateRequestPath,
|
|
878
|
+
LIVEDESK_MANAGER_VERSION: readVersion(),
|
|
879
|
+
LIVEDESK_CLIENT_PACKAGE_VERSION: readClientVersion(),
|
|
880
|
+
LIVEDESK_HUB_UPDATE_REQUEST_PATH: updateRequestPath,
|
|
881
|
+
LIVEDESK_HUB_UPDATE_RESULT_PATH: hubUpdateResultPath,
|
|
827
882
|
REMOTE_HUB_PORT: String(remotePort),
|
|
828
883
|
REMOTE_HUB_PAIR_TOKEN: pairToken,
|
|
829
884
|
LIVEDESK_WEB_DIST: existsSync(resolve(packagedWebDist, 'index.html')) ? packagedWebDist : (process.env.LIVEDESK_WEB_DIST || ''),
|
|
@@ -854,35 +909,93 @@ async function runManager(args, resolvedRole = null) {
|
|
|
854
909
|
};
|
|
855
910
|
|
|
856
911
|
let startupRetryCount = 0;
|
|
857
|
-
let
|
|
858
|
-
let activeChild = null;
|
|
859
|
-
let updatePollId = null;
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
912
|
+
let restartRequest = null;
|
|
913
|
+
let activeChild = null;
|
|
914
|
+
let updatePollId = null;
|
|
915
|
+
let updateStopTimer = null;
|
|
916
|
+
let updateHardStopTimer = null;
|
|
917
|
+
|
|
918
|
+
const restartWithLatest = request => {
|
|
919
|
+
const requestedVersion = /^[0-9A-Za-z][0-9A-Za-z.+-]*$/.test(String(request?.latestVersion || '').trim())
|
|
920
|
+
? String(request.latestVersion).trim()
|
|
921
|
+
: '';
|
|
922
|
+
const packageTarget = requestedVersion ? `livedesk@${requestedVersion}` : 'livedesk@latest';
|
|
923
|
+
const nextArgs = ['-y', '--prefer-online', packageTarget, '--force-role', 'hub', '--no-open', ...args];
|
|
863
924
|
const restartInvocation = resolveNpxInvocation(nextArgs);
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
925
|
+
const fallbackInvocation = {
|
|
926
|
+
command: process.execPath,
|
|
927
|
+
args: [String(process.argv[1] || resolve(__dirname, 'livedesk.js')), '--force-role', 'hub', '--no-open', ...args]
|
|
928
|
+
};
|
|
929
|
+
const operationId = String(request?.operationId || `hub-update-${Date.now()}`).trim();
|
|
930
|
+
const restartStartedAt = new Date().toISOString();
|
|
931
|
+
try {
|
|
932
|
+
writeJsonAtomic(hubUpdateResultPath, {
|
|
933
|
+
operationId,
|
|
934
|
+
stage: 'supervisor-starting',
|
|
935
|
+
outcome: 'in-progress',
|
|
936
|
+
targetVersion: requestedVersion,
|
|
937
|
+
fallbackVersion: readVersion(),
|
|
938
|
+
previousRuntimePid: Number(request?.pid || 0),
|
|
939
|
+
launcherPid: process.pid,
|
|
940
|
+
startedAt: restartStartedAt,
|
|
941
|
+
updatedAt: restartStartedAt
|
|
942
|
+
});
|
|
943
|
+
} catch (error) {
|
|
944
|
+
reportLauncherUpdate(`[LiveDesk Hub] Could not persist the restart supervisor state: ${error instanceof Error ? error.message : String(error)}.`, true);
|
|
945
|
+
}
|
|
946
|
+
const restartBootstrap = spawn(process.execPath, ['-e', buildHubRestartBootstrapScript()], {
|
|
947
|
+
env: {
|
|
948
|
+
...process.env,
|
|
949
|
+
LIVEDESK_RESTART_WAIT_PID: String(process.pid),
|
|
950
|
+
LIVEDESK_RESTART_PREVIOUS_RUNTIME_PID: String(request?.pid || 0),
|
|
951
|
+
LIVEDESK_RESTART_OPERATION_ID: operationId,
|
|
952
|
+
LIVEDESK_RESTART_STARTED_AT: restartStartedAt,
|
|
953
|
+
LIVEDESK_RESTART_RESULT_PATH: hubUpdateResultPath,
|
|
868
954
|
LIVEDESK_RESTART_COMMAND: restartInvocation.command,
|
|
869
955
|
LIVEDESK_RESTART_ARGS_BASE64: Buffer.from(JSON.stringify(restartInvocation.args), 'utf8').toString('base64'),
|
|
956
|
+
LIVEDESK_RESTART_FALLBACK_COMMAND: fallbackInvocation.command,
|
|
957
|
+
LIVEDESK_RESTART_FALLBACK_ARGS_BASE64: Buffer.from(JSON.stringify(fallbackInvocation.args), 'utf8').toString('base64'),
|
|
958
|
+
LIVEDESK_RESTART_EXPECTED_VERSION: requestedVersion,
|
|
959
|
+
LIVEDESK_RESTART_FALLBACK_VERSION: readVersion(),
|
|
960
|
+
LIVEDESK_RESTART_HEALTH_URL: hubProbeBaseUrl,
|
|
961
|
+
LIVEDESK_RESTART_REMOTE_HOST: '127.0.0.1',
|
|
962
|
+
LIVEDESK_RESTART_REMOTE_PORT: String(remotePort),
|
|
870
963
|
LIVEDESK_RESTART_CWD: process.cwd(),
|
|
871
964
|
LIVEDESK_RESTART_LOG_PATH: hubLogPath
|
|
872
|
-
},
|
|
965
|
+
},
|
|
873
966
|
stdio: 'ignore',
|
|
874
967
|
detached: true,
|
|
875
968
|
windowsHide: true
|
|
876
|
-
});
|
|
877
|
-
restartBootstrap.once('error', error => {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
969
|
+
});
|
|
970
|
+
restartBootstrap.once('error', error => {
|
|
971
|
+
reportLauncherUpdate(`[LiveDesk Hub] Failed to start the update restart supervisor: ${error.message}. Restoring the current Hub.`, true);
|
|
972
|
+
try {
|
|
973
|
+
writeJsonAtomic(hubUpdateResultPath, {
|
|
974
|
+
operationId,
|
|
975
|
+
stage: 'failed',
|
|
976
|
+
outcome: 'failed',
|
|
977
|
+
targetVersion: requestedVersion,
|
|
978
|
+
fallbackVersion: readVersion(),
|
|
979
|
+
previousRuntimePid: Number(request?.pid || 0),
|
|
980
|
+
launcherPid: process.pid,
|
|
981
|
+
startedAt: restartStartedAt,
|
|
982
|
+
updatedAt: new Date().toISOString(),
|
|
983
|
+
completedAt: new Date().toISOString(),
|
|
984
|
+
error: `The Hub restart supervisor could not start: ${error.message}. The existing launcher restarted the unchanged Hub.`
|
|
985
|
+
});
|
|
986
|
+
} catch (persistError) {
|
|
987
|
+
reportLauncherUpdate(`[LiveDesk Hub] Could not persist the supervisor failure: ${persistError instanceof Error ? persistError.message : String(persistError)}.`, true);
|
|
988
|
+
}
|
|
989
|
+
startHubProcess();
|
|
990
|
+
});
|
|
991
|
+
restartBootstrap.once('spawn', () => {
|
|
992
|
+
restartBootstrap.unref();
|
|
993
|
+
reportLauncherUpdate(`[LiveDesk Hub] Update handoff prepared target=${requestedVersion || 'latest'} supervisorPid=${restartBootstrap.pid || 0}. The current launcher will exit after its Hub stopped. Restart diagnostics: ${hubLogPath}`);
|
|
994
|
+
setTimeout(() => process.exit(0), 100);
|
|
995
|
+
});
|
|
996
|
+
};
|
|
884
997
|
|
|
885
|
-
const pollUpdateRequest = () => {
|
|
998
|
+
const pollUpdateRequest = () => {
|
|
886
999
|
if (!existsSync(updateRequestPath)) return;
|
|
887
1000
|
try {
|
|
888
1001
|
const request = JSON.parse(readFileSync(updateRequestPath, 'utf8'));
|
|
@@ -901,12 +1014,41 @@ async function runManager(args, resolvedRole = null) {
|
|
|
901
1014
|
} else if (requestPid !== activeChildPid) {
|
|
902
1015
|
reportLauncherUpdate(`[LiveDesk Hub] Ignored stale update restart request for pid ${requestPid}; active Hub pid is ${activeChildPid || 'unavailable'}.`, true);
|
|
903
1016
|
} else {
|
|
904
|
-
|
|
905
|
-
reportLauncherUpdate(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'}.`);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1017
|
+
restartRequest = { ...request, pid: requestPid };
|
|
1018
|
+
reportLauncherUpdate(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'} target=${request.latestVersion || 'latest'}.`);
|
|
1019
|
+
const updateChild = activeChild;
|
|
1020
|
+
let shutdownFallbackStarted = false;
|
|
1021
|
+
const forceStop = reason => {
|
|
1022
|
+
if (!updateChild || updateChild.exitCode !== null || updateChild.signalCode) return;
|
|
1023
|
+
if (shutdownFallbackStarted) return;
|
|
1024
|
+
shutdownFallbackStarted = true;
|
|
1025
|
+
reportLauncherUpdate(`[LiveDesk Hub] Update shutdown fallback: ${reason}; terminating Hub pid=${requestPid}.`, true);
|
|
1026
|
+
try { updateChild.kill('SIGTERM'); } catch { /* child already exited */ }
|
|
1027
|
+
updateHardStopTimer = setTimeout(() => {
|
|
1028
|
+
if (!updateChild || updateChild.exitCode !== null || updateChild.signalCode) return;
|
|
1029
|
+
reportLauncherUpdate(`[LiveDesk Hub] Update shutdown hard fallback: Hub pid=${requestPid} ignored graceful termination; force-stopping its process tree.`, true);
|
|
1030
|
+
if (process.platform === 'win32') {
|
|
1031
|
+
execFile('taskkill.exe', ['/PID', String(requestPid), '/T', '/F'], { windowsHide: true }, error => {
|
|
1032
|
+
if (error && updateChild.exitCode === null && !updateChild.signalCode) {
|
|
1033
|
+
reportLauncherUpdate(`[LiveDesk Hub] taskkill fallback failed for pid=${requestPid}: ${error.message}`, true);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
} else {
|
|
1037
|
+
try { updateChild.kill('SIGKILL'); } catch { /* child already exited */ }
|
|
1038
|
+
}
|
|
1039
|
+
}, 3_000);
|
|
1040
|
+
updateHardStopTimer.unref?.();
|
|
1041
|
+
};
|
|
1042
|
+
updateStopTimer = setTimeout(() => forceStop('graceful shutdown exceeded 10 seconds'), 10_000);
|
|
1043
|
+
updateStopTimer.unref?.();
|
|
1044
|
+
void fetch(`${hubProbeBaseUrl}/api/runtime/shutdown`, {
|
|
1045
|
+
method: 'POST',
|
|
1046
|
+
signal: AbortSignal.timeout(2_500)
|
|
1047
|
+
}).then(response => {
|
|
1048
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1049
|
+
reportLauncherUpdate(`[LiveDesk Hub] Update shutdown acknowledged by Hub pid=${requestPid}.`);
|
|
1050
|
+
}).catch(error => forceStop(`shutdown API failed (${error?.message || error})`));
|
|
1051
|
+
}
|
|
910
1052
|
} catch {
|
|
911
1053
|
// The Hub writes the request atomically; a partial/stale file is retried.
|
|
912
1054
|
}
|
|
@@ -941,18 +1083,27 @@ async function runManager(args, resolvedRole = null) {
|
|
|
941
1083
|
child.once('error', error => {
|
|
942
1084
|
console.error(`Failed to start LiveDesk Hub: ${error.message}`);
|
|
943
1085
|
process.exitCode = 1;
|
|
944
|
-
});
|
|
945
|
-
child.once('exit', (code, signal) => {
|
|
946
|
-
const startupOutput = stderrChunks.join('');
|
|
947
|
-
if (
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1086
|
+
});
|
|
1087
|
+
child.once('exit', (code, signal) => {
|
|
1088
|
+
const startupOutput = stderrChunks.join('');
|
|
1089
|
+
if (restartRequest) {
|
|
1090
|
+
const request = restartRequest;
|
|
1091
|
+
restartRequest = null;
|
|
1092
|
+
if (updateStopTimer) {
|
|
1093
|
+
clearTimeout(updateStopTimer);
|
|
1094
|
+
updateStopTimer = null;
|
|
1095
|
+
}
|
|
1096
|
+
if (updateHardStopTimer) {
|
|
1097
|
+
clearTimeout(updateHardStopTimer);
|
|
1098
|
+
updateHardStopTimer = null;
|
|
1099
|
+
}
|
|
1100
|
+
if (updatePollId) {
|
|
1101
|
+
clearInterval(updatePollId);
|
|
1102
|
+
updatePollId = null;
|
|
1103
|
+
}
|
|
1104
|
+
restartWithLatest(request);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
956
1107
|
if (!signal && code === ROLE_TRANSITION_EXIT_CODE) {
|
|
957
1108
|
if (updatePollId) {
|
|
958
1109
|
clearInterval(updatePollId);
|