@livedesk/client 0.1.214 → 0.1.216

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.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import net from 'net';
4
4
  import os from 'os';
@@ -7,259 +7,263 @@ import crypto from 'crypto';
7
7
  import { existsSync, promises as fs, statfsSync } from 'fs';
8
8
  import { spawn } from 'child_process';
9
9
  import { createRequire } from 'node:module';
10
+ import { fileURLToPath } from 'node:url';
10
11
 
11
12
  const require = createRequire(import.meta.url);
12
- const AGENT_VERSION = (() => {
13
+ const CLIENT_UPDATE_BOOTSTRAP_PATH = fileURLToPath(
14
+ new URL('./livedesk-client-update-bootstrap.cjs', import.meta.url)
15
+ );
16
+ const AGENT_VERSION = (() => {
13
17
  try {
14
18
  return String(require('../package.json').version || '0.0.0');
15
19
  } catch {
16
20
  return '0.0.0';
17
- }
18
- })();
19
- const PRODUCT_VERSION = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || AGENT_VERSION).trim() || AGENT_VERSION;
20
- const DEFAULT_MANAGER = '127.0.0.1:5197';
21
+ }
22
+ })();
23
+ const PRODUCT_VERSION = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || AGENT_VERSION).trim() || AGENT_VERSION;
24
+ const DEFAULT_MANAGER = '127.0.0.1:5197';
21
25
  const DEFAULT_HEARTBEAT_MS = 5000;
22
- const DEFAULT_RECONNECT_MS = 5000;
23
- const EXIT_INVALID_PAIR_TOKEN = 23;
24
- const EXIT_CLIENT_UPDATE = 42;
25
- const DEFAULT_LIVE_FPS = 30;
26
- const MAX_LIVE_FPS = 30;
27
- const MAX_FRAME_BASE64_CHARS = 3 * 1024 * 1024;
28
- const MAX_FILE_TRANSFER_FILES = 24;
26
+ const DEFAULT_RECONNECT_MS = 5000;
27
+ const EXIT_INVALID_PAIR_TOKEN = 23;
28
+ const EXIT_CLIENT_UPDATE = 42;
29
+ const DEFAULT_LIVE_FPS = 30;
30
+ const MAX_LIVE_FPS = 30;
31
+ const MAX_FRAME_BASE64_CHARS = 3 * 1024 * 1024;
32
+ const MAX_FILE_TRANSFER_FILES = 24;
29
33
  const MAX_FILE_TRANSFER_BYTES = 24 * 1024 * 1024;
30
34
  const MAX_AGENT_OUTPUT_CHARS = 32000;
31
- const launchedProcessRegistry = new Map();
32
- const CLIENT_UPDATE_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
33
-
34
- function normalizeClientUpdateTargetVersion(value) {
35
- const version = String(value || '').trim().replace(/^v/i, '');
36
- if (!version) return '';
37
- if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
38
- throw new Error(`Invalid LiveDesk product update version: ${version}`);
39
- }
40
- return version;
41
- }
42
-
43
- function getClientUpdateStatePath() {
44
- return path.resolve(
45
- String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'))
46
- );
47
- }
48
-
49
- function getClientUpdateNeutralCwd(operationId) {
50
- const safeOperationId = String(operationId || '')
51
- .replace(/[^A-Za-z0-9._-]/g, '_')
52
- .slice(0, 120) || 'unknown';
53
- return path.join(os.tmpdir(), 'livedesk-update-cwd', `client-${safeOperationId}`);
54
- }
55
-
56
- async function prepareClientUpdateNeutralCwd(operationId) {
57
- const neutralCwd = path.resolve(getClientUpdateNeutralCwd(operationId));
58
- await fs.mkdir(neutralCwd, { recursive: true });
59
- const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
60
- if (unexpectedEntry) {
61
- const shadowPath = path.join(neutralCwd, unexpectedEntry);
62
- throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
63
- }
64
- return neutralCwd;
65
- }
66
-
67
- function buildClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
68
- const env = { ...baseEnv };
69
- const isolatedNames = new Set([
70
- 'init_cwd',
71
- 'npm_config_local_prefix',
72
- 'npm_config_workspace',
73
- 'npm_config_workspaces',
74
- 'npm_config_include_workspace_root',
75
- 'npm_package_json',
76
- 'npm_lifecycle_event',
77
- 'npm_lifecycle_script'
78
- ]);
79
- for (const key of Object.keys(env)) {
80
- if (isolatedNames.has(key.toLowerCase())) delete env[key];
81
- }
82
- env.INIT_CWD = neutralCwd;
83
- env.npm_config_local_prefix = neutralCwd;
84
- env.npm_config_workspaces = 'false';
85
- env.npm_config_include_workspace_root = 'false';
86
- return env;
87
- }
88
-
89
- function isClientUpdatePidAlive(value) {
90
- const pid = Number(value);
91
- if (!Number.isInteger(pid) || pid <= 1) return false;
92
- try {
93
- process.kill(pid, 0);
94
- return true;
95
- } catch (error) {
96
- return error?.code === 'EPERM';
97
- }
98
- }
99
-
100
- function canReclaimClientUpdateOperation(previous) {
101
- const handoffOwners = [
102
- previous?.supervisorPid,
103
- previous?.starterPid
104
- ].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
105
- if (handoffOwners.length > 0) {
106
- return handoffOwners.every(pid => !isClientUpdatePidAlive(pid));
107
- }
108
- if (String(previous?.stage || '') === 'scheduled') {
109
- const schedulingOwners = [
110
- previous?.agentPid,
111
- previous?.launcherPid
112
- ].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
113
- return schedulingOwners.length > 0
114
- && schedulingOwners.every(pid => !isClientUpdatePidAlive(pid));
115
- }
116
- return false;
117
- }
118
-
119
- async function writeClientUpdateState(stage, details = {}) {
120
- const statePath = getClientUpdateStatePath();
121
- const operationId = String(details.operationId || process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
122
- if (!operationId) return;
123
- const attemptId = stage === 'scheduled'
124
- ? ''
125
- : String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
126
- let previous = null;
127
- try {
128
- previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
129
- } catch {
130
- // The first update stage creates the file.
131
- }
132
- const replacesTerminalOperation = previous?.operationId
133
- && previous.operationId !== operationId
134
- && stage === 'scheduled'
135
- && ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
136
- const reclaimsDeadOperation = previous?.operationId
137
- && previous.operationId !== operationId
138
- && stage === 'scheduled'
139
- && canReclaimClientUpdateOperation(previous);
140
- const replacesPreviousOperation = replacesTerminalOperation || reclaimsDeadOperation;
141
- if (previous?.operationId && previous.operationId !== operationId && !replacesPreviousOperation) return;
142
- if (!replacesPreviousOperation
143
- && previous?.attemptId
144
- && attemptId
145
- && previous.attemptId !== attemptId) {
146
- return;
147
- }
148
- if (!replacesPreviousOperation
149
- && previous?.stage === 'connected'
150
- && previous?.restartVerified === true
151
- && stage !== 'connected') {
152
- return;
153
- }
154
- const targetProductVersion = String(
155
- details.targetProductVersion
156
- || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
157
- || (!replacesPreviousOperation ? previous?.targetProductVersion : '')
158
- || ''
159
- ).trim();
160
- const targetAgentVersion = String(
161
- details.targetAgentVersion
162
- || details.targetVersion
163
- || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
164
- || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
165
- || (!replacesPreviousOperation ? previous?.targetAgentVersion : '')
166
- || (!replacesPreviousOperation ? previous?.targetVersion : '')
167
- || ''
168
- ).trim();
169
- const state = {
170
- ...(!replacesPreviousOperation && previous && typeof previous === 'object' ? previous : {}),
171
- operationId,
172
- stage,
173
- targetVersion: targetAgentVersion,
174
- targetProductVersion,
175
- targetAgentVersion,
176
- productVersion: PRODUCT_VERSION,
177
- agentVersion: AGENT_VERSION,
178
- agentPid: process.pid,
179
- launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
180
- attemptId,
181
- updatedAt: new Date().toISOString(),
182
- ...details
183
- };
184
- await fs.mkdir(path.dirname(statePath), { recursive: true });
185
- const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
186
- try {
187
- await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
188
- let current = null;
189
- try {
190
- current = JSON.parse(await fs.readFile(statePath, 'utf8'));
191
- } catch {
192
- // A missing first-write state is still owned by this transition.
193
- }
194
- if (String(current?.operationId || '') !== String(previous?.operationId || '')
195
- || String(current?.stage || '') !== String(previous?.stage || '')
196
- || String(current?.updatedAt || '') !== String(previous?.updatedAt || '')) {
197
- return;
198
- }
199
- await fs.rename(temporaryPath, statePath);
200
- } finally {
201
- await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
202
- }
203
- }
204
-
205
- async function waitForClientUpdateShutdownSignal(
206
- operationId,
207
- updateDeadlineEpochMs,
208
- timeoutMs = 180_000
209
- ) {
210
- const statePath = getClientUpdateStatePath();
211
- const deadline = Math.min(
212
- Date.now() + timeoutMs,
213
- Number(updateDeadlineEpochMs) || 0
214
- );
215
- while (Date.now() < deadline) {
216
- try {
217
- const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
218
- if (state?.operationId !== operationId) {
219
- await wait(200);
220
- continue;
221
- }
222
- if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') {
223
- return { ready: true, error: '' };
224
- }
225
- if (state.stage === 'failed') {
226
- const error = String(state.error || 'LiveDesk client update preparation failed.');
227
- console.error(`LiveDesk client update preparation failed: ${error}`);
228
- return { ready: false, error };
229
- }
230
- } catch {
231
- // The detached supervisor may still be writing its first stage.
232
- }
233
- await wait(200);
234
- }
235
- const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
236
- const error = deadlineExpired
237
- ? 'The absolute LiveDesk Client update deadline expired before shutdown.'
238
- : 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.';
239
- await writeClientUpdateState('failed', {
240
- operationId,
241
- error,
242
- cancelRequested: true,
243
- deadlineExpired,
244
- failedAt: new Date().toISOString()
245
- }).catch(() => undefined);
246
- console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
247
- return { ready: false, error };
248
- }
249
-
250
- function markClientUpdateConnected() {
251
- const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
252
- if (!operationId) return;
253
- void writeClientUpdateState('connected', {
254
- operationId,
255
- attemptId: String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
256
- connectedAt: new Date().toISOString(),
257
- restartVerified: true,
258
- error: ''
259
- }).catch(error => {
260
- console.error(`LiveDesk client update diagnostics could not be persisted: ${error?.message || error}`);
261
- });
262
- }
35
+ const launchedProcessRegistry = new Map();
36
+ const CLIENT_UPDATE_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
37
+
38
+ function normalizeClientUpdateTargetVersion(value) {
39
+ const version = String(value || '').trim().replace(/^v/i, '');
40
+ if (!version) return '';
41
+ if (!CLIENT_UPDATE_VERSION_PATTERN.test(version)) {
42
+ throw new Error(`Invalid LiveDesk product update version: ${version}`);
43
+ }
44
+ return version;
45
+ }
46
+
47
+ function getClientUpdateStatePath() {
48
+ return path.resolve(
49
+ String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'))
50
+ );
51
+ }
52
+
53
+ function getClientUpdateNeutralCwd(operationId) {
54
+ const safeOperationId = String(operationId || '')
55
+ .replace(/[^A-Za-z0-9._-]/g, '_')
56
+ .slice(0, 120) || 'unknown';
57
+ return path.join(os.tmpdir(), 'livedesk-update-cwd', `client-${safeOperationId}`);
58
+ }
59
+
60
+ async function prepareClientUpdateNeutralCwd(operationId) {
61
+ const neutralCwd = path.resolve(getClientUpdateNeutralCwd(operationId));
62
+ await fs.mkdir(neutralCwd, { recursive: true });
63
+ const unexpectedEntry = (await fs.readdir(neutralCwd))[0];
64
+ if (unexpectedEntry) {
65
+ const shadowPath = path.join(neutralCwd, unexpectedEntry);
66
+ throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
67
+ }
68
+ return neutralCwd;
69
+ }
70
+
71
+ function buildClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
72
+ const env = { ...baseEnv };
73
+ const isolatedNames = new Set([
74
+ 'init_cwd',
75
+ 'npm_config_local_prefix',
76
+ 'npm_config_workspace',
77
+ 'npm_config_workspaces',
78
+ 'npm_config_include_workspace_root',
79
+ 'npm_package_json',
80
+ 'npm_lifecycle_event',
81
+ 'npm_lifecycle_script'
82
+ ]);
83
+ for (const key of Object.keys(env)) {
84
+ if (isolatedNames.has(key.toLowerCase())) delete env[key];
85
+ }
86
+ env.INIT_CWD = neutralCwd;
87
+ env.npm_config_local_prefix = neutralCwd;
88
+ env.npm_config_workspaces = 'false';
89
+ env.npm_config_include_workspace_root = 'false';
90
+ return env;
91
+ }
92
+
93
+ function isClientUpdatePidAlive(value) {
94
+ const pid = Number(value);
95
+ if (!Number.isInteger(pid) || pid <= 1) return false;
96
+ try {
97
+ process.kill(pid, 0);
98
+ return true;
99
+ } catch (error) {
100
+ return error?.code === 'EPERM';
101
+ }
102
+ }
103
+
104
+ function canReclaimClientUpdateOperation(previous) {
105
+ const handoffOwners = [
106
+ previous?.supervisorPid,
107
+ previous?.starterPid
108
+ ].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
109
+ if (handoffOwners.length > 0) {
110
+ return handoffOwners.every(pid => !isClientUpdatePidAlive(pid));
111
+ }
112
+ if (String(previous?.stage || '') === 'scheduled') {
113
+ const schedulingOwners = [
114
+ previous?.agentPid,
115
+ previous?.launcherPid
116
+ ].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
117
+ return schedulingOwners.length > 0
118
+ && schedulingOwners.every(pid => !isClientUpdatePidAlive(pid));
119
+ }
120
+ return false;
121
+ }
122
+
123
+ async function writeClientUpdateState(stage, details = {}) {
124
+ const statePath = getClientUpdateStatePath();
125
+ const operationId = String(details.operationId || process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
126
+ if (!operationId) return;
127
+ const attemptId = stage === 'scheduled'
128
+ ? ''
129
+ : String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
130
+ let previous = null;
131
+ try {
132
+ previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
133
+ } catch {
134
+ // The first update stage creates the file.
135
+ }
136
+ const replacesTerminalOperation = previous?.operationId
137
+ && previous.operationId !== operationId
138
+ && stage === 'scheduled'
139
+ && ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
140
+ const reclaimsDeadOperation = previous?.operationId
141
+ && previous.operationId !== operationId
142
+ && stage === 'scheduled'
143
+ && canReclaimClientUpdateOperation(previous);
144
+ const replacesPreviousOperation = replacesTerminalOperation || reclaimsDeadOperation;
145
+ if (previous?.operationId && previous.operationId !== operationId && !replacesPreviousOperation) return;
146
+ if (!replacesPreviousOperation
147
+ && previous?.attemptId
148
+ && attemptId
149
+ && previous.attemptId !== attemptId) {
150
+ return;
151
+ }
152
+ if (!replacesPreviousOperation
153
+ && previous?.stage === 'connected'
154
+ && previous?.restartVerified === true
155
+ && stage !== 'connected') {
156
+ return;
157
+ }
158
+ const targetProductVersion = String(
159
+ details.targetProductVersion
160
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
161
+ || (!replacesPreviousOperation ? previous?.targetProductVersion : '')
162
+ || ''
163
+ ).trim();
164
+ const targetAgentVersion = String(
165
+ details.targetAgentVersion
166
+ || details.targetVersion
167
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
168
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
169
+ || (!replacesPreviousOperation ? previous?.targetAgentVersion : '')
170
+ || (!replacesPreviousOperation ? previous?.targetVersion : '')
171
+ || ''
172
+ ).trim();
173
+ const state = {
174
+ ...(!replacesPreviousOperation && previous && typeof previous === 'object' ? previous : {}),
175
+ operationId,
176
+ stage,
177
+ targetVersion: targetAgentVersion,
178
+ targetProductVersion,
179
+ targetAgentVersion,
180
+ productVersion: PRODUCT_VERSION,
181
+ agentVersion: AGENT_VERSION,
182
+ agentPid: process.pid,
183
+ launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
184
+ attemptId,
185
+ updatedAt: new Date().toISOString(),
186
+ ...details
187
+ };
188
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
189
+ const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
190
+ try {
191
+ await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
192
+ let current = null;
193
+ try {
194
+ current = JSON.parse(await fs.readFile(statePath, 'utf8'));
195
+ } catch {
196
+ // A missing first-write state is still owned by this transition.
197
+ }
198
+ if (String(current?.operationId || '') !== String(previous?.operationId || '')
199
+ || String(current?.stage || '') !== String(previous?.stage || '')
200
+ || String(current?.updatedAt || '') !== String(previous?.updatedAt || '')) {
201
+ return;
202
+ }
203
+ await fs.rename(temporaryPath, statePath);
204
+ } finally {
205
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
206
+ }
207
+ }
208
+
209
+ async function waitForClientUpdateShutdownSignal(
210
+ operationId,
211
+ updateDeadlineEpochMs,
212
+ timeoutMs = 180_000
213
+ ) {
214
+ const statePath = getClientUpdateStatePath();
215
+ const deadline = Math.min(
216
+ Date.now() + timeoutMs,
217
+ Number(updateDeadlineEpochMs) || 0
218
+ );
219
+ while (Date.now() < deadline) {
220
+ try {
221
+ const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
222
+ if (state?.operationId !== operationId) {
223
+ await wait(200);
224
+ continue;
225
+ }
226
+ if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') {
227
+ return { ready: true, error: '' };
228
+ }
229
+ if (state.stage === 'failed') {
230
+ const error = String(state.error || 'LiveDesk client update preparation failed.');
231
+ console.error(`LiveDesk client update preparation failed: ${error}`);
232
+ return { ready: false, error };
233
+ }
234
+ } catch {
235
+ // The detached supervisor may still be writing its first stage.
236
+ }
237
+ await wait(200);
238
+ }
239
+ const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
240
+ const error = deadlineExpired
241
+ ? 'The absolute LiveDesk Client update deadline expired before shutdown.'
242
+ : 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.';
243
+ await writeClientUpdateState('failed', {
244
+ operationId,
245
+ error,
246
+ cancelRequested: true,
247
+ deadlineExpired,
248
+ failedAt: new Date().toISOString()
249
+ }).catch(() => undefined);
250
+ console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
251
+ return { ready: false, error };
252
+ }
253
+
254
+ function markClientUpdateConnected() {
255
+ const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
256
+ if (!operationId) return;
257
+ void writeClientUpdateState('connected', {
258
+ operationId,
259
+ attemptId: String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
260
+ connectedAt: new Date().toISOString(),
261
+ restartVerified: true,
262
+ error: ''
263
+ }).catch(error => {
264
+ console.error(`LiveDesk client update diagnostics could not be persisted: ${error?.message || error}`);
265
+ });
266
+ }
263
267
 
264
268
  function printHelp() {
265
269
  console.log(`
@@ -279,10 +283,10 @@ Options:
279
283
  --no-thumbnail Disable thumbnail capture capability.
280
284
  --live Enable focused live screen streaming. Default on.
281
285
  --no-live Disable focused live screen streaming.
282
- --tasks Enable safe remote task inbox. Default on.
283
- --no-tasks Disable remote task dispatch capability.
284
- --files-dir <path> Default folder for received files. Default: ~/Desktop/LiveDeskFiles.
285
- --fake-thumbnail Use generated thumbnail frames for smoke tests.
286
+ --tasks Enable safe remote task inbox. Default on.
287
+ --no-tasks Disable remote task dispatch capability.
288
+ --files-dir <path> Default folder for received files. Default: ~/Desktop/LiveDeskFiles.
289
+ --fake-thumbnail Use generated thumbnail frames for smoke tests.
286
290
  --exit-on-disconnect Exit after Hub disconnect. Useful for tests.
287
291
  --exit-on-invalid-pair Exit when the Hub rejects the pair token.
288
292
  --once Connect, send one status packet, and exit after welcome.
@@ -322,10 +326,10 @@ function parseArgs(argv) {
322
326
  heartbeatMs: DEFAULT_HEARTBEAT_MS,
323
327
  deviceId: '',
324
328
  thumbnailEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_THUMBNAIL ?? process.env.MINDEXEC_REMOTE_THUMBNAIL),
325
- liveEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_LIVE ?? process.env.MINDEXEC_REMOTE_LIVE),
326
- taskEnabled: !isFalsy(process.env.LIVEDESK_CLIENT_TASKS ?? process.env.MINDEXEC_REMOTE_TASKS),
327
- filesDir: process.env.LIVEDESK_CLIENT_FILES_DIR || process.env.MINDEXEC_REMOTE_FILES_DIR || '',
328
- fakeThumbnail: isTruthy(process.env.LIVEDESK_CLIENT_FAKE_THUMBNAIL || process.env.MINDEXEC_REMOTE_FAKE_THUMBNAIL),
329
+ liveEnabled: defaultDesktopCaptureEnabled && !isFalsy(process.env.LIVEDESK_CLIENT_LIVE ?? process.env.MINDEXEC_REMOTE_LIVE),
330
+ taskEnabled: !isFalsy(process.env.LIVEDESK_CLIENT_TASKS ?? process.env.MINDEXEC_REMOTE_TASKS),
331
+ filesDir: process.env.LIVEDESK_CLIENT_FILES_DIR || process.env.MINDEXEC_REMOTE_FILES_DIR || '',
332
+ fakeThumbnail: isTruthy(process.env.LIVEDESK_CLIENT_FAKE_THUMBNAIL || process.env.MINDEXEC_REMOTE_FAKE_THUMBNAIL),
329
333
  exitOnDisconnect: false,
330
334
  exitOnInvalidPair: false,
331
335
  once: false,
@@ -377,11 +381,11 @@ function parseArgs(argv) {
377
381
  case '--no-tasks':
378
382
  result.taskEnabled = false;
379
383
  break;
380
- case '--files-dir':
381
- result.filesDir = args[++index] || result.filesDir;
382
- break;
383
- case '--fake-thumbnail':
384
- result.fakeThumbnail = true;
384
+ case '--files-dir':
385
+ result.filesDir = args[++index] || result.filesDir;
386
+ break;
387
+ case '--fake-thumbnail':
388
+ result.fakeThumbnail = true;
385
389
  result.thumbnailEnabled = true;
386
390
  break;
387
391
  case '--exit-on-disconnect':
@@ -561,7 +565,7 @@ function getStatus(options = {}) {
561
565
  usedMemRatio,
562
566
  platform: os.platform(),
563
567
  release: os.release(),
564
- role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
568
+ role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
565
569
  cpu: {
566
570
  cores: cpuCores,
567
571
  model: cpus[0]?.model || '',
@@ -584,9 +588,9 @@ function getStatus(options = {}) {
584
588
  gpu: acceleratorStatus.gpu,
585
589
  npu: acceleratorStatus.npu,
586
590
  network: getNetworkStatus(),
587
- workload: {
588
- role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
589
- status: 'idle',
591
+ workload: {
592
+ role: options.taskEnabled ? 'Command-ready Client' : 'Local machine',
593
+ status: 'idle',
590
594
  title: 'idle'
591
595
  },
592
596
  timestamp: new Date().toISOString()
@@ -916,17 +920,17 @@ async function captureThumbnailFrame(options, payload, frameSeq) {
916
920
  return await captureScreenFrame(options, payload, frameSeq, 'thumbnail');
917
921
  }
918
922
 
919
- const RETIRED_CLIENT_AGENT_TASK_ERROR = 'agent-ai-assist-retired';
920
-
921
- function getRetiredClientAgentTaskError(payload = {}) {
922
- const source = payload && typeof payload === 'object' ? payload : {};
923
- const approvalLevel = String(source.approvalLevel || '').trim().toLowerCase();
924
- const hasModelSelection = ['model', 'aiModel', 'aiProvider', 'provider']
925
- .some(key => Object.prototype.hasOwnProperty.call(source, key));
926
- return approvalLevel === 'ai-assist' || hasModelSelection
927
- ? RETIRED_CLIENT_AGENT_TASK_ERROR
928
- : '';
929
- }
923
+ const RETIRED_CLIENT_AGENT_TASK_ERROR = 'agent-ai-assist-retired';
924
+
925
+ function getRetiredClientAgentTaskError(payload = {}) {
926
+ const source = payload && typeof payload === 'object' ? payload : {};
927
+ const approvalLevel = String(source.approvalLevel || '').trim().toLowerCase();
928
+ const hasModelSelection = ['model', 'aiModel', 'aiProvider', 'provider']
929
+ .some(key => Object.prototype.hasOwnProperty.call(source, key));
930
+ return approvalLevel === 'ai-assist' || hasModelSelection
931
+ ? RETIRED_CLIENT_AGENT_TASK_ERROR
932
+ : '';
933
+ }
930
934
 
931
935
  function clampInteger(value, min, max, fallback) {
932
936
  const number = Number(value);
@@ -1372,15 +1376,15 @@ async function executeNodeAgentOperation(options, operation, payload = {}) {
1372
1376
  restart: { executable: 'shutdown.exe', args: ['/r', '/t', delaySec] },
1373
1377
  shutdown: { executable: 'shutdown.exe', args: ['/s', '/t', delaySec] }
1374
1378
  }[action] || {});
1375
- } else if (process.platform === 'darwin') {
1376
- ({ executable, args: powerArgs } = {
1377
- lock: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to keystroke "q" using {control down, command down}'] },
1378
- sleep: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to sleep'] },
1379
- logoff: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to log out'] },
1380
- restart: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to restart'] },
1381
- shutdown: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to shut down'] }
1382
- }[action] || {});
1383
- } else {
1379
+ } else if (process.platform === 'darwin') {
1380
+ ({ executable, args: powerArgs } = {
1381
+ lock: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to keystroke "q" using {control down, command down}'] },
1382
+ sleep: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to sleep'] },
1383
+ logoff: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to log out'] },
1384
+ restart: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to restart'] },
1385
+ shutdown: { executable: '/usr/bin/osascript', args: ['-e', 'tell application "System Events" to shut down'] }
1386
+ }[action] || {});
1387
+ } else {
1384
1388
  ({ executable, args: powerArgs } = {
1385
1389
  lock: { executable: 'loginctl', args: ['lock-session'] },
1386
1390
  sleep: { executable: 'systemctl', args: ['suspend'] },
@@ -1432,11 +1436,11 @@ function remotePolicyAllows(options, command) {
1432
1436
  const policy = options.effectivePolicy;
1433
1437
  if (!policy || typeof policy !== 'object') return { ok: true };
1434
1438
  if (policy.accessMode === 'block-remote-access') return { ok: false, error: 'remote-access-blocked-by-settings' };
1435
- const required = command === 'input.control'
1436
- ? ['allowControl']
1437
- : command === 'system.power'
1438
- ? ['allowPowerActions']
1439
- : command.startsWith('file.transfer')
1439
+ const required = command === 'input.control'
1440
+ ? ['allowControl']
1441
+ : command === 'system.power'
1442
+ ? ['allowPowerActions']
1443
+ : command.startsWith('file.transfer')
1440
1444
  ? ['allowFileTransfer']
1441
1445
  : command === 'audio.start'
1442
1446
  ? ['allowRemoteAudio']
@@ -1449,192 +1453,139 @@ function remotePolicyAllows(options, command) {
1449
1453
  return denied ? { ok: false, error: `${denied}-blocked-by-settings` } : { ok: true };
1450
1454
  }
1451
1455
 
1452
- function buildClientUpdateBootstrapScript() {
1453
- return [
1454
- 'const { execFile, spawn } = require("node:child_process");',
1455
- 'const fs = require("node:fs");',
1456
- 'const path = require("node:path");',
1457
- 'const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();',
1458
- 'const originalCwd = path.resolve(String(process.env.LIVEDESK_UPDATE_ORIGINAL_CWD || process.cwd()));',
1459
- 'const statePath = path.resolve(originalCwd, String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(require("node:os").homedir(), ".livedesk", "client-update.json")));',
1460
- 'const neutralCwdValue = String(process.env.LIVEDESK_UPDATE_NEUTRAL_CWD || "").trim();',
1461
- 'const neutralCwd = neutralCwdValue ? path.resolve(neutralCwdValue) : "";',
1462
- 'const cleanVersion = value => String(value || "").trim().replace(/^v/i, "");',
1463
- 'const targetProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION);',
1464
- 'const updateDeadlineEpochMs = Number(process.env.LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS || 0);',
1465
- 'const deadlineExpired = () => !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs;',
1466
- 'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
1467
- 'const readState = () => { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return {}; } };',
1468
- 'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
1469
- 'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
1470
- 'const isGroupAlive = pid => { try { process.kill(-pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
1471
- 'const runWindowsPowerShell = script => new Promise((resolve, reject) => { execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 15000, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error(String(stderr || error.message || "PowerShell failed").trim())); return; } resolve(String(stdout || "").trim()); }); });',
1472
- 'const queryWindowsProcessTable = async () => { const stdout = await runWindowsPowerShell(`$ErrorActionPreference = "Stop"; $records = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object { $ticks = if ($_.CreationDate) { [string]$_.CreationDate.ToUniversalTime().Ticks } else { "" }; [pscustomobject]@{ ProcessId = [int]$_.ProcessId; ParentProcessId = [int]$_.ParentProcessId; CreationUtcTicks = $ticks } }); [pscustomobject]@{ Records = $records } | ConvertTo-Json -Compress -Depth 4`); if (!stdout) throw new Error("Windows process snapshot returned no data"); const parsed = JSON.parse(stdout); const values = parsed?.Records == null ? [] : (Array.isArray(parsed.Records) ? parsed.Records : [parsed.Records]); return values.map(item => ({ pid: Number(item?.ProcessId || 0), parentPid: Number(item?.ParentProcessId || 0), startOrder: String(item?.CreationUtcTicks || "") })).filter(item => Number.isInteger(item.pid) && item.pid > 1 && /^\\d+$/.test(item.startOrder)); };',
1473
- 'const queryTrackedWindowsProcessTable = child => { if (child.livedeskWindowsSnapshotPromise) return child.livedeskWindowsSnapshotPromise; const pending = queryWindowsProcessTable(); child.livedeskWindowsSnapshotPromise = pending; const clear = () => { if (child.livedeskWindowsSnapshotPromise === pending) child.livedeskWindowsSnapshotPromise = null; }; pending.then(clear, clear); return pending; };',
1474
- 'const sameWindowsIdentity = (left, right) => Number(left?.pid || 0) === Number(right?.pid || 0) && String(left?.startOrder || "") === String(right?.startOrder || "");',
1475
- 'const captureWindowsIdentity = async child => { const processId = Number(child?.pid || 0); const deadline = Date.now() + 2500; let lastError = null; do { try { const snapshot = await queryTrackedWindowsProcessTable(child); const identity = snapshot.find(item => item.pid === processId) || null; if (identity) return identity; } catch (error) { lastError = error; } if (Date.now() < deadline) await sleep(50); } while (Date.now() < deadline); throw new Error(`Could not capture immutable Windows CreationDate for pid=${processId}: ${lastError?.message || "process not visible"}`); };',
1476
- 'const windowsTicksAtMilliseconds = value => BigInt(Math.trunc(Number(value) || Date.now())) * 10000n + 621355968000000000n;',
1477
- 'const setWindowsRootLifetimeEnd = (child, ticks) => { const candidate = BigInt(ticks); const previous = child.livedeskWindowsRootLifetimeEndTicks; if (!previous || candidate < BigInt(previous)) child.livedeskWindowsRootLifetimeEndTicks = String(candidate); return BigInt(child.livedeskWindowsRootLifetimeEndTicks); };',
1478
- 'const mergeRootAbsentWindowsTree = (child, rootPid, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const exactCurrentByPid = new Map(); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } const lowerBound = windowsTicksAtMilliseconds(Number(child.livedeskWindowsSpawnedAtMs || Date.now()) - 2000); const upperBound = child.livedeskWindowsRootLifetimeEndTicks ? BigInt(child.livedeskWindowsRootLifetimeEndTicks) : null; let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === rootPid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < lowerBound || (upperBound && recordStart > upperBound)) continue; } catch { continue; } const parent = record.parentPid === rootPid ? { depth: 0 } : exactCurrentByPid.get(record.parentPid); if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };',
1479
- 'const mergeExactWindowsTree = (child, root, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const currentRoot = currentByPid.get(root.pid) || null; const rootIsExact = sameWindowsIdentity(currentRoot, root); const rootWasReused = Boolean(currentRoot && !rootIsExact); if (rootWasReused) setWindowsRootLifetimeEnd(child, BigInt(currentRoot.startOrder) - 1n); else if (!currentRoot && child.livedeskWindowsRootExitedAtMs) setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); const rootLifetimeClosed = Boolean(child.livedeskWindowsRootLifetimeEndTicks); if (rootIsExact) tracked.set(`${root.pid}:${root.startOrder}`, { ...root, depth: 0 }); const exactCurrentByPid = new Map(); if (rootIsExact) exactCurrentByPid.set(root.pid, { ...root, depth: 0 }); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === root.pid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < BigInt(root.startOrder)) continue; if (rootLifetimeClosed && recordStart > BigInt(child.livedeskWindowsRootLifetimeEndTicks)) continue; } catch { continue; } let parent = exactCurrentByPid.get(record.parentPid) || null; if (!parent && !currentRoot && rootLifetimeClosed && record.parentPid === root.pid) parent = { ...root, depth: 0 }; if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };',
1480
- 'const refreshTrackedWindowsTree = async (child, root = child.livedeskWindowsIdentity || null) => { const snapshot = await queryTrackedWindowsProcessTable(child); if (root) mergeExactWindowsTree(child, root, snapshot); else mergeRootAbsentWindowsTree(child, Number(child.pid || 0), snapshot); return snapshot; };',
1481
- '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); };',
1482
- 'const trackWindowsChild = child => { if (process.platform !== "win32" || Number(child?.pid || 0) <= 1 || child.livedeskWindowsIdentityPromise) return; child.livedeskWindowsTrackedRecords = new Map(); child.livedeskWindowsSpawnedAtMs = Date.now(); child.livedeskWindowsRootExitedAtMs = null; child.livedeskWindowsTrackingStopped = false; child.once("exit", () => { if (!child.livedeskWindowsRootExitedAtMs) child.livedeskWindowsRootExitedAtMs = Date.now(); setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); }); const refresh = () => { void refreshTrackedWindowsTree(child).then(() => { child.livedeskWindowsTrackingError = null; }).catch(error => { child.livedeskWindowsTrackingError = error; }); }; refresh(); child.livedeskWindowsTrackingTimer = setInterval(refresh, 250); child.livedeskWindowsTrackingTimer.unref?.(); child.livedeskWindowsIdentityPromise = (async () => { while (!child.livedeskWindowsTrackingStopped && !child.livedeskWindowsRootExitedAtMs) { try { const identity = await captureWindowsIdentity(child); child.livedeskWindowsIdentity = identity; refresh(); return identity; } catch (error) { child.livedeskWindowsIdentityError = error; if (!child.livedeskWindowsTrackingStopped && !child.livedeskWindowsRootExitedAtMs) await sleep(250); } } return null; })(); };',
1483
- 'const stopWindowsTracking = child => { child.livedeskWindowsTrackingStopped = true; if (child?.livedeskWindowsTrackingTimer) clearInterval(child.livedeskWindowsTrackingTimer); child.livedeskWindowsTrackingTimer = null; };',
1484
- 'const terminateExactWindowsTrackedTree = (child, root) => { if (child.livedeskWindowsDrainPromise) return child.livedeskWindowsDrainPromise; child.livedeskWindowsDrainPromise = (async () => { let attempt = 0; let emptyPasses = 0; for (;;) { attempt += 1; try { const snapshot = await refreshTrackedWindowsTree(child, root); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const remaining = [...child.livedeskWindowsTrackedRecords.values()].filter(target => sameWindowsIdentity(currentByPid.get(target.pid), target)).sort((left, right) => Number(right.depth || 0) - Number(left.depth || 0)); if (remaining.length === 0) { emptyPasses += 1; if (emptyPasses >= 2) { stopWindowsTracking(child); return; } } else { emptyPasses = 0; await stopExactWindowsRecords(remaining); } child.livedeskWindowsTrackingError = null; } catch (error) { emptyPasses = 0; child.livedeskWindowsTrackingError = error; if (attempt === 1 || attempt % 10 === 0) process.stderr.write(`LiveDesk update cleanup is retrying exact Windows process-tree drain: ${error?.message || error}\\n`); } await sleep(Math.min(1000, 100 + (attempt * 50))); } })(); return child.livedeskWindowsDrainPromise; };',
1485
- 'const stateLockPath = statePath + ".lock"; const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));',
1486
- 'const waitForStateLock = ms => Atomics.wait(lockWaitBuffer, 0, 0, Math.max(1, ms));',
1487
- 'const removeAbandonedStateLock = () => { let owner = null; try { owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); } catch {} const ownerPid = Number(owner?.pid || 0); if (Number.isInteger(ownerPid) && ownerPid > 1) { if (isAlive(ownerPid)) return false; } else { try { if (Date.now() - fs.statSync(stateLockPath).mtimeMs < 5000) return false; } catch { return true; } } try { fs.rmSync(stateLockPath); return true; } catch { return false; } };',
1488
- 'const withStateLock = callback => { fs.mkdirSync(path.dirname(stateLockPath), { recursive: true }); const deadline = Date.now() + 10000; let descriptor = null; let token = ""; while (descriptor === null) { try { descriptor = fs.openSync(stateLockPath, "wx", 0o600); token = process.pid + "-" + Date.now() + "-" + Math.random().toString(16).slice(2); fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token, acquiredAt: new Date().toISOString() }), "utf8"); } catch (error) { if (descriptor !== null) { try { fs.closeSync(descriptor); } catch {} descriptor = null; try { fs.rmSync(stateLockPath, { force: true }); } catch {} } if (error?.code !== "EEXIST") throw error; if (removeAbandonedStateLock()) continue; if (Date.now() >= deadline) throw new Error("Timed out waiting for the LiveDesk update state lock: " + stateLockPath); waitForStateLock(Math.min(25, Math.max(1, deadline - Date.now()))); } } try { return callback(); } finally { try { fs.closeSync(descriptor); } catch {} try { const owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); if (owner?.token === token && Number(owner?.pid) === process.pid) fs.rmSync(stateLockPath, { force: true }); } catch {} } };',
1489
- 'const writeFailure = (error, cancelRequested = false) => { try { withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown" || previous.stage === "connected" || previous.stage === "restored") return; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: cancelRequested || previous.cancelRequested === true, error: String(error?.message || error).slice(0, 4000), failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); }); } catch {} process.stderr.write("LiveDesk update handoff failed: " + (error?.message || error) + "\\n"); process.exitCode = 1; };',
1490
- 'const writeCancellation = error => withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return "superseded"; if (previous.operationId !== operationId) return false; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown") return "ready"; if (previous.stage === "failed") return previous.cancelRequested === true ? "cancelled" : "failed"; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: true, error: String(error?.message || error).slice(0, 4000), cancelledAt: now, failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".cancel.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return "cancelled"; });',
1491
- 'const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; if (process.platform === "win32") { const root = child.livedeskWindowsIdentity || await child.livedeskWindowsIdentityPromise; if (!root && !child.livedeskWindowsRootExitedAtMs) { child.livedeskWindowsTrackingError = child.livedeskWindowsIdentityError || new Error(`No immutable Windows identity is available for pid=${pid}`); while (!child.livedeskWindowsRootExitedAtMs && !child.livedeskWindowsIdentity) await sleep(250); } await terminateExactWindowsTrackedTree(child, child.livedeskWindowsIdentity || root || null); 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)) { try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } } };',
1492
- 'const writeHandoffStarted = () => withStateLock(() => { if (deadlineExpired()) throw new Error("The absolute LiveDesk Client update deadline expired before handoff."); const previous = readState(); if (previous.operationId !== operationId) throw new Error("LiveDesk update handoff was superseded before exact-package launch."); const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "handoff-started", starterPid: process.pid, updateDeadlineEpochMs, restartVerified: false, error: "", updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff-started.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); });',
1493
- 'const monitorPreflight = child => { let settled = false; let draining = false; let drainPromise = null; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timeout); stopWindowsTracking(child); child.unref(); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const drainAndFinish = (error, preserveFailure = false) => { if (settled) return Promise.resolve(); draining = true; if (!drainPromise) drainPromise = terminateTree(child).then(() => finish(error, preserveFailure)); return drainPromise; }; const inspect = () => { if (draining) return; const state = readState(); if (state.operationId !== operationId) return; if (state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") finish(); else if (state.stage === "failed") void drainAndFinish(null, true); }; const cancelTimedOutHandoff = async () => { const error = new Error(deadlineExpired() ? "The absolute LiveDesk Client update deadline expired before exact-package preflight." : "Timed out waiting for exact-package update preflight."); draining = true; while (!settled) { let outcome; try { outcome = writeCancellation(error); } catch (lockError) { await drainAndFinish(lockError); return; } if (outcome === "ready") { draining = false; inspect(); return; } if (outcome === "failed" || outcome === "cancelled" || outcome === "superseded") { await drainAndFinish(null, true); return; } await sleep(25); } }; const poll = setInterval(inspect, 100); const timeoutMs = Math.max(1, Math.min(Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000)), updateDeadlineEpochMs - Date.now())); const timeout = setTimeout(() => { void cancelTimedOutHandoff().catch(error => { void drainAndFinish(error); }); }, timeoutMs); child.once("exit", (code, signal) => { inspect(); if (settled || draining) return; const error = new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ")."); void drainAndFinish(error); }); inspect(); };',
1494
- 'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk update neutral working directory is unavailable"); fs.mkdirSync(neutralCwd, { recursive: true }); const unexpectedEntry = fs.readdirSync(neutralCwd)[0]; if (unexpectedEntry) { const shadow = path.join(neutralCwd, unexpectedEntry); throw new Error("LiveDesk update neutral working directory is shadowed by " + shadow + ": " + neutralCwd); } };',
1495
- 'if (!operationId || !versionPattern.test(targetProductVersion) || deadlineExpired()) { writeFailure(new Error("Invalid or expired LiveDesk exact-package update handoff."), true); } else { try { prepareNeutralCwd(); writeHandoffStarted();',
1496
- 'const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) }; 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";',
1497
- 'const nodeDir = path.dirname(process.execPath);',
1498
- 'const npxCli = [env.LIVEDESK_NPX_CLI_PATH, env.npm_execpath ? path.join(path.dirname(env.npm_execpath), "npx-cli.js") : "", env.LIVEDESK_NPX_EXECUTABLE ? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), "node_modules", "npm", "bin", "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].find(value => value && fs.existsSync(value));',
1499
- 'const npx = env.LIVEDESK_NPX_EXECUTABLE || (process.platform === "win32" ? "npx.cmd" : "npx");',
1500
- 'const args = ["-y", "--prefer-online", "--prefix", neutralCwd, "--workspaces=false", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
1501
- 'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
1502
- 'const invocation = npxCli ? { command: process.execPath, args: [npxCli, ...args] } : process.platform === "win32" ? { command: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + args.map(quoteCmd).join(" ")] } : { command: npx, args };',
1503
- 'if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { cwd: neutralCwd, env, detached: true, stdio: "ignore", windowsHide: true }); trackWindowsChild(child); child.once("error", writeFailure); child.once("spawn", () => { trackWindowsChild(child); monitorPreflight(child); }); } catch (error) { writeFailure(error); } }',
1504
- '} catch (error) { writeFailure(error, deadlineExpired()); } }',
1505
- ''
1506
- ].join('\n');
1507
- }
1508
-
1509
- async function scheduleClientUpdate(options, payload = {}) {
1510
- const operationId = String(payload.operationId || crypto.randomUUID()).trim();
1511
- const statePath = getClientUpdateStatePath();
1512
- let targetProductVersion = '';
1513
- let targetAgentVersion = '';
1514
- let updateDeadlineEpochMs = 0;
1515
- try {
1516
- targetProductVersion = normalizeClientUpdateTargetVersion(
1517
- payload.targetProductVersion
1518
- || payload.productVersion
1519
- || payload.managerVersion
1520
- || payload.targetVersion
1521
- || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
1522
- || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1523
- || ''
1524
- );
1525
- targetAgentVersion = normalizeClientUpdateTargetVersion(
1526
- payload.targetAgentVersion
1527
- || payload.clientVersion
1528
- || payload.targetVersion
1529
- || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
1530
- || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1531
- || targetProductVersion
1532
- || ''
1533
- );
1534
- const updateTimeoutMs = Number(payload.updateTimeoutMs);
1535
- if (!Number.isSafeInteger(updateTimeoutMs)
1536
- || updateTimeoutMs < 1_000
1537
- || updateTimeoutMs > 90 * 60_000) {
1538
- throw new Error('LiveDesk Client update requires a bounded deadline budget.');
1539
- }
1540
- updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
1541
- const originalCwd = path.resolve(process.cwd());
1542
- const neutralCwd = await prepareClientUpdateNeutralCwd(operationId);
1543
- await writeClientUpdateState('scheduled', {
1544
- operationId,
1545
- targetVersion: targetAgentVersion,
1546
- targetProductVersion,
1547
- targetAgentVersion,
1548
- updateTimeoutMs,
1549
- updateDeadlineEpochMs,
1550
- requestedAt: new Date().toISOString(),
1551
- restartVerified: false,
1552
- error: ''
1553
- });
1554
- const configuredParentPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || process.ppid);
1555
- const parentPid = Number.isInteger(configuredParentPid) && configuredParentPid > 1
1556
- ? configuredParentPid
1557
- : process.pid;
1558
- if (!Number.isInteger(parentPid) || parentPid <= 1) {
1559
- throw new Error('LiveDesk client launcher process id is unavailable.');
1560
- }
1561
- const nodeBinDir = path.dirname(process.execPath);
1562
- const npxCandidates = process.platform === 'win32'
1563
- ? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx.cmd'), path.join(nodeBinDir, 'npx.exe'), 'npx.cmd']
1564
- : [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
1565
- const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
1566
- if (!command) {
1567
- throw new Error('LiveDesk npx executable was not found.');
1568
- }
1569
- const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
1570
- ? command
1571
- : path.resolve(originalCwd, command);
1572
- const configuredLocalLauncher = String(
1573
- process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
1574
- || process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
1575
- || ''
1576
- ).trim();
1577
- const localLauncher = configuredLocalLauncher
1578
- ? path.resolve(originalCwd, configuredLocalLauncher)
1579
- : '';
1580
- const updateEnvironment = buildClientUpdateNpxEnvironment({
1581
- ...process.env,
1582
- LIVEDESK_NODE_EXECUTABLE: process.execPath,
1583
- LIVEDESK_NPX_EXECUTABLE: frozenCommand,
1584
- LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
1585
- LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
1586
- LIVEDESK_SKIP_BROWSER_OPEN: '1',
1587
- LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
1588
- LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
1589
- LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
1590
- LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
1591
- LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
1592
- LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: localLauncher,
1593
- LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
1594
- LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS: String(updateDeadlineEpochMs),
1595
- LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
1596
- LIVEDESK_UPDATE_NEUTRAL_CWD: neutralCwd,
1597
- LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
1598
- LIVEDESK_CLIENT_UPDATE_CWD: originalCwd
1599
- }, neutralCwd);
1600
- return await new Promise((resolve, reject) => {
1601
- const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
1602
- cwd: neutralCwd,
1603
- env: updateEnvironment,
1604
- detached: true,
1605
- stdio: 'ignore',
1606
- windowsHide: true
1607
- });
1608
- child.once('error', reject);
1609
- child.once('spawn', () => {
1610
- child.unref();
1611
- resolve({
1612
- ok: true,
1613
- status: 'update-scheduled',
1614
- targetVersion: targetAgentVersion,
1615
- targetProductVersion,
1616
- targetAgentVersion,
1617
- operationId,
1618
- updateDeadlineEpochMs,
1619
- statePath,
1620
- parentPid,
1621
- restartAfterMs: 0
1622
- });
1623
- });
1624
- });
1625
- } catch (error) {
1626
- await writeClientUpdateState('failed', {
1627
- operationId,
1628
- targetVersion: targetAgentVersion,
1629
- targetProductVersion,
1630
- targetAgentVersion,
1631
- error: error?.message || String(error),
1632
- failedAt: new Date().toISOString(),
1633
- restartVerified: false
1634
- }).catch(() => undefined);
1635
- throw error;
1636
- }
1637
- }
1456
+ async function scheduleClientUpdate(options, payload = {}) {
1457
+ const operationId = String(payload.operationId || crypto.randomUUID()).trim();
1458
+ const statePath = getClientUpdateStatePath();
1459
+ let targetProductVersion = '';
1460
+ let targetAgentVersion = '';
1461
+ let updateDeadlineEpochMs = 0;
1462
+ try {
1463
+ targetProductVersion = normalizeClientUpdateTargetVersion(
1464
+ payload.targetProductVersion
1465
+ || payload.productVersion
1466
+ || payload.managerVersion
1467
+ || payload.targetVersion
1468
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
1469
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1470
+ || ''
1471
+ );
1472
+ targetAgentVersion = normalizeClientUpdateTargetVersion(
1473
+ payload.targetAgentVersion
1474
+ || payload.clientVersion
1475
+ || payload.targetVersion
1476
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
1477
+ || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
1478
+ || targetProductVersion
1479
+ || ''
1480
+ );
1481
+ const updateTimeoutMs = Number(payload.updateTimeoutMs);
1482
+ if (!Number.isSafeInteger(updateTimeoutMs)
1483
+ || updateTimeoutMs < 1_000
1484
+ || updateTimeoutMs > 90 * 60_000) {
1485
+ throw new Error('LiveDesk Client update requires a bounded deadline budget.');
1486
+ }
1487
+ updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
1488
+ const originalCwd = path.resolve(process.cwd());
1489
+ const neutralCwd = await prepareClientUpdateNeutralCwd(operationId);
1490
+ await writeClientUpdateState('scheduled', {
1491
+ operationId,
1492
+ targetVersion: targetAgentVersion,
1493
+ targetProductVersion,
1494
+ targetAgentVersion,
1495
+ updateTimeoutMs,
1496
+ updateDeadlineEpochMs,
1497
+ requestedAt: new Date().toISOString(),
1498
+ restartVerified: false,
1499
+ error: ''
1500
+ });
1501
+ const configuredParentPid = Number(process.env.LIVEDESK_CLIENT_PARENT_PID || process.ppid);
1502
+ const parentPid = Number.isInteger(configuredParentPid) && configuredParentPid > 1
1503
+ ? configuredParentPid
1504
+ : process.pid;
1505
+ if (!Number.isInteger(parentPid) || parentPid <= 1) {
1506
+ throw new Error('LiveDesk client launcher process id is unavailable.');
1507
+ }
1508
+ const nodeBinDir = path.dirname(process.execPath);
1509
+ const npxCandidates = process.platform === 'win32'
1510
+ ? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx.cmd'), path.join(nodeBinDir, 'npx.exe'), 'npx.cmd']
1511
+ : [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeBinDir, 'npx'), 'npx'];
1512
+ const command = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || existsSync(candidate)));
1513
+ if (!command) {
1514
+ throw new Error('LiveDesk npx executable was not found.');
1515
+ }
1516
+ const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
1517
+ ? command
1518
+ : path.resolve(originalCwd, command);
1519
+ const configuredLocalLauncher = String(
1520
+ process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
1521
+ || process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
1522
+ || ''
1523
+ ).trim();
1524
+ const localLauncher = configuredLocalLauncher
1525
+ ? path.resolve(originalCwd, configuredLocalLauncher)
1526
+ : '';
1527
+ const updateEnvironment = buildClientUpdateNpxEnvironment({
1528
+ ...process.env,
1529
+ LIVEDESK_NODE_EXECUTABLE: process.execPath,
1530
+ LIVEDESK_NPX_EXECUTABLE: frozenCommand,
1531
+ LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
1532
+ LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
1533
+ LIVEDESK_SKIP_BROWSER_OPEN: '1',
1534
+ LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
1535
+ LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
1536
+ LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
1537
+ LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
1538
+ LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
1539
+ LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: localLauncher,
1540
+ LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
1541
+ LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS: String(updateDeadlineEpochMs),
1542
+ LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
1543
+ LIVEDESK_UPDATE_NEUTRAL_CWD: neutralCwd,
1544
+ LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
1545
+ LIVEDESK_CLIENT_UPDATE_CWD: originalCwd
1546
+ }, neutralCwd);
1547
+ const bootstrapStat = await fs.stat(CLIENT_UPDATE_BOOTSTRAP_PATH);
1548
+ if (!bootstrapStat.isFile() || bootstrapStat.size <= 0) {
1549
+ throw new Error(`LiveDesk Client update bootstrap is unavailable: ${CLIENT_UPDATE_BOOTSTRAP_PATH}`);
1550
+ }
1551
+ return await new Promise((resolve, reject) => {
1552
+ const child = spawn(process.execPath, [CLIENT_UPDATE_BOOTSTRAP_PATH], {
1553
+ cwd: neutralCwd,
1554
+ env: updateEnvironment,
1555
+ detached: true,
1556
+ stdio: 'ignore',
1557
+ windowsHide: true
1558
+ });
1559
+ child.once('error', reject);
1560
+ child.once('spawn', () => {
1561
+ child.unref();
1562
+ resolve({
1563
+ ok: true,
1564
+ status: 'update-scheduled',
1565
+ targetVersion: targetAgentVersion,
1566
+ targetProductVersion,
1567
+ targetAgentVersion,
1568
+ operationId,
1569
+ updateDeadlineEpochMs,
1570
+ statePath,
1571
+ parentPid,
1572
+ restartAfterMs: 0
1573
+ });
1574
+ });
1575
+ });
1576
+ } catch (error) {
1577
+ await writeClientUpdateState('failed', {
1578
+ operationId,
1579
+ targetVersion: targetAgentVersion,
1580
+ targetProductVersion,
1581
+ targetAgentVersion,
1582
+ error: error?.message || String(error),
1583
+ failedAt: new Date().toISOString(),
1584
+ restartVerified: false
1585
+ }).catch(() => undefined);
1586
+ throw error;
1587
+ }
1588
+ }
1638
1589
 
1639
1590
  async function handleRemoteCommand(socket, options, message, nextFrameSeq, activeStreams) {
1640
1591
  const command = String(message.command || '');
@@ -1661,37 +1612,37 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
1661
1612
  return;
1662
1613
  }
1663
1614
 
1664
- if (command === 'livedesk.client-update') {
1665
- try {
1666
- const result = await scheduleClientUpdate(options, message.payload || {});
1615
+ if (command === 'livedesk.client-update') {
1616
+ try {
1617
+ const result = await scheduleClientUpdate(options, message.payload || {});
1667
1618
  writeJsonLine(socket, {
1668
1619
  type: 'command.result',
1669
- commandId: message.commandId,
1670
- result
1671
- });
1672
- const shutdownSignal = await waitForClientUpdateShutdownSignal(
1673
- result.operationId,
1674
- result.updateDeadlineEpochMs
1675
- );
1676
- if (shutdownSignal.ready) {
1677
- // The exact package supervisor is the sole owner of old-runtime
1678
- // termination, so a deadline can never race a voluntary exit.
1679
- return;
1680
- } else {
1681
- writeJsonLine(socket, {
1682
- type: 'command.result',
1683
- commandId: message.commandId,
1684
- error: shutdownSignal.error,
1685
- result: {
1686
- ok: false,
1687
- status: 'failed',
1688
- error: shutdownSignal.error,
1689
- operationId: result.operationId,
1690
- completedAt: new Date().toISOString()
1691
- }
1692
- });
1693
- }
1694
- } catch (error) {
1620
+ commandId: message.commandId,
1621
+ result
1622
+ });
1623
+ const shutdownSignal = await waitForClientUpdateShutdownSignal(
1624
+ result.operationId,
1625
+ result.updateDeadlineEpochMs
1626
+ );
1627
+ if (shutdownSignal.ready) {
1628
+ // The exact package supervisor is the sole owner of old-runtime
1629
+ // termination, so a deadline can never race a voluntary exit.
1630
+ return;
1631
+ } else {
1632
+ writeJsonLine(socket, {
1633
+ type: 'command.result',
1634
+ commandId: message.commandId,
1635
+ error: shutdownSignal.error,
1636
+ result: {
1637
+ ok: false,
1638
+ status: 'failed',
1639
+ error: shutdownSignal.error,
1640
+ operationId: result.operationId,
1641
+ completedAt: new Date().toISOString()
1642
+ }
1643
+ });
1644
+ }
1645
+ } catch (error) {
1695
1646
  writeJsonLine(socket, {
1696
1647
  type: 'command.result',
1697
1648
  commandId: message.commandId,
@@ -1745,25 +1696,25 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
1745
1696
  error: 'remote task capability is disabled'
1746
1697
  });
1747
1698
  return;
1748
- }
1749
-
1750
- const payload = message.payload || {};
1751
- const retiredTaskError = getRetiredClientAgentTaskError(payload);
1752
- if (retiredTaskError) {
1753
- writeJsonLine(socket, {
1754
- type: 'command.result',
1755
- commandId: message.commandId,
1756
- error: retiredTaskError,
1757
- result: {
1758
- ok: false,
1759
- status: 'failed',
1760
- error: retiredTaskError,
1761
- completedAt: new Date().toISOString()
1762
- }
1763
- });
1764
- return;
1765
- }
1766
- const instruction = String(payload.instruction || '').replace(/\0/g, '').trim().slice(0, 4000);
1699
+ }
1700
+
1701
+ const payload = message.payload || {};
1702
+ const retiredTaskError = getRetiredClientAgentTaskError(payload);
1703
+ if (retiredTaskError) {
1704
+ writeJsonLine(socket, {
1705
+ type: 'command.result',
1706
+ commandId: message.commandId,
1707
+ error: retiredTaskError,
1708
+ result: {
1709
+ ok: false,
1710
+ status: 'failed',
1711
+ error: retiredTaskError,
1712
+ completedAt: new Date().toISOString()
1713
+ }
1714
+ });
1715
+ return;
1716
+ }
1717
+ const instruction = String(payload.instruction || '').replace(/\0/g, '').trim().slice(0, 4000);
1767
1718
  if (!instruction) {
1768
1719
  writeJsonLine(socket, {
1769
1720
  type: 'command.result',
@@ -1773,13 +1724,13 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
1773
1724
  return;
1774
1725
  }
1775
1726
 
1776
- const completedAt = new Date().toISOString();
1777
- const title = String(payload.title || instruction.split(/\r?\n/)[0] || 'Remote task')
1778
- .replace(/[\r\n\t]/g, ' ')
1779
- .trim()
1780
- .slice(0, 120);
1781
-
1782
- writeJsonLine(socket, {
1727
+ const completedAt = new Date().toISOString();
1728
+ const title = String(payload.title || instruction.split(/\r?\n/)[0] || 'Remote task')
1729
+ .replace(/[\r\n\t]/g, ' ')
1730
+ .trim()
1731
+ .slice(0, 120);
1732
+
1733
+ writeJsonLine(socket, {
1783
1734
  type: 'command.result',
1784
1735
  commandId: message.commandId,
1785
1736
  result: {
@@ -2004,10 +1955,10 @@ function connectOnce(options, deviceId) {
2004
1955
  hostname: os.hostname(),
2005
1956
  platform: os.platform(),
2006
1957
  arch: os.arch(),
2007
- pid: process.pid,
2008
- agentVersion: AGENT_VERSION,
2009
- productVersion: PRODUCT_VERSION,
2010
- capabilities: {
1958
+ pid: process.pid,
1959
+ agentVersion: AGENT_VERSION,
1960
+ productVersion: PRODUCT_VERSION,
1961
+ capabilities: {
2011
1962
  status: true,
2012
1963
  thumbnail: options.thumbnailEnabled,
2013
1964
  liveStream: options.liveEnabled,
@@ -2022,13 +1973,13 @@ function connectOnce(options, deviceId) {
2022
1973
  fileTransferMaxBytes: MAX_FILE_TRANSFER_BYTES,
2023
1974
  computerAgent: options.taskEnabled,
2024
1975
  taskDispatch: options.taskEnabled,
2025
- clientUpdate: true,
2026
- productVersion: PRODUCT_VERSION,
2027
- agentApproval: options.taskEnabled,
2028
- agentAudit: options.taskEnabled,
2029
- agentTools: [...NODE_AGENT_OPERATIONS],
2030
- elevation: typeof process.getuid === 'function' ? process.getuid() === 0 : false,
2031
- externalEffects: options.taskEnabled
1976
+ clientUpdate: true,
1977
+ productVersion: PRODUCT_VERSION,
1978
+ agentApproval: options.taskEnabled,
1979
+ agentAudit: options.taskEnabled,
1980
+ agentTools: [...NODE_AGENT_OPERATIONS],
1981
+ elevation: typeof process.getuid === 'function' ? process.getuid() === 0 : false,
1982
+ externalEffects: options.taskEnabled
2032
1983
  }
2033
1984
  });
2034
1985
  });
@@ -2045,12 +1996,12 @@ function connectOnce(options, deviceId) {
2045
1996
  }
2046
1997
 
2047
1998
  const message = JSON.parse(line);
2048
- if (message.type === 'welcome') {
2049
- options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
2050
- ? message.effectivePolicy
1999
+ if (message.type === 'welcome') {
2000
+ options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
2001
+ ? message.effectivePolicy
2051
2002
  : null;
2052
- console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
2053
- markClientUpdateConnected();
2003
+ console.log(`Connected to LiveDesk Hub as ${options.name} (${deviceId})`);
2004
+ markClientUpdateConnected();
2054
2005
  writeJsonLine(socket, {
2055
2006
  type: 'status',
2056
2007
  status: getStatus(options)
@@ -2060,17 +2011,17 @@ function connectOnce(options, deviceId) {
2060
2011
  return;
2061
2012
  }
2062
2013
 
2063
- heartbeatTimer = setInterval(() => {
2064
- writeJsonLine(socket, {
2065
- type: 'status',
2066
- status: getStatus(options)
2067
- });
2068
- }, options.heartbeatMs);
2069
- } else if (message.type === 'policy.update') {
2070
- options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
2071
- ? message.effectivePolicy
2072
- : options.effectivePolicy;
2073
- } else if (message.type === 'command') {
2014
+ heartbeatTimer = setInterval(() => {
2015
+ writeJsonLine(socket, {
2016
+ type: 'status',
2017
+ status: getStatus(options)
2018
+ });
2019
+ }, options.heartbeatMs);
2020
+ } else if (message.type === 'policy.update') {
2021
+ options.effectivePolicy = message.effectivePolicy && typeof message.effectivePolicy === 'object'
2022
+ ? message.effectivePolicy
2023
+ : options.effectivePolicy;
2024
+ } else if (message.type === 'command') {
2074
2025
  handleRemoteCommand(socket, options, message, () => {
2075
2026
  frameSeq += 1;
2076
2027
  return frameSeq;