livedesk 0.1.504 → 0.1.506
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 +9 -3
- package/bin/livedesk.js +147 -76
- package/bootstrap/hub-self-agent.js +243 -0
- package/client/bin/livedesk-client-fast.js +1 -1
- package/client/bin/livedesk-client-node.js +1 -1
- package/client/bin/livedesk-client.js +84 -20
- package/client/package.json +5 -5
- package/hub/package.json +1 -1
- package/hub/src/live-desk-update.js +24 -13
- package/hub/src/server.js +6 -5
- package/package.json +6 -6
- package/web/dist/assets/index-BvG_LkYd.js +188 -0
- package/web/dist/index.html +1 -1
- package/web/dist/livedesk-build-evidence.json +9 -9
- package/web/dist/assets/index-DqXhTKiG.js +0 -188
package/README.md
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
RemoteFast package for the current operating system and CPU, so Windows, macOS,
|
|
5
5
|
and Linux native binaries are no longer downloaded together.
|
|
6
6
|
|
|
7
|
-
One-command LiveDesk application. The same launcher resolves this computer's
|
|
8
|
-
Hub or Client role
|
|
7
|
+
One-command LiveDesk application. The same launcher resolves this computer's
|
|
8
|
+
Hub or Client role. A Client starts one managed Agent. A Hub starts the Hub
|
|
9
|
+
server plus one headless Self Agent, so the Hub computer also appears on the
|
|
10
|
+
wall and can be controlled like the other computers.
|
|
9
11
|
|
|
10
12
|
The `hub/`, `client/`, `runtime-core/`, and `web/` folders in this package are
|
|
11
13
|
generated copies. Edit the source packages and run
|
|
@@ -43,7 +45,11 @@ npx -y --prefer-online livedesk@latest --force-role client
|
|
|
43
45
|
npx -y --prefer-online livedesk@latest hub
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
This starts the LiveDesk Hub, opens the local screen wall,
|
|
48
|
+
This starts the LiveDesk Hub, opens the local screen wall, accepts clients, and
|
|
49
|
+
connects one local Self Agent through loopback TCP. The Self Agent reuses the
|
|
50
|
+
Hub computer's stable device identity and does not open another login or status
|
|
51
|
+
page. Hub shutdown, update, and role change drain that exact Agent tree before
|
|
52
|
+
the launcher exits or hands off.
|
|
47
53
|
The Hub UI/API listens on `127.0.0.1` by default while the client endpoint
|
|
48
54
|
continues to listen on the LAN.
|
|
49
55
|
|
package/bin/livedesk.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import net from 'node:net';
|
|
@@ -46,7 +46,11 @@ import {
|
|
|
46
46
|
readDeadWindowsOwnedProcessManifest,
|
|
47
47
|
readWindowsOwnedProcessManifest
|
|
48
48
|
} from '../client/src/runtime/windows-owned-process-manifest.js';
|
|
49
|
-
import { inspectMacDiagnosticLease } from '../diagnostics/livedesk-mac-physical-isolation.mjs';
|
|
49
|
+
import { inspectMacDiagnosticLease } from '../diagnostics/livedesk-mac-physical-isolation.mjs';
|
|
50
|
+
import {
|
|
51
|
+
buildHubSelfAgentLaunchSpec,
|
|
52
|
+
createHubSelfAgentSupervisor
|
|
53
|
+
} from '../bootstrap/hub-self-agent.js';
|
|
50
54
|
|
|
51
55
|
const require = createRequire(import.meta.url);
|
|
52
56
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -2536,9 +2540,47 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2536
2540
|
let updateStopTimer = null;
|
|
2537
2541
|
let updateHardStopTimer = null;
|
|
2538
2542
|
let launcherShutdownSignal = '';
|
|
2539
|
-
let launcherShutdownHardStopTimer = null;
|
|
2540
|
-
let runtimeShutdownWatcher = null;
|
|
2541
|
-
|
|
2543
|
+
let launcherShutdownHardStopTimer = null;
|
|
2544
|
+
let runtimeShutdownWatcher = null;
|
|
2545
|
+
let selfAgentStopPromise = null;
|
|
2546
|
+
const launcherSignalHandlers = new Map();
|
|
2547
|
+
|
|
2548
|
+
let hubSelfAgent = null;
|
|
2549
|
+
try {
|
|
2550
|
+
const clientEntry = resolve(packageRoot, 'client', 'bin', 'livedesk-client.js');
|
|
2551
|
+
if (!existsSync(clientEntry)) {
|
|
2552
|
+
throw new Error('the packaged Client runtime is missing');
|
|
2553
|
+
}
|
|
2554
|
+
hubSelfAgent = createHubSelfAgentSupervisor({
|
|
2555
|
+
launch: buildHubSelfAgentLaunchSpec({
|
|
2556
|
+
nodePath: process.execPath,
|
|
2557
|
+
clientEntry,
|
|
2558
|
+
remotePort,
|
|
2559
|
+
pairToken,
|
|
2560
|
+
deviceId: env.LIVEDESK_DEVICE_ID,
|
|
2561
|
+
deviceName: env.LIVEDESK_DEVICE_NAME,
|
|
2562
|
+
stateDir: MANAGER_STATE_DIR,
|
|
2563
|
+
runtimeOwner,
|
|
2564
|
+
baseEnv: env
|
|
2565
|
+
}),
|
|
2566
|
+
report: reportLauncherUpdate
|
|
2567
|
+
});
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
reportLauncherUpdate(
|
|
2570
|
+
`[LiveDesk Hub] Self Agent is unavailable: ${error?.message || error}. Remote Clients remain available.`,
|
|
2571
|
+
true
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
const stopHubSelfAgent = reason => {
|
|
2576
|
+
if (!hubSelfAgent) return Promise.resolve({ ok: true, skipped: true });
|
|
2577
|
+
if (!selfAgentStopPromise) {
|
|
2578
|
+
selfAgentStopPromise = hubSelfAgent.stop(reason).finally(() => {
|
|
2579
|
+
selfAgentStopPromise = null;
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
return selfAgentStopPromise;
|
|
2583
|
+
};
|
|
2542
2584
|
|
|
2543
2585
|
const removeLauncherSignalHandlers = () => {
|
|
2544
2586
|
for (const [signal, handler] of launcherSignalHandlers) {
|
|
@@ -2547,7 +2589,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2547
2589
|
launcherSignalHandlers.clear();
|
|
2548
2590
|
};
|
|
2549
2591
|
|
|
2550
|
-
const requestLauncherShutdown = signal => {
|
|
2592
|
+
const requestLauncherShutdown = signal => {
|
|
2551
2593
|
if (launcherShutdownSignal) return;
|
|
2552
2594
|
launcherShutdownSignal = signal;
|
|
2553
2595
|
restartRequest = null;
|
|
@@ -2559,15 +2601,18 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2559
2601
|
clearTimeout(updateStopTimer);
|
|
2560
2602
|
updateStopTimer = null;
|
|
2561
2603
|
}
|
|
2562
|
-
if (updateHardStopTimer) {
|
|
2604
|
+
if (updateHardStopTimer) {
|
|
2563
2605
|
clearTimeout(updateHardStopTimer);
|
|
2564
|
-
updateHardStopTimer = null;
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2606
|
+
updateHardStopTimer = null;
|
|
2607
|
+
}
|
|
2608
|
+
void stopHubSelfAgent(`launcher-${signal}`);
|
|
2609
|
+
const child = activeChild;
|
|
2610
|
+
if (!child || child.exitCode !== null || child.signalCode) {
|
|
2611
|
+
void stopHubSelfAgent(`launcher-${signal}`).finally(() => {
|
|
2612
|
+
try { hubLogStream?.end(); } catch {}
|
|
2613
|
+
process.exit(0);
|
|
2614
|
+
});
|
|
2615
|
+
return;
|
|
2571
2616
|
}
|
|
2572
2617
|
reportLauncherUpdate(
|
|
2573
2618
|
`[LiveDesk Hub] Launcher received ${signal}; requesting graceful Hub shutdown for pid=${child.pid || 'unknown'}.`
|
|
@@ -2814,16 +2859,17 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2814
2859
|
failPreflight(message);
|
|
2815
2860
|
return;
|
|
2816
2861
|
}
|
|
2817
|
-
restartRequest = {
|
|
2862
|
+
restartRequest = {
|
|
2818
2863
|
...request,
|
|
2819
2864
|
latestVersion: requestedVersion,
|
|
2820
2865
|
pid: requestPid,
|
|
2821
2866
|
operationId,
|
|
2822
2867
|
_neutralCwd: neutralCwd,
|
|
2823
2868
|
_originalCwd: UPDATE_ORIGINAL_CWD
|
|
2824
|
-
};
|
|
2825
|
-
reportLauncherUpdate(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'} target=${requestedVersion}.`);
|
|
2826
|
-
|
|
2869
|
+
};
|
|
2870
|
+
reportLauncherUpdate(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'} target=${requestedVersion}.`);
|
|
2871
|
+
void stopHubSelfAgent('hub-update');
|
|
2872
|
+
const updateChild = activeChild;
|
|
2827
2873
|
let shutdownFallbackStarted = false;
|
|
2828
2874
|
const forceStop = reason => {
|
|
2829
2875
|
if (!updateChild || updateChild.exitCode !== null || updateChild.signalCode) return;
|
|
@@ -2864,7 +2910,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2864
2910
|
}
|
|
2865
2911
|
};
|
|
2866
2912
|
|
|
2867
|
-
const startHubProcess = () => {
|
|
2913
|
+
const startHubProcess = () => {
|
|
2868
2914
|
const stderrChunks = [];
|
|
2869
2915
|
let stderrLength = 0;
|
|
2870
2916
|
const child = spawn(process.execPath, [hubEntry, ...options.forwarded], {
|
|
@@ -2872,7 +2918,22 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2872
2918
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
2873
2919
|
windowsHide: !debugConsole
|
|
2874
2920
|
});
|
|
2875
|
-
activeChild = child;
|
|
2921
|
+
activeChild = child;
|
|
2922
|
+
|
|
2923
|
+
void waitForManager(hubProbeBaseUrl).then(ok => {
|
|
2924
|
+
if (!ok
|
|
2925
|
+
|| activeChild !== child
|
|
2926
|
+
|| child.exitCode !== null
|
|
2927
|
+
|| child.signalCode
|
|
2928
|
+
|| launcherShutdownSignal
|
|
2929
|
+
|| restartRequest) return;
|
|
2930
|
+
hubSelfAgent?.start();
|
|
2931
|
+
}).catch(error => {
|
|
2932
|
+
reportLauncherUpdate(
|
|
2933
|
+
`[LiveDesk Hub] Self Agent startup wait failed: ${error?.message || error}.`,
|
|
2934
|
+
true
|
|
2935
|
+
);
|
|
2936
|
+
});
|
|
2876
2937
|
|
|
2877
2938
|
child.stdout.on('data', chunk => {
|
|
2878
2939
|
appendHubLog(chunk.toString());
|
|
@@ -2896,17 +2957,19 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2896
2957
|
});
|
|
2897
2958
|
child.once('exit', (code, signal) => {
|
|
2898
2959
|
const startupOutput = stderrChunks.join('');
|
|
2899
|
-
if (launcherShutdownSignal) {
|
|
2960
|
+
if (launcherShutdownSignal) {
|
|
2900
2961
|
if (launcherShutdownHardStopTimer) {
|
|
2901
2962
|
clearTimeout(launcherShutdownHardStopTimer);
|
|
2902
2963
|
launcherShutdownHardStopTimer = null;
|
|
2903
2964
|
}
|
|
2904
|
-
runtimeShutdownWatcher?.stop();
|
|
2905
|
-
removeLauncherSignalHandlers();
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2965
|
+
runtimeShutdownWatcher?.stop();
|
|
2966
|
+
removeLauncherSignalHandlers();
|
|
2967
|
+
void stopHubSelfAgent(`launcher-${launcherShutdownSignal}`).finally(() => {
|
|
2968
|
+
try { hubLogStream?.end(); } catch {}
|
|
2969
|
+
process.exit(0);
|
|
2970
|
+
});
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2910
2973
|
if (restartRequest) {
|
|
2911
2974
|
const request = restartRequest;
|
|
2912
2975
|
restartRequest = null;
|
|
@@ -2922,7 +2985,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2922
2985
|
clearInterval(updatePollId);
|
|
2923
2986
|
updatePollId = null;
|
|
2924
2987
|
}
|
|
2925
|
-
void restartWithLatest(request).catch(error => {
|
|
2988
|
+
void stopHubSelfAgent('hub-update').then(() => restartWithLatest(request)).catch(error => {
|
|
2926
2989
|
const message = `The Hub restart handoff failed after shutdown: ${error instanceof Error ? error.message : String(error)}`;
|
|
2927
2990
|
reportLauncherUpdate(`[LiveDesk Hub] ${message}. Restarting the unchanged Hub.`, true);
|
|
2928
2991
|
try {
|
|
@@ -2949,7 +3012,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2949
3012
|
} catch {
|
|
2950
3013
|
// The unchanged Hub still restarts even if diagnostics are read-only.
|
|
2951
3014
|
}
|
|
2952
|
-
startHubProcess();
|
|
3015
|
+
startHubProcess();
|
|
2953
3016
|
if (!updatePollId) {
|
|
2954
3017
|
updatePollId = setInterval(pollUpdateRequest, HUB_UPDATE_POLL_MS);
|
|
2955
3018
|
updatePollId.unref?.();
|
|
@@ -2957,39 +3020,44 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
2957
3020
|
});
|
|
2958
3021
|
return;
|
|
2959
3022
|
}
|
|
2960
|
-
if (!signal && code === ROLE_TRANSITION_EXIT_CODE) {
|
|
2961
|
-
if (updatePollId) {
|
|
2962
|
-
clearInterval(updatePollId);
|
|
2963
|
-
updatePollId = null;
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3023
|
+
if (!signal && code === ROLE_TRANSITION_EXIT_CODE) {
|
|
3024
|
+
if (updatePollId) {
|
|
3025
|
+
clearInterval(updatePollId);
|
|
3026
|
+
updatePollId = null;
|
|
3027
|
+
}
|
|
3028
|
+
void stopHubSelfAgent('role-transition').then(() => {
|
|
3029
|
+
try {
|
|
3030
|
+
runtimeLock?.updateRole?.('client');
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
console.error(
|
|
3033
|
+
`[LiveDesk Hub] The shared runtime lock could not transition to Client: ${error?.message || error}. `
|
|
3034
|
+
+ 'The launcher will exit so a clean invocation can recover.'
|
|
3035
|
+
);
|
|
3036
|
+
runtimeShutdownWatcher?.stop();
|
|
3037
|
+
removeLauncherSignalHandlers();
|
|
3038
|
+
try { hubLogStream?.end(); } catch {}
|
|
3039
|
+
process.exit(1);
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
writeRoleCache({
|
|
3043
|
+
role: 'client',
|
|
3044
|
+
deviceId: resolvedRole?.deviceId || process.env.LIVEDESK_DEVICE_ID || '',
|
|
3045
|
+
deviceName: resolvedRole?.deviceName || os.hostname(),
|
|
3046
|
+
source: 'supabase',
|
|
3047
|
+
isOfflineFallback: false
|
|
3048
|
+
}, { stateDir: MANAGER_STATE_DIR });
|
|
3049
|
+
console.log('[LiveDesk Hub] Role transition accepted. Starting the Client runtime.');
|
|
3050
|
+
runtimeShutdownWatcher?.stop();
|
|
3051
|
+
removeLauncherSignalHandlers();
|
|
3052
|
+
void runClient([], { ...resolvedRole, role: 'client', source: 'supabase', roleTransition: true }, runtimeLock).catch(error => {
|
|
3053
|
+
console.error(`Failed to start LiveDesk Client runtime: ${error?.message || error}`);
|
|
3054
|
+
process.exitCode = 1;
|
|
3055
|
+
});
|
|
3056
|
+
}).catch(error => {
|
|
3057
|
+
console.error(`[LiveDesk Hub] Self Agent did not stop for role transition: ${error?.message || error}`);
|
|
3058
|
+
process.exit(1);
|
|
3059
|
+
});
|
|
3060
|
+
return;
|
|
2993
3061
|
}
|
|
2994
3062
|
if (!signal
|
|
2995
3063
|
&& code !== 0
|
|
@@ -3011,11 +3079,11 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
|
|
|
3011
3079
|
});
|
|
3012
3080
|
return;
|
|
3013
3081
|
}
|
|
3014
|
-
if (signal) {
|
|
3015
|
-
process.kill(process.pid, signal);
|
|
3016
|
-
return;
|
|
3017
|
-
}
|
|
3018
|
-
process.exit(code ?? 0);
|
|
3082
|
+
if (signal) {
|
|
3083
|
+
void stopHubSelfAgent(`hub-signal-${signal}`).finally(() => process.kill(process.pid, signal));
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
void stopHubSelfAgent('hub-exited').finally(() => process.exit(code ?? 0));
|
|
3019
3087
|
});
|
|
3020
3088
|
};
|
|
3021
3089
|
|
|
@@ -3321,12 +3389,12 @@ async function main() {
|
|
|
3321
3389
|
await stopProcessesOnPorts([replacementCleanup.runtimePort, remotePort]);
|
|
3322
3390
|
}
|
|
3323
3391
|
|
|
3324
|
-
if (resolvedRole.role === 'client') {
|
|
3325
|
-
// A launcher and Agent can both die while an independently spawned
|
|
3326
|
-
// survives.
|
|
3327
|
-
// manifest generation returned by the
|
|
3328
|
-
// process-name scan participates
|
|
3329
|
-
const reclaimedManifestDrain = await drainReclaimedWindowsClientManifest(lock.reclaimedOwner);
|
|
3392
|
+
if (resolvedRole.role === 'client' || resolvedRole.role === 'hub') {
|
|
3393
|
+
// A launcher and Agent can both die while an independently spawned capture
|
|
3394
|
+
// helper survives. This applies to a normal Client and to the Hub-owned
|
|
3395
|
+
// Self Agent. Consume only the exact manifest generation returned by the
|
|
3396
|
+
// stale-lock compare-and-swap; no process-name scan participates here.
|
|
3397
|
+
const reclaimedManifestDrain = await drainReclaimedWindowsClientManifest(lock.reclaimedOwner);
|
|
3330
3398
|
const testHoldAfterDrainMs = Number(
|
|
3331
3399
|
process.env.LIVEDESK_TEST_HOLD_AFTER_RECLAIMED_WINDOWS_DRAIN_MS || 0
|
|
3332
3400
|
);
|
|
@@ -3334,9 +3402,12 @@ async function main() {
|
|
|
3334
3402
|
&& process.env.LIVEDESK_DEBUG === '1'
|
|
3335
3403
|
&& Number.isFinite(testHoldAfterDrainMs)
|
|
3336
3404
|
&& testHoldAfterDrainMs > 0) {
|
|
3337
|
-
console.log('[LiveDesk test] Holding after reclaimed manifest drain before
|
|
3338
|
-
await waitMilliseconds(Math.min(60_000, Math.round(testHoldAfterDrainMs)));
|
|
3339
|
-
}
|
|
3405
|
+
console.log('[LiveDesk test] Holding after reclaimed manifest drain before Agent startup.');
|
|
3406
|
+
await waitMilliseconds(Math.min(60_000, Math.round(testHoldAfterDrainMs)));
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
if (resolvedRole.role === 'client') {
|
|
3340
3411
|
const clientRuntimePort = normalizePort(
|
|
3341
3412
|
process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_LOCAL_RUNTIME_PORT,
|
|
3342
3413
|
DEFAULT_MANAGER_HTTP_PORT
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SELF_AGENT_SHUTDOWN_MESSAGE = 'livedesk.self-agent.shutdown';
|
|
5
|
+
const SELF_AGENT_READY_MESSAGE = 'livedesk.self-agent.ready';
|
|
6
|
+
|
|
7
|
+
function normalizeText(value) {
|
|
8
|
+
return String(value || '').trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizePort(value) {
|
|
12
|
+
const port = Number(value);
|
|
13
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildHubSelfAgentLaunchSpec({
|
|
17
|
+
nodePath = process.execPath,
|
|
18
|
+
clientEntry,
|
|
19
|
+
remotePort,
|
|
20
|
+
pairToken,
|
|
21
|
+
deviceId,
|
|
22
|
+
deviceName,
|
|
23
|
+
stateDir,
|
|
24
|
+
runtimeOwner = null,
|
|
25
|
+
baseEnv = process.env
|
|
26
|
+
} = {}) {
|
|
27
|
+
const normalizedNodePath = normalizeText(nodePath);
|
|
28
|
+
const normalizedClientEntry = normalizeText(clientEntry);
|
|
29
|
+
const normalizedRemotePort = normalizePort(remotePort);
|
|
30
|
+
const normalizedPairToken = normalizeText(pairToken);
|
|
31
|
+
const normalizedDeviceId = normalizeText(deviceId);
|
|
32
|
+
const normalizedDeviceName = normalizeText(deviceName);
|
|
33
|
+
const normalizedStateDir = normalizeText(stateDir);
|
|
34
|
+
if (!normalizedNodePath) throw new Error('Hub Self Agent requires the current Node executable.');
|
|
35
|
+
if (!normalizedClientEntry) throw new Error('Hub Self Agent requires the packaged Client entry.');
|
|
36
|
+
if (!normalizedRemotePort) throw new Error('Hub Self Agent requires a valid local Hub Client port.');
|
|
37
|
+
if (!normalizedPairToken) throw new Error('Hub Self Agent requires the current Hub pair token.');
|
|
38
|
+
if (!normalizedDeviceId) throw new Error('Hub Self Agent requires the Hub device identity.');
|
|
39
|
+
|
|
40
|
+
const manager = `127.0.0.1:${normalizedRemotePort}`;
|
|
41
|
+
const env = {
|
|
42
|
+
...baseEnv,
|
|
43
|
+
LIVEDESK_HUB_SELF_AGENT: '1',
|
|
44
|
+
LIVEDESK_NPM_LAUNCHER_NAME: 'livedesk',
|
|
45
|
+
LIVEDESK_NPM_LAUNCHER_VERSION: String(
|
|
46
|
+
baseEnv.LIVEDESK_MANAGER_VERSION || baseEnv.LIVEDESK_NPM_LAUNCHER_VERSION || ''
|
|
47
|
+
),
|
|
48
|
+
LIVEDESK_UNIFIED_RUNTIME: '1',
|
|
49
|
+
LIVEDESK_RUNTIME_ROLE: 'client',
|
|
50
|
+
LIVEDESK_ROLE_SOURCE: 'hub-self-agent',
|
|
51
|
+
LIVEDESK_SKIP_BROWSER_OPEN: '1',
|
|
52
|
+
LIVEDESK_CLIENT_ENGINE: 'fast',
|
|
53
|
+
LIVEDESK_CLIENT_REQUIRE_FAST: '1',
|
|
54
|
+
LIVEDESK_CLIENT_TRANSPORT: 'tcp',
|
|
55
|
+
LIVEDESK_CLIENT_MANAGER: manager,
|
|
56
|
+
LIVEDESK_CLIENT_PAIR_TOKEN: normalizedPairToken,
|
|
57
|
+
LIVEDESK_DEVICE_ID: normalizedDeviceId,
|
|
58
|
+
LIVEDESK_DEVICE_NAME: normalizedDeviceName,
|
|
59
|
+
LIVEDESK_CLIENT_NAME: normalizedDeviceName,
|
|
60
|
+
LIVEDESK_STATE_DIR: normalizedStateDir,
|
|
61
|
+
LIVEDESK_CLIENT_UPDATE_STATE_PATH: normalizedStateDir
|
|
62
|
+
? join(normalizedStateDir, 'hub-self-agent-update.json')
|
|
63
|
+
: '',
|
|
64
|
+
LIVEDESK_RUNTIME_OWNER_PID: String(runtimeOwner?.pid || ''),
|
|
65
|
+
LIVEDESK_RUNTIME_OWNER_TOKEN: String(runtimeOwner?.ownerToken || ''),
|
|
66
|
+
LIVEDESK_RUNTIME_OWNER_INSTANCE_MARKER: String(runtimeOwner?.ownerInstanceMarker || ''),
|
|
67
|
+
LIVEDESK_RUNTIME_OWNER_START_ORDER: String(runtimeOwner?.ownerStartOrder || '')
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
command: normalizedNodePath,
|
|
72
|
+
args: [
|
|
73
|
+
normalizedClientEntry,
|
|
74
|
+
'--no-login',
|
|
75
|
+
'--no-open',
|
|
76
|
+
'--fast',
|
|
77
|
+
'--transport',
|
|
78
|
+
'tcp',
|
|
79
|
+
'--device-id',
|
|
80
|
+
normalizedDeviceId
|
|
81
|
+
],
|
|
82
|
+
options: {
|
|
83
|
+
env,
|
|
84
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
85
|
+
windowsHide: true
|
|
86
|
+
},
|
|
87
|
+
manager
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createHubSelfAgentSupervisor({
|
|
92
|
+
launch,
|
|
93
|
+
spawnImpl = spawn,
|
|
94
|
+
report = () => undefined,
|
|
95
|
+
restartBaseDelayMs = 1_000,
|
|
96
|
+
restartMaxDelayMs = 8_000,
|
|
97
|
+
shutdownTimeoutMs = 12_000
|
|
98
|
+
} = {}) {
|
|
99
|
+
if (!launch || typeof launch !== 'object') {
|
|
100
|
+
throw new TypeError('Hub Self Agent launch specification is required.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let child = null;
|
|
104
|
+
let restartTimer = null;
|
|
105
|
+
let stopping = false;
|
|
106
|
+
let consecutiveFailures = 0;
|
|
107
|
+
let stopPromise = null;
|
|
108
|
+
|
|
109
|
+
const clearRestartTimer = () => {
|
|
110
|
+
if (!restartTimer) return;
|
|
111
|
+
clearTimeout(restartTimer);
|
|
112
|
+
restartTimer = null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const scheduleRestart = () => {
|
|
116
|
+
if (stopping || restartTimer) return;
|
|
117
|
+
consecutiveFailures += 1;
|
|
118
|
+
const delayMs = Math.min(
|
|
119
|
+
Math.max(100, Number(restartMaxDelayMs) || 8_000),
|
|
120
|
+
Math.max(100, Number(restartBaseDelayMs) || 1_000) * (2 ** Math.min(3, consecutiveFailures - 1))
|
|
121
|
+
);
|
|
122
|
+
report(`[LiveDesk Hub] Self Agent stopped unexpectedly; retrying in ${delayMs}ms.`, true);
|
|
123
|
+
restartTimer = setTimeout(() => {
|
|
124
|
+
restartTimer = null;
|
|
125
|
+
start();
|
|
126
|
+
}, delayMs);
|
|
127
|
+
restartTimer.unref?.();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const start = () => {
|
|
131
|
+
stopping = false;
|
|
132
|
+
clearRestartTimer();
|
|
133
|
+
if (child && child.exitCode === null && !child.signalCode) return false;
|
|
134
|
+
|
|
135
|
+
let spawned;
|
|
136
|
+
try {
|
|
137
|
+
spawned = spawnImpl(launch.command, launch.args, launch.options);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
report(`[LiveDesk Hub] Self Agent launch failed: ${error?.message || error}.`, true);
|
|
140
|
+
scheduleRestart();
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
child = spawned;
|
|
144
|
+
let settled = false;
|
|
145
|
+
|
|
146
|
+
spawned.stdout?.on?.('data', chunk => process.stdout.write(chunk));
|
|
147
|
+
spawned.stderr?.on?.('data', chunk => process.stderr.write(chunk));
|
|
148
|
+
spawned.on?.('message', message => {
|
|
149
|
+
if (message?.type !== SELF_AGENT_READY_MESSAGE) return;
|
|
150
|
+
consecutiveFailures = 0;
|
|
151
|
+
report(`[LiveDesk Hub] Self Agent ready pid=${spawned.pid || 'unknown'} manager=${launch.manager}.`);
|
|
152
|
+
});
|
|
153
|
+
spawned.once?.('error', error => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
if (child === spawned) child = null;
|
|
157
|
+
report(`[LiveDesk Hub] Self Agent process error: ${error?.message || error}.`, true);
|
|
158
|
+
scheduleRestart();
|
|
159
|
+
});
|
|
160
|
+
spawned.once?.('exit', (code, signal) => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
if (child === spawned) child = null;
|
|
164
|
+
if (!stopping) {
|
|
165
|
+
report(
|
|
166
|
+
`[LiveDesk Hub] Self Agent exited code=${code ?? 'none'} signal=${signal || 'none'}.`,
|
|
167
|
+
true
|
|
168
|
+
);
|
|
169
|
+
scheduleRestart();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
report(`[LiveDesk Hub] Starting Self Agent pid=${spawned.pid || 'pending'} manager=${launch.manager}.`);
|
|
173
|
+
return true;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const stop = (reason = 'hub-stopping') => {
|
|
177
|
+
stopping = true;
|
|
178
|
+
clearRestartTimer();
|
|
179
|
+
if (stopPromise) return stopPromise;
|
|
180
|
+
const ownedChild = child;
|
|
181
|
+
if (!ownedChild || ownedChild.exitCode !== null || ownedChild.signalCode) {
|
|
182
|
+
child = null;
|
|
183
|
+
return Promise.resolve({ ok: true, alreadyStopped: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
stopPromise = new Promise(resolveStop => {
|
|
187
|
+
let finished = false;
|
|
188
|
+
let hardStopTimer = null;
|
|
189
|
+
const finish = result => {
|
|
190
|
+
if (finished) return;
|
|
191
|
+
finished = true;
|
|
192
|
+
if (hardStopTimer) clearTimeout(hardStopTimer);
|
|
193
|
+
if (child === ownedChild) child = null;
|
|
194
|
+
stopPromise = null;
|
|
195
|
+
resolveStop(result);
|
|
196
|
+
};
|
|
197
|
+
ownedChild.once?.('exit', (code, signal) => finish({ ok: true, code, signal }));
|
|
198
|
+
try {
|
|
199
|
+
ownedChild.send?.({
|
|
200
|
+
type: SELF_AGENT_SHUTDOWN_MESSAGE,
|
|
201
|
+
signal: 'SIGTERM',
|
|
202
|
+
reason: normalizeText(reason).slice(0, 120)
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
report(`[LiveDesk Hub] Self Agent shutdown message failed: ${error?.message || error}.`, true);
|
|
206
|
+
}
|
|
207
|
+
hardStopTimer = setTimeout(() => {
|
|
208
|
+
if (ownedChild.exitCode !== null || ownedChild.signalCode) {
|
|
209
|
+
finish({ ok: true });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
report(
|
|
213
|
+
`[LiveDesk Hub] Self Agent pid=${ownedChild.pid || 'unknown'} exceeded graceful shutdown; force-stopping that exact child root.`,
|
|
214
|
+
true
|
|
215
|
+
);
|
|
216
|
+
try {
|
|
217
|
+
ownedChild.kill('SIGKILL');
|
|
218
|
+
} catch (error) {
|
|
219
|
+
finish({ ok: false, error });
|
|
220
|
+
}
|
|
221
|
+
}, Math.max(1_000, Number(shutdownTimeoutMs) || 12_000));
|
|
222
|
+
hardStopTimer.unref?.();
|
|
223
|
+
});
|
|
224
|
+
return stopPromise;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
start,
|
|
229
|
+
stop,
|
|
230
|
+
getStatus: () => ({
|
|
231
|
+
running: !!child && child.exitCode === null && !child.signalCode,
|
|
232
|
+
pid: Number(child?.pid || 0),
|
|
233
|
+
stopping,
|
|
234
|
+
restartScheduled: !!restartTimer,
|
|
235
|
+
consecutiveFailures
|
|
236
|
+
})
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const HUB_SELF_AGENT_MESSAGES = Object.freeze({
|
|
241
|
+
shutdown: SELF_AGENT_SHUTDOWN_MESSAGE,
|
|
242
|
+
ready: SELF_AGENT_READY_MESSAGE
|
|
243
|
+
});
|