@livedesk/client 0.1.215 → 0.1.217
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/livedesk-client-node.js +506 -555
- package/bin/livedesk-client-update-bootstrap.cjs +59 -0
- package/bin/livedesk-client.js +195 -58
- package/package.json +11 -11
- package/src/runtime/agent-process-lifecycle.js +85 -23
- package/src/runtime/client-runtime-server.js +719 -131
- package/src/runtime/hub-wake-listener.js +42 -11
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { dirname, join, parse, resolve } from 'node:path';
|
|
4
|
+
import { accessSync, chmodSync, constants as fsConstants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { delimiter, dirname, join, parse, posix, resolve } from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import { createRuntimeManager, normalizeRuntimeAuthSession, runtimeRoleError, RuntimeState } from '../../../runtime-core/src/index.js';
|
|
8
8
|
|
|
9
|
-
const DEFAULT_HOST = '127.0.0.1';
|
|
10
|
-
const DEFAULT_PORT = 5179;
|
|
9
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
10
|
+
const DEFAULT_PORT = 5179;
|
|
11
|
+
const CLIENT_RUNTIME_BIND_TIMEOUT_MS = 10_000;
|
|
12
|
+
const CLIENT_DIAGNOSTIC_COMMAND_FORCE_KILL_MS = 150;
|
|
13
|
+
const CLIENT_DIAGNOSTIC_COMMAND_DRAIN_MS = 750;
|
|
14
|
+
const CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS = 1_500;
|
|
15
|
+
const CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS = 15_000;
|
|
16
|
+
const CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS = 60_000;
|
|
17
|
+
const CLIENT_DIAGNOSTIC_HARDWARE_TTL_MS = 300_000;
|
|
11
18
|
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
12
19
|
const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co';
|
|
13
20
|
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
@@ -181,15 +188,406 @@ function percentFromRatio(value) {
|
|
|
181
188
|
return Math.round(Math.max(0, Math.min(100, number)) * 10) / 10;
|
|
182
189
|
}
|
|
183
190
|
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
function destroyChildRedirectedIo(child) {
|
|
192
|
+
for (const stream of [child?.stdin, child?.stdout, child?.stderr]) {
|
|
193
|
+
try { stream?.destroy(); } catch { /* an exact owned stream already closed */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sanitizeDiagnosticCommandName(value) {
|
|
198
|
+
return normalizeString(value, 200) || 'unknown-diagnostic-command';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function defaultDiagnosticMonotonicNow() {
|
|
202
|
+
return Number(process.hrtime.bigint() / 1_000_000n);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Contract:
|
|
207
|
+
* - A probe is idle until an explicit diagnostic request calls read().
|
|
208
|
+
* - Every successful, unavailable, empty, or failed result is retained for at
|
|
209
|
+
* least 15 monotonic seconds measured from refresh completion.
|
|
210
|
+
* - Concurrent readers share one refresh promise. A failed tool therefore
|
|
211
|
+
* cannot respawn once per status heartbeat.
|
|
212
|
+
*/
|
|
213
|
+
export function createClientDiagnosticProbeCache(options = {}) {
|
|
214
|
+
if (typeof options.refresh !== 'function') {
|
|
215
|
+
throw new TypeError('client-diagnostic-probe-refresh-required');
|
|
216
|
+
}
|
|
217
|
+
const probeId = normalizeString(options.probeId, 80) || 'client-diagnostic-probe';
|
|
218
|
+
const monotonicNow = typeof options.monotonicNow === 'function'
|
|
219
|
+
? options.monotonicNow
|
|
220
|
+
: defaultDiagnosticMonotonicNow;
|
|
221
|
+
const ttlMs = Math.max(
|
|
222
|
+
CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
223
|
+
Number(options.ttlMs) || CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS
|
|
224
|
+
);
|
|
225
|
+
const negativeTtlMs = Math.max(
|
|
226
|
+
CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
227
|
+
Number(options.negativeTtlMs) || CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS
|
|
228
|
+
);
|
|
229
|
+
const fallbackValue = options.fallbackValue;
|
|
230
|
+
let initialized = false;
|
|
231
|
+
let sampledAtMonotonicMs = 0;
|
|
232
|
+
let generation = 0;
|
|
233
|
+
let refreshCount = 0;
|
|
234
|
+
let cacheHitCount = 0;
|
|
235
|
+
let sharedReadCount = 0;
|
|
236
|
+
let record = {
|
|
237
|
+
ok: false,
|
|
238
|
+
value: fallbackValue,
|
|
239
|
+
error: 'not-requested'
|
|
240
|
+
};
|
|
241
|
+
let inFlight = null;
|
|
242
|
+
|
|
243
|
+
const snapshot = () => {
|
|
244
|
+
const now = Number(monotonicNow());
|
|
245
|
+
const ageMs = initialized && Number.isFinite(now)
|
|
246
|
+
? Math.max(0, now - sampledAtMonotonicMs)
|
|
247
|
+
: null;
|
|
248
|
+
return {
|
|
249
|
+
probeId,
|
|
250
|
+
initialized,
|
|
251
|
+
inFlight: Boolean(inFlight),
|
|
252
|
+
ok: initialized ? record.ok === true : false,
|
|
253
|
+
error: initialized ? normalizeString(record.error, 160) : 'not-requested',
|
|
254
|
+
ttlMs,
|
|
255
|
+
negativeTtlMs,
|
|
256
|
+
ageMs,
|
|
257
|
+
generation,
|
|
258
|
+
refreshCount,
|
|
259
|
+
cacheHitCount,
|
|
260
|
+
sharedReadCount
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const read = () => {
|
|
265
|
+
const now = Number(monotonicNow());
|
|
266
|
+
const activeTtlMs = record.ok === true ? ttlMs : negativeTtlMs;
|
|
267
|
+
if (initialized
|
|
268
|
+
&& Number.isFinite(now)
|
|
269
|
+
&& Math.max(0, now - sampledAtMonotonicMs) < activeTtlMs) {
|
|
270
|
+
cacheHitCount += 1;
|
|
271
|
+
return Promise.resolve({
|
|
272
|
+
...record,
|
|
273
|
+
cached: true,
|
|
274
|
+
shared: false,
|
|
275
|
+
generation
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (inFlight) {
|
|
279
|
+
sharedReadCount += 1;
|
|
280
|
+
return inFlight.then(result => ({ ...result, shared: true }));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
inFlight = (async () => {
|
|
284
|
+
let next;
|
|
285
|
+
try {
|
|
286
|
+
const candidate = await options.refresh();
|
|
287
|
+
next = candidate && typeof candidate === 'object'
|
|
288
|
+
? {
|
|
289
|
+
ok: candidate.ok === true,
|
|
290
|
+
value: candidate.value === undefined ? fallbackValue : candidate.value,
|
|
291
|
+
error: candidate.ok === true ? '' : normalizeString(candidate.error, 160) || 'probe-unavailable'
|
|
292
|
+
}
|
|
293
|
+
: {
|
|
294
|
+
ok: false,
|
|
295
|
+
value: fallbackValue,
|
|
296
|
+
error: 'probe-invalid-result'
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
next = {
|
|
300
|
+
ok: false,
|
|
301
|
+
value: fallbackValue,
|
|
302
|
+
error: normalizeString(error?.message || error, 160) || 'probe-failed'
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
record = next;
|
|
306
|
+
initialized = true;
|
|
307
|
+
generation += 1;
|
|
308
|
+
refreshCount += 1;
|
|
309
|
+
sampledAtMonotonicMs = Number(monotonicNow());
|
|
310
|
+
return {
|
|
311
|
+
...record,
|
|
312
|
+
cached: false,
|
|
313
|
+
shared: false,
|
|
314
|
+
generation
|
|
315
|
+
};
|
|
316
|
+
})().finally(() => {
|
|
317
|
+
inFlight = null;
|
|
318
|
+
});
|
|
319
|
+
return inFlight;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
read,
|
|
324
|
+
getSnapshot: snapshot
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isExecutablePath(path) {
|
|
329
|
+
try {
|
|
330
|
+
accessSync(path, fsConstants.X_OK);
|
|
331
|
+
return true;
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function resolveNvidiaSmiCommand(options = {}) {
|
|
338
|
+
const platform = normalizeString(options.platform || process.platform, 20);
|
|
339
|
+
const executablePath = normalizeString(
|
|
340
|
+
options.explicitPath ?? process.env.LIVEDESK_NVIDIA_SMI_PATH,
|
|
341
|
+
1000
|
|
342
|
+
);
|
|
343
|
+
const executableCheck = typeof options.isExecutable === 'function'
|
|
344
|
+
? options.isExecutable
|
|
345
|
+
: isExecutablePath;
|
|
346
|
+
if (platform === 'win32') {
|
|
347
|
+
return executablePath || 'nvidia-smi.exe';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const pathValue = String(options.pathValue ?? process.env.PATH ?? '');
|
|
351
|
+
const pathDelimiter = normalizeString(options.pathDelimiter, 4) || delimiter;
|
|
352
|
+
const joinExecutablePath = platform === 'win32' ? join : posix.join;
|
|
353
|
+
const candidates = [
|
|
354
|
+
executablePath,
|
|
355
|
+
...pathValue
|
|
356
|
+
.split(pathDelimiter)
|
|
357
|
+
.map(directory => directory.trim())
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.map(directory => joinExecutablePath(directory, 'nvidia-smi')),
|
|
360
|
+
...(platform === 'linux' ? ['/usr/bin/nvidia-smi', '/usr/local/bin/nvidia-smi'] : [])
|
|
361
|
+
].filter(Boolean);
|
|
362
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
363
|
+
try {
|
|
364
|
+
if (executableCheck(candidate) === true) return candidate;
|
|
365
|
+
} catch {
|
|
366
|
+
// An unreadable PATH entry is negative probe evidence.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return '';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Contract:
|
|
374
|
+
* - Owns only ChildProcess objects spawned through this instance.
|
|
375
|
+
* - Identity is ownerId + commandId + the immutable ChildProcess handle; a PID
|
|
376
|
+
* is diagnostic evidence and is never rediscovered to authorize termination.
|
|
377
|
+
* - A command deadline and owner close both abort, terminate, drain redirected
|
|
378
|
+
* stdio, and wait for the exact child's terminal `close` event.
|
|
379
|
+
* - Concurrent identical probes share the same command/output/terminal owner.
|
|
380
|
+
* - Output, close, and active-ledger retention are bounded independently so an
|
|
381
|
+
* inherited pipe can never keep the Client event loop alive without evidence.
|
|
382
|
+
*/
|
|
383
|
+
export function createClientDiagnosticCommandOwner(options = {}) {
|
|
384
|
+
const execFileImpl = typeof options.execFileImpl === 'function' ? options.execFileImpl : execFile;
|
|
385
|
+
const ownerId = normalizeString(options.ownerId, 160)
|
|
386
|
+
|| `client-diagnostics-${process.pid}-${randomBytes(8).toString('hex')}`;
|
|
387
|
+
const forceKillDelayMs = Math.max(
|
|
388
|
+
10,
|
|
389
|
+
Number(options.forceKillDelayMs) || CLIENT_DIAGNOSTIC_COMMAND_FORCE_KILL_MS
|
|
390
|
+
);
|
|
391
|
+
const commandDrainMs = Math.max(
|
|
392
|
+
forceKillDelayMs + 10,
|
|
393
|
+
Number(options.commandDrainMs) || CLIENT_DIAGNOSTIC_COMMAND_DRAIN_MS
|
|
394
|
+
);
|
|
395
|
+
const active = new Map();
|
|
396
|
+
const activeByKey = new Map();
|
|
397
|
+
let commandSequence = 0;
|
|
398
|
+
let spawnedCount = 0;
|
|
399
|
+
let joinedCount = 0;
|
|
400
|
+
let timedOutCount = 0;
|
|
401
|
+
let sharedRequestCount = 0;
|
|
402
|
+
let closePromise = null;
|
|
403
|
+
let closed = false;
|
|
404
|
+
|
|
405
|
+
const snapshotRecord = record => ({
|
|
406
|
+
ownerId,
|
|
407
|
+
commandId: record.commandId,
|
|
408
|
+
pid: record.pid,
|
|
409
|
+
command: record.command,
|
|
410
|
+
startedAt: record.startedAt,
|
|
411
|
+
terminationReason: record.terminationReason || ''
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const getSnapshot = () => ({
|
|
415
|
+
ownerId,
|
|
416
|
+
closed,
|
|
417
|
+
spawnedCount,
|
|
418
|
+
joinedCount,
|
|
419
|
+
timedOutCount,
|
|
420
|
+
sharedRequestCount,
|
|
421
|
+
activeCount: active.size,
|
|
422
|
+
redirectedStreamCount: [...active.values()].reduce(
|
|
423
|
+
(count, record) => count + [record.child?.stdin, record.child?.stdout, record.child?.stderr]
|
|
424
|
+
.filter(stream => stream && !stream.destroyed).length,
|
|
425
|
+
0
|
|
426
|
+
),
|
|
427
|
+
active: [...active.values()].map(snapshotRecord)
|
|
192
428
|
});
|
|
429
|
+
|
|
430
|
+
const settleOutput = (record, output = '') => {
|
|
431
|
+
if (record.outputSettled) return;
|
|
432
|
+
record.outputSettled = true;
|
|
433
|
+
record.resolveOutput(String(output || ''));
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const markTerminal = record => {
|
|
437
|
+
if (record.terminal) return;
|
|
438
|
+
record.terminal = true;
|
|
439
|
+
clearTimeout(record.commandTimer);
|
|
440
|
+
clearTimeout(record.forceKillTimer);
|
|
441
|
+
clearTimeout(record.outputDrainTimer);
|
|
442
|
+
destroyChildRedirectedIo(record.child);
|
|
443
|
+
active.delete(record.commandId);
|
|
444
|
+
if (activeByKey.get(record.commandKey) === record) {
|
|
445
|
+
activeByKey.delete(record.commandKey);
|
|
446
|
+
}
|
|
447
|
+
joinedCount += 1;
|
|
448
|
+
settleOutput(record, record.error ? '' : record.stdout);
|
|
449
|
+
record.resolveTerminal();
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const terminateOwnedRecord = (record, reason) => {
|
|
453
|
+
if (!record || record.terminal || record.terminationRequested) return;
|
|
454
|
+
record.terminationRequested = true;
|
|
455
|
+
record.terminationReason = normalizeString(reason, 80) || 'diagnostic-owner-close';
|
|
456
|
+
if (record.terminationReason === 'command-timeout') timedOutCount += 1;
|
|
457
|
+
clearTimeout(record.commandTimer);
|
|
458
|
+
try { record.abortController.abort(new Error(record.terminationReason)); } catch { /* already aborted */ }
|
|
459
|
+
try { record.child?.kill('SIGTERM'); } catch { /* exact child already exited */ }
|
|
460
|
+
record.forceKillTimer = setTimeout(() => {
|
|
461
|
+
if (record.terminal) return;
|
|
462
|
+
try { record.child?.kill('SIGKILL'); } catch { /* exact child already exited */ }
|
|
463
|
+
// An inherited writer can keep execFile's callback pending even after the
|
|
464
|
+
// root exits. Closing our exact pipe ends the Client-side handle owner.
|
|
465
|
+
destroyChildRedirectedIo(record.child);
|
|
466
|
+
}, forceKillDelayMs);
|
|
467
|
+
record.forceKillTimer.unref?.();
|
|
468
|
+
record.outputDrainTimer = setTimeout(() => {
|
|
469
|
+
if (record.terminal) return;
|
|
470
|
+
settleOutput(record, '');
|
|
471
|
+
destroyChildRedirectedIo(record.child);
|
|
472
|
+
record.child?.unref?.();
|
|
473
|
+
}, commandDrainMs);
|
|
474
|
+
record.outputDrainTimer.unref?.();
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const capture = (command, args = [], timeout = 3500) => {
|
|
478
|
+
if (closed) return Promise.resolve('');
|
|
479
|
+
const normalizedArgs = Array.isArray(args) ? args : [];
|
|
480
|
+
const commandKey = JSON.stringify([
|
|
481
|
+
String(command || ''),
|
|
482
|
+
...normalizedArgs.map(value => String(value ?? ''))
|
|
483
|
+
]);
|
|
484
|
+
const sharedRecord = activeByKey.get(commandKey);
|
|
485
|
+
if (sharedRecord) {
|
|
486
|
+
sharedRequestCount += 1;
|
|
487
|
+
return sharedRecord.outputPromise;
|
|
488
|
+
}
|
|
489
|
+
const commandId = `${ownerId}:${++commandSequence}`;
|
|
490
|
+
const abortController = new AbortController();
|
|
491
|
+
let resolveOutput;
|
|
492
|
+
let resolveTerminal;
|
|
493
|
+
const outputPromise = new Promise(resolve => { resolveOutput = resolve; });
|
|
494
|
+
const terminalPromise = new Promise(resolve => { resolveTerminal = resolve; });
|
|
495
|
+
const record = {
|
|
496
|
+
ownerId,
|
|
497
|
+
commandId,
|
|
498
|
+
commandKey,
|
|
499
|
+
command: sanitizeDiagnosticCommandName(command),
|
|
500
|
+
pid: 0,
|
|
501
|
+
startedAt: new Date().toISOString(),
|
|
502
|
+
child: null,
|
|
503
|
+
abortController,
|
|
504
|
+
resolveOutput,
|
|
505
|
+
resolveTerminal,
|
|
506
|
+
outputPromise,
|
|
507
|
+
terminalPromise,
|
|
508
|
+
stdout: '',
|
|
509
|
+
error: null,
|
|
510
|
+
outputSettled: false,
|
|
511
|
+
terminal: false,
|
|
512
|
+
terminationRequested: false,
|
|
513
|
+
terminationReason: '',
|
|
514
|
+
commandTimer: null,
|
|
515
|
+
forceKillTimer: null,
|
|
516
|
+
outputDrainTimer: null
|
|
517
|
+
};
|
|
518
|
+
active.set(commandId, record);
|
|
519
|
+
activeByKey.set(commandKey, record);
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
record.child = execFileImpl(command, normalizedArgs, {
|
|
523
|
+
encoding: 'utf8',
|
|
524
|
+
maxBuffer: 512 * 1024,
|
|
525
|
+
windowsHide: true,
|
|
526
|
+
signal: abortController.signal
|
|
527
|
+
}, (error, stdout) => {
|
|
528
|
+
record.error = error || null;
|
|
529
|
+
record.stdout = error ? '' : String(stdout || '');
|
|
530
|
+
settleOutput(record, record.stdout);
|
|
531
|
+
});
|
|
532
|
+
record.pid = Number(record.child?.pid || 0);
|
|
533
|
+
spawnedCount += 1;
|
|
534
|
+
record.child.once('close', () => markTerminal(record));
|
|
535
|
+
record.child.once('error', error => {
|
|
536
|
+
record.error = record.error || error;
|
|
537
|
+
});
|
|
538
|
+
const commandTimeoutMs = Math.max(10, Number(timeout) || 3500);
|
|
539
|
+
record.commandTimer = setTimeout(
|
|
540
|
+
() => terminateOwnedRecord(record, 'command-timeout'),
|
|
541
|
+
commandTimeoutMs
|
|
542
|
+
);
|
|
543
|
+
record.commandTimer.unref?.();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
record.error = error;
|
|
546
|
+
settleOutput(record, '');
|
|
547
|
+
markTerminal(record);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return outputPromise;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const close = (timeoutMs = CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS) => {
|
|
554
|
+
if (closePromise) return closePromise;
|
|
555
|
+
closed = true;
|
|
556
|
+
const boundedTimeoutMs = Math.max(100, Number(timeoutMs) || CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS);
|
|
557
|
+
closePromise = (async () => {
|
|
558
|
+
const records = [...active.values()];
|
|
559
|
+
for (const record of records) terminateOwnedRecord(record, 'diagnostic-owner-close');
|
|
560
|
+
if (records.length > 0) {
|
|
561
|
+
let deadlineTimer;
|
|
562
|
+
await Promise.race([
|
|
563
|
+
Promise.allSettled(records.map(record => record.terminalPromise)),
|
|
564
|
+
new Promise(resolve => {
|
|
565
|
+
deadlineTimer = setTimeout(resolve, boundedTimeoutMs);
|
|
566
|
+
})
|
|
567
|
+
]);
|
|
568
|
+
clearTimeout(deadlineTimer);
|
|
569
|
+
}
|
|
570
|
+
for (const record of active.values()) {
|
|
571
|
+
settleOutput(record, '');
|
|
572
|
+
try { record.child?.kill('SIGKILL'); } catch { /* exact child already exited */ }
|
|
573
|
+
destroyChildRedirectedIo(record.child);
|
|
574
|
+
record.child?.unref?.();
|
|
575
|
+
}
|
|
576
|
+
const snapshot = getSnapshot();
|
|
577
|
+
return {
|
|
578
|
+
ok: snapshot.activeCount === 0 && snapshot.redirectedStreamCount === 0,
|
|
579
|
+
...snapshot
|
|
580
|
+
};
|
|
581
|
+
})();
|
|
582
|
+
return closePromise;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
ownerId,
|
|
587
|
+
capture,
|
|
588
|
+
close,
|
|
589
|
+
getSnapshot
|
|
590
|
+
};
|
|
193
591
|
}
|
|
194
592
|
|
|
195
593
|
export function parseNvidiaSmiOutput(output) {
|
|
@@ -280,17 +678,30 @@ function activeNetworkInterfaces() {
|
|
|
280
678
|
return interfaces;
|
|
281
679
|
}
|
|
282
680
|
|
|
283
|
-
async function
|
|
284
|
-
if (
|
|
285
|
-
|
|
681
|
+
async function readNetworkTotalsProbe(commandOwner, platform = process.platform) {
|
|
682
|
+
if (platform === 'win32') {
|
|
683
|
+
const output = await commandOwner.capture('netstat.exe', ['-e'], 2500);
|
|
684
|
+
return String(output || '').trim()
|
|
685
|
+
? { ok: true, value: parseWindowsNetworkOutput(output) }
|
|
686
|
+
: { ok: false, value: { receivedBytes: 0, sentBytes: 0 }, error: 'netstat-unavailable' };
|
|
286
687
|
}
|
|
287
|
-
if (
|
|
288
|
-
|
|
688
|
+
if (platform === 'darwin') {
|
|
689
|
+
const output = await commandOwner.capture('netstat', ['-ibn'], 2500);
|
|
690
|
+
return String(output || '').trim()
|
|
691
|
+
? { ok: true, value: parseMacNetworkOutput(output) }
|
|
692
|
+
: { ok: false, value: { receivedBytes: 0, sentBytes: 0 }, error: 'netstat-unavailable' };
|
|
289
693
|
}
|
|
290
694
|
try {
|
|
291
|
-
return
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
value: parseLinuxNetworkOutput(readFileSync('/proc/net/dev', 'utf8'))
|
|
698
|
+
};
|
|
292
699
|
} catch {
|
|
293
|
-
return {
|
|
700
|
+
return {
|
|
701
|
+
ok: false,
|
|
702
|
+
value: { receivedBytes: 0, sentBytes: 0 },
|
|
703
|
+
error: 'network-counters-unavailable'
|
|
704
|
+
};
|
|
294
705
|
}
|
|
295
706
|
}
|
|
296
707
|
|
|
@@ -444,7 +855,7 @@ export function readMacDiskSnapshots(options = {}) {
|
|
|
444
855
|
return disks;
|
|
445
856
|
}
|
|
446
857
|
|
|
447
|
-
async function readWindowsHardwareSnapshot() {
|
|
858
|
+
async function readWindowsHardwareSnapshot(commandOwner) {
|
|
448
859
|
const script = [
|
|
449
860
|
'$ErrorActionPreference="Stop"',
|
|
450
861
|
'$OutputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false)',
|
|
@@ -453,7 +864,7 @@ async function readWindowsHardwareSnapshot() {
|
|
|
453
864
|
'$disks=@(Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | Where-Object { $physicalDriveIds -contains $_.DeviceID } | Sort-Object DeviceID | ForEach-Object { [pscustomobject]@{ name=[string]$_.VolumeName; mount=[string]$_.DeviceID; totalBytes=[double]$_.Size; freeBytes=[double]$_.FreeSpace } })',
|
|
454
865
|
'[pscustomobject]@{ gpus=$gpus; disks=$disks } | ConvertTo-Json -Compress -Depth 5'
|
|
455
866
|
].join('; ');
|
|
456
|
-
return parseWindowsHardwareOutput(await
|
|
867
|
+
return parseWindowsHardwareOutput(await commandOwner.capture('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], 5000));
|
|
457
868
|
}
|
|
458
869
|
|
|
459
870
|
function parseMacGpuOutput(output) {
|
|
@@ -467,37 +878,45 @@ function parseMacGpuOutput(output) {
|
|
|
467
878
|
}
|
|
468
879
|
}
|
|
469
880
|
|
|
470
|
-
async function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
} else {
|
|
477
|
-
const disk = process.platform === 'darwin' ? null : readRootDiskSnapshot();
|
|
478
|
-
const gpus = process.platform === 'darwin'
|
|
479
|
-
? parseMacGpuOutput(await captureCommand('system_profiler', ['SPDisplaysDataType', '-json'], 5000))
|
|
480
|
-
: [];
|
|
481
|
-
platformSnapshot = { gpus, disks: process.platform === 'darwin' ? readMacDiskSnapshots() : disk ? [disk] : [] };
|
|
881
|
+
async function collectPlatformHardwareProbe(commandOwner, platform = process.platform) {
|
|
882
|
+
if (platform === 'win32') {
|
|
883
|
+
const snapshot = await readWindowsHardwareSnapshot(commandOwner);
|
|
884
|
+
return snapshot.gpus.length > 0 || snapshot.disks.length > 0
|
|
885
|
+
? { ok: true, value: snapshot }
|
|
886
|
+
: { ok: false, value: snapshot, error: 'windows-hardware-unavailable' };
|
|
482
887
|
}
|
|
483
|
-
|
|
484
|
-
|
|
888
|
+
if (platform === 'darwin') {
|
|
889
|
+
const output = await commandOwner.capture('system_profiler', ['SPDisplaysDataType', '-json'], 5000);
|
|
890
|
+
const snapshot = {
|
|
891
|
+
gpus: parseMacGpuOutput(output),
|
|
892
|
+
disks: readMacDiskSnapshots()
|
|
893
|
+
};
|
|
894
|
+
return String(output || '').trim()
|
|
895
|
+
? { ok: true, value: snapshot }
|
|
896
|
+
: { ok: false, value: snapshot, error: 'system-profiler-unavailable' };
|
|
897
|
+
}
|
|
898
|
+
const disk = readRootDiskSnapshot();
|
|
485
899
|
return {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
gpus: mergeGpuSnapshots(platformSnapshot.gpus, nvidiaGpus),
|
|
490
|
-
disks: platformSnapshot.disks,
|
|
491
|
-
collectedAt: new Date().toISOString()
|
|
900
|
+
ok: Boolean(disk),
|
|
901
|
+
value: { gpus: [], disks: disk ? [disk] : [] },
|
|
902
|
+
error: disk ? '' : 'root-disk-unavailable'
|
|
492
903
|
};
|
|
493
904
|
}
|
|
494
905
|
|
|
495
|
-
async function
|
|
496
|
-
|
|
497
|
-
|
|
906
|
+
async function collectNvidiaGpuProbe(commandOwner, platform = process.platform) {
|
|
907
|
+
const command = resolveNvidiaSmiCommand({ platform });
|
|
908
|
+
if (!command) {
|
|
909
|
+
return { ok: false, value: [], error: 'nvidia-smi-unavailable' };
|
|
910
|
+
}
|
|
911
|
+
const output = await commandOwner.capture(
|
|
912
|
+
command,
|
|
498
913
|
['--query-gpu=name,utilization.gpu,memory.total,memory.used', '--format=csv,noheader,nounits'],
|
|
499
914
|
3000
|
|
500
|
-
)
|
|
915
|
+
);
|
|
916
|
+
const gpus = parseNvidiaSmiOutput(output);
|
|
917
|
+
return gpus.length > 0
|
|
918
|
+
? { ok: true, value: gpus }
|
|
919
|
+
: { ok: false, value: [], error: 'nvidia-smi-empty-or-failed' };
|
|
501
920
|
}
|
|
502
921
|
|
|
503
922
|
function normalizeOrigin(value) {
|
|
@@ -697,19 +1116,76 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
697
1116
|
const deviceId = normalizeString(options.deviceId || process.env.LIVEDESK_DEVICE_ID, 160);
|
|
698
1117
|
const deviceName = normalizeString(options.deviceName || os.hostname(), 160) || os.hostname();
|
|
699
1118
|
const appVersion = normalizeString(options.appVersion || process.env.LIVEDESK_NPM_LAUNCHER_VERSION, 80) || 'dev';
|
|
700
|
-
const savedSession = options.
|
|
1119
|
+
const savedSession = options.loadSavedSession === false
|
|
1120
|
+
? null
|
|
1121
|
+
: options.savedSession?.refresh_token
|
|
1122
|
+
? options.savedSession
|
|
1123
|
+
: readSavedSession();
|
|
701
1124
|
const savedSessionPersisted = Boolean(savedSession?.refresh_token);
|
|
702
1125
|
const savedAccountProfile = readClientAccountProfile(savedSession);
|
|
1126
|
+
const diagnosticPlatform = normalizeString(options.diagnosticPlatform || process.platform, 20);
|
|
1127
|
+
const diagnosticMonotonicNow = typeof options.diagnosticMonotonicNow === 'function'
|
|
1128
|
+
? options.diagnosticMonotonicNow
|
|
1129
|
+
: defaultDiagnosticMonotonicNow;
|
|
1130
|
+
const diagnosticProbeTtlMs = Math.max(
|
|
1131
|
+
CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
1132
|
+
Number(options.diagnosticProbeTtlMs) || CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS
|
|
1133
|
+
);
|
|
703
1134
|
let previousCpuTotals = readCpuTotals();
|
|
704
|
-
let hardwareSnapshot = {
|
|
1135
|
+
let hardwareSnapshot = {
|
|
1136
|
+
gpus: [],
|
|
1137
|
+
disks: [],
|
|
1138
|
+
collectedAt: '',
|
|
1139
|
+
collecting: false,
|
|
1140
|
+
lastError: options.diagnosticsEnabled === false ? 'diagnostics-disabled' : 'not-requested'
|
|
1141
|
+
};
|
|
705
1142
|
let networkSnapshot = { interfaces: activeNetworkInterfaces(), receivedBytes: 0, sentBytes: 0, receiveBytesPerSecond: 0, sendBytesPerSecond: 0, sampledAt: '' };
|
|
706
1143
|
let previousNetworkTotals = null;
|
|
707
|
-
let
|
|
708
|
-
let
|
|
709
|
-
let
|
|
710
|
-
let
|
|
711
|
-
let
|
|
712
|
-
|
|
1144
|
+
let lastDiagnosticRequestAt = '';
|
|
1145
|
+
let lastDiagnosticCompletedAt = '';
|
|
1146
|
+
let diagnosticRefreshInFlight = null;
|
|
1147
|
+
let closed = false;
|
|
1148
|
+
let runtimeClosePromise = null;
|
|
1149
|
+
const diagnosticCommandOwner = options.diagnosticCommandOwner
|
|
1150
|
+
|| createClientDiagnosticCommandOwner();
|
|
1151
|
+
if (typeof diagnosticCommandOwner.capture !== 'function'
|
|
1152
|
+
|| typeof diagnosticCommandOwner.close !== 'function'
|
|
1153
|
+
|| typeof diagnosticCommandOwner.getSnapshot !== 'function') {
|
|
1154
|
+
throw new TypeError('invalid-client-diagnostic-command-owner');
|
|
1155
|
+
}
|
|
1156
|
+
const diagnosticTasks = new Set();
|
|
1157
|
+
const trackDiagnosticTask = operation => {
|
|
1158
|
+
if (closed) return Promise.resolve();
|
|
1159
|
+
const task = Promise.resolve()
|
|
1160
|
+
.then(operation)
|
|
1161
|
+
.finally(() => diagnosticTasks.delete(task));
|
|
1162
|
+
diagnosticTasks.add(task);
|
|
1163
|
+
return task;
|
|
1164
|
+
};
|
|
1165
|
+
const hardwareProbeCache = createClientDiagnosticProbeCache({
|
|
1166
|
+
probeId: 'platform-hardware',
|
|
1167
|
+
ttlMs: Math.max(CLIENT_DIAGNOSTIC_HARDWARE_TTL_MS, diagnosticProbeTtlMs),
|
|
1168
|
+
negativeTtlMs: CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS,
|
|
1169
|
+
monotonicNow: diagnosticMonotonicNow,
|
|
1170
|
+
fallbackValue: { gpus: [], disks: [] },
|
|
1171
|
+
refresh: () => collectPlatformHardwareProbe(diagnosticCommandOwner, diagnosticPlatform)
|
|
1172
|
+
});
|
|
1173
|
+
const nvidiaProbeCache = createClientDiagnosticProbeCache({
|
|
1174
|
+
probeId: 'nvidia-gpu',
|
|
1175
|
+
ttlMs: diagnosticProbeTtlMs,
|
|
1176
|
+
negativeTtlMs: CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS,
|
|
1177
|
+
monotonicNow: diagnosticMonotonicNow,
|
|
1178
|
+
fallbackValue: [],
|
|
1179
|
+
refresh: () => collectNvidiaGpuProbe(diagnosticCommandOwner, diagnosticPlatform)
|
|
1180
|
+
});
|
|
1181
|
+
const networkProbeCache = createClientDiagnosticProbeCache({
|
|
1182
|
+
probeId: 'network-totals',
|
|
1183
|
+
ttlMs: diagnosticProbeTtlMs,
|
|
1184
|
+
negativeTtlMs: CLIENT_DIAGNOSTIC_PROBE_NEGATIVE_TTL_MS,
|
|
1185
|
+
monotonicNow: diagnosticMonotonicNow,
|
|
1186
|
+
fallbackValue: { receivedBytes: 0, sentBytes: 0 },
|
|
1187
|
+
refresh: () => readNetworkTotalsProbe(diagnosticCommandOwner, diagnosticPlatform)
|
|
1188
|
+
});
|
|
713
1189
|
const csrfToken = randomBytes(32).toString('hex');
|
|
714
1190
|
const state = {
|
|
715
1191
|
route: '/computer',
|
|
@@ -759,76 +1235,81 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
759
1235
|
});
|
|
760
1236
|
runtime.emit('runtime.waiting-auth', { role: 'client' });
|
|
761
1237
|
|
|
762
|
-
const
|
|
763
|
-
if (
|
|
764
|
-
|
|
1238
|
+
const requestDiagnosticRefresh = () => {
|
|
1239
|
+
if (closed || options.diagnosticsEnabled === false) return Promise.resolve();
|
|
1240
|
+
if (diagnosticRefreshInFlight) return diagnosticRefreshInFlight;
|
|
1241
|
+
lastDiagnosticRequestAt = new Date().toISOString();
|
|
765
1242
|
hardwareSnapshot = { ...hardwareSnapshot, collecting: true };
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1243
|
+
diagnosticRefreshInFlight = trackDiagnosticTask(async () => {
|
|
1244
|
+
const [platformResult, nvidiaResult, networkResult] = await Promise.all([
|
|
1245
|
+
hardwareProbeCache.read(),
|
|
1246
|
+
nvidiaProbeCache.read(),
|
|
1247
|
+
networkProbeCache.read()
|
|
1248
|
+
]);
|
|
1249
|
+
if (closed) return;
|
|
1250
|
+
|
|
1251
|
+
const platformSnapshot = platformResult.value && typeof platformResult.value === 'object'
|
|
1252
|
+
? platformResult.value
|
|
1253
|
+
: { gpus: [], disks: [] };
|
|
1254
|
+
const nvidiaGpus = Array.isArray(nvidiaResult.value) ? nvidiaResult.value : [];
|
|
1255
|
+
const errors = [platformResult, nvidiaResult, networkResult]
|
|
1256
|
+
.filter(result => result.ok !== true && result.error)
|
|
1257
|
+
.map(result => normalizeString(result.error, 160));
|
|
773
1258
|
hardwareSnapshot = {
|
|
774
|
-
|
|
1259
|
+
// Preserve the platform adapter inventory on hybrid-GPU computers. The
|
|
1260
|
+
// NVIDIA sampler enriches its matching adapter instead of replacing
|
|
1261
|
+
// every non-NVIDIA display adapter with the telemetry subset.
|
|
1262
|
+
gpus: mergeGpuSnapshots(platformSnapshot.gpus, nvidiaGpus),
|
|
1263
|
+
disks: Array.isArray(platformSnapshot.disks) ? platformSnapshot.disks : [],
|
|
1264
|
+
collectedAt: new Date().toISOString(),
|
|
775
1265
|
collecting: false,
|
|
776
|
-
lastError:
|
|
1266
|
+
lastError: errors.join(', ')
|
|
777
1267
|
};
|
|
778
|
-
} finally {
|
|
779
|
-
hardwareRefreshInFlight = false;
|
|
780
|
-
}
|
|
781
|
-
};
|
|
782
1268
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1269
|
+
const totals = networkResult.value && typeof networkResult.value === 'object'
|
|
1270
|
+
? networkResult.value
|
|
1271
|
+
: { receivedBytes: 0, sentBytes: 0 };
|
|
1272
|
+
if (networkResult.cached !== true || !networkSnapshot.sampledAt) {
|
|
1273
|
+
const sampledAtMs = Date.now();
|
|
1274
|
+
const elapsedSeconds = previousNetworkTotals
|
|
1275
|
+
? Math.max(0.25, (sampledAtMs - previousNetworkTotals.sampledAtMs) / 1000)
|
|
1276
|
+
: 0;
|
|
1277
|
+
networkSnapshot = {
|
|
1278
|
+
interfaces: activeNetworkInterfaces(),
|
|
1279
|
+
receivedBytes: totals.receivedBytes,
|
|
1280
|
+
sentBytes: totals.sentBytes,
|
|
1281
|
+
receiveBytesPerSecond: elapsedSeconds > 0
|
|
1282
|
+
? Math.max(0, (totals.receivedBytes - previousNetworkTotals.receivedBytes) / elapsedSeconds)
|
|
1283
|
+
: 0,
|
|
1284
|
+
sendBytesPerSecond: elapsedSeconds > 0
|
|
1285
|
+
? Math.max(0, (totals.sentBytes - previousNetworkTotals.sentBytes) / elapsedSeconds)
|
|
1286
|
+
: 0,
|
|
1287
|
+
sampledAt: new Date(sampledAtMs).toISOString()
|
|
1288
|
+
};
|
|
1289
|
+
previousNetworkTotals = { ...totals, sampledAtMs };
|
|
1290
|
+
}
|
|
1291
|
+
lastDiagnosticCompletedAt = new Date().toISOString();
|
|
1292
|
+
runtime.emit('client.system.diagnostics', {
|
|
1293
|
+
gpuCount: hardwareSnapshot.gpus.length,
|
|
1294
|
+
diskCount: hardwareSnapshot.disks.length,
|
|
1295
|
+
platformHardwareGeneration: platformResult.generation,
|
|
1296
|
+
nvidiaGeneration: nvidiaResult.generation,
|
|
1297
|
+
networkGeneration: networkResult.generation
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
void diagnosticRefreshInFlight.finally(() => {
|
|
1301
|
+
diagnosticRefreshInFlight = null;
|
|
1302
|
+
if (!closed && hardwareSnapshot.collecting) {
|
|
789
1303
|
hardwareSnapshot = {
|
|
790
1304
|
...hardwareSnapshot,
|
|
791
|
-
|
|
792
|
-
|
|
1305
|
+
collecting: false,
|
|
1306
|
+
lastError: hardwareSnapshot.lastError || 'diagnostic-refresh-failed'
|
|
793
1307
|
};
|
|
794
1308
|
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
const refreshNetworkSnapshot = async () => {
|
|
801
|
-
if (networkRefreshInFlight) return;
|
|
802
|
-
networkRefreshInFlight = true;
|
|
803
|
-
try {
|
|
804
|
-
const totals = await readNetworkTotals();
|
|
805
|
-
const sampledAtMs = Date.now();
|
|
806
|
-
const elapsedSeconds = previousNetworkTotals
|
|
807
|
-
? Math.max(0.25, (sampledAtMs - previousNetworkTotals.sampledAtMs) / 1000)
|
|
808
|
-
: 0;
|
|
809
|
-
networkSnapshot = {
|
|
810
|
-
interfaces: activeNetworkInterfaces(),
|
|
811
|
-
receivedBytes: totals.receivedBytes,
|
|
812
|
-
sentBytes: totals.sentBytes,
|
|
813
|
-
receiveBytesPerSecond: elapsedSeconds > 0 ? Math.max(0, (totals.receivedBytes - previousNetworkTotals.receivedBytes) / elapsedSeconds) : 0,
|
|
814
|
-
sendBytesPerSecond: elapsedSeconds > 0 ? Math.max(0, (totals.sentBytes - previousNetworkTotals.sentBytes) / elapsedSeconds) : 0,
|
|
815
|
-
sampledAt: new Date(sampledAtMs).toISOString()
|
|
816
|
-
};
|
|
817
|
-
previousNetworkTotals = { ...totals, sampledAtMs };
|
|
818
|
-
} finally {
|
|
819
|
-
networkRefreshInFlight = false;
|
|
820
|
-
}
|
|
1309
|
+
});
|
|
1310
|
+
return diagnosticRefreshInFlight;
|
|
821
1311
|
};
|
|
822
1312
|
|
|
823
|
-
void refreshHardwareSnapshot();
|
|
824
|
-
void refreshNetworkSnapshot();
|
|
825
|
-
hardwareRefreshTimer = setInterval(() => { void refreshHardwareSnapshot(); }, 60_000);
|
|
826
|
-
hardwareRefreshTimer.unref?.();
|
|
827
|
-
gpuRefreshTimer = setInterval(() => { void refreshGpuSnapshot(); }, 2500);
|
|
828
|
-
gpuRefreshTimer.unref?.();
|
|
829
|
-
networkRefreshTimer = setInterval(() => { void refreshNetworkSnapshot(); }, 2000);
|
|
830
|
-
networkRefreshTimer.unref?.();
|
|
831
|
-
|
|
832
1313
|
const recordAuthAttempt = (result, error = '') => {
|
|
833
1314
|
const message = normalizeString(error, 240);
|
|
834
1315
|
state.auth = { ...state.auth, lastAttemptAt: new Date().toISOString(), lastResult: normalizeString(result, 80), lastError: message };
|
|
@@ -880,6 +1361,7 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
880
1361
|
lastAttemptAt: state.connectedAt,
|
|
881
1362
|
lastResult: 'accepted',
|
|
882
1363
|
lastError: '',
|
|
1364
|
+
persisted: Boolean(choice?.session?.refresh_token) || state.auth.persisted,
|
|
883
1365
|
email: accountProfile.email || state.auth.email,
|
|
884
1366
|
name: accountProfile.name || state.auth.name,
|
|
885
1367
|
avatarUrl: accountProfile.avatarUrl || state.auth.avatarUrl
|
|
@@ -945,7 +1427,17 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
945
1427
|
disks: hardwareSnapshot.disks,
|
|
946
1428
|
network: networkSnapshot,
|
|
947
1429
|
hardwareCollectedAt: hardwareSnapshot.collectedAt,
|
|
948
|
-
hardwareCollecting: hardwareSnapshot.collecting
|
|
1430
|
+
hardwareCollecting: hardwareSnapshot.collecting,
|
|
1431
|
+
hardwareLastError: hardwareSnapshot.lastError || '',
|
|
1432
|
+
diagnosticProbes: {
|
|
1433
|
+
requestDriven: true,
|
|
1434
|
+
minimumTtlMs: CLIENT_DIAGNOSTIC_PROBE_MIN_TTL_MS,
|
|
1435
|
+
requestedAt: lastDiagnosticRequestAt,
|
|
1436
|
+
completedAt: lastDiagnosticCompletedAt,
|
|
1437
|
+
platformHardware: hardwareProbeCache.getSnapshot(),
|
|
1438
|
+
nvidiaGpu: nvidiaProbeCache.getSnapshot(),
|
|
1439
|
+
networkTotals: networkProbeCache.getSnapshot()
|
|
1440
|
+
}
|
|
949
1441
|
},
|
|
950
1442
|
permissions: {
|
|
951
1443
|
screenCapture: state.agent.state === 'running',
|
|
@@ -1014,11 +1506,16 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1014
1506
|
respondJson(200, { ok: true, product: 'LiveDesk', role: 'client', timestamp: new Date().toISOString() });
|
|
1015
1507
|
return;
|
|
1016
1508
|
}
|
|
1017
|
-
if (pathname === '/api/
|
|
1509
|
+
if (pathname === '/api/client/diagnostics') {
|
|
1510
|
+
await requestDiagnosticRefresh();
|
|
1018
1511
|
respondJson(200, getState());
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
if (pathname === '/api/runtime/
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (pathname === '/api/runtime' || pathname === '/api/runtime/status' || pathname === '/api/client/status' || pathname === '/api/client/computer' || pathname === '/api/client/system' || pathname === '/api/client/permissions') {
|
|
1515
|
+
respondJson(200, getState());
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (pathname === '/api/runtime/events') {
|
|
1022
1519
|
respondJson(200, { ok: true, events: runtime.getEvents() });
|
|
1023
1520
|
return;
|
|
1024
1521
|
}
|
|
@@ -1355,14 +1852,33 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1355
1852
|
|
|
1356
1853
|
const start = () => new Promise((resolveStart, rejectStart) => {
|
|
1357
1854
|
if (process.env.LIVEDESK_DESKTOP_HOST === '1') clearSavedSession();
|
|
1855
|
+
let settled = false;
|
|
1358
1856
|
server = createServer((req, res) => {
|
|
1359
1857
|
void handleRequest(req, res).catch(error => {
|
|
1360
1858
|
if (!res.headersSent) sendJson(res, 500, { ok: false, error: normalizeString(error?.message || error) }, req.headers.origin, port, trustedWebOrigins);
|
|
1361
1859
|
else res.end();
|
|
1362
|
-
});
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
const bindTimer = setTimeout(() => {
|
|
1863
|
+
if (settled) return;
|
|
1864
|
+
settled = true;
|
|
1865
|
+
try { server.close(); } catch { /* bind never completed */ }
|
|
1866
|
+
rejectStart(new Error(`client-runtime-bind-timeout:${host}:${port}`));
|
|
1867
|
+
}, CLIENT_RUNTIME_BIND_TIMEOUT_MS);
|
|
1868
|
+
bindTimer.unref?.();
|
|
1869
|
+
server.once('error', error => {
|
|
1870
|
+
if (settled) return;
|
|
1871
|
+
settled = true;
|
|
1872
|
+
clearTimeout(bindTimer);
|
|
1873
|
+
rejectStart(error);
|
|
1874
|
+
});
|
|
1365
1875
|
server.listen(port, host, () => {
|
|
1876
|
+
if (settled) {
|
|
1877
|
+
try { server.close(); } catch { /* timed-out bind already owns cleanup */ }
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
settled = true;
|
|
1881
|
+
clearTimeout(bindTimer);
|
|
1366
1882
|
runtime.emit('runtime.http.started', { host, port });
|
|
1367
1883
|
if (initialChoice) {
|
|
1368
1884
|
setImmediate(() => {
|
|
@@ -1392,15 +1908,90 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1392
1908
|
}
|
|
1393
1909
|
resolveStart();
|
|
1394
1910
|
});
|
|
1395
|
-
});
|
|
1396
|
-
|
|
1397
|
-
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
const closeHttpServer = timeoutMs => new Promise(resolveClose => {
|
|
1914
|
+
if (!server) {
|
|
1915
|
+
resolveClose({ ok: true, listening: false });
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
let settled = false;
|
|
1919
|
+
const finish = ok => {
|
|
1920
|
+
if (settled) return;
|
|
1921
|
+
settled = true;
|
|
1922
|
+
clearTimeout(timer);
|
|
1923
|
+
resolveClose({ ok, listening: Boolean(server?.listening) });
|
|
1924
|
+
};
|
|
1925
|
+
const timer = setTimeout(() => {
|
|
1926
|
+
try { server.closeAllConnections?.(); } catch { /* exact HTTP owner already closed */ }
|
|
1927
|
+
finish(!server.listening);
|
|
1928
|
+
}, Math.max(100, Number(timeoutMs) || CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS));
|
|
1929
|
+
try {
|
|
1930
|
+
server.close(error => finish(!error));
|
|
1931
|
+
server.closeIdleConnections?.();
|
|
1932
|
+
} catch {
|
|
1933
|
+
finish(true);
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
const closeRuntime = async () => {
|
|
1938
|
+
const closeStartedAt = Date.now();
|
|
1939
|
+
closed = true;
|
|
1940
|
+
runtime.emit('runtime.http.stopping', { port });
|
|
1941
|
+
|
|
1942
|
+
const taskOwners = [...diagnosticTasks];
|
|
1943
|
+
const [diagnosticClose, httpClose] = await Promise.all([
|
|
1944
|
+
diagnosticCommandOwner.close(CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS),
|
|
1945
|
+
closeHttpServer(CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS)
|
|
1946
|
+
]);
|
|
1947
|
+
if (taskOwners.length > 0) {
|
|
1948
|
+
const remainingCloseMs = Math.max(
|
|
1949
|
+
0,
|
|
1950
|
+
CLIENT_DIAGNOSTIC_CLOSE_TIMEOUT_MS - (Date.now() - closeStartedAt)
|
|
1951
|
+
);
|
|
1952
|
+
if (remainingCloseMs > 0) {
|
|
1953
|
+
await Promise.race([
|
|
1954
|
+
Promise.allSettled(taskOwners),
|
|
1955
|
+
new Promise(resolve => setTimeout(resolve, remainingCloseMs))
|
|
1956
|
+
]);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const finalDiagnostics = diagnosticCommandOwner.getSnapshot();
|
|
1960
|
+
const result = {
|
|
1961
|
+
ok: diagnosticClose.ok === true
|
|
1962
|
+
&& httpClose.ok === true
|
|
1963
|
+
&& diagnosticTasks.size === 0
|
|
1964
|
+
&& finalDiagnostics.activeCount === 0
|
|
1965
|
+
&& finalDiagnostics.redirectedStreamCount === 0,
|
|
1966
|
+
diagnostics: {
|
|
1967
|
+
...diagnosticClose,
|
|
1968
|
+
taskCount: diagnosticTasks.size
|
|
1969
|
+
},
|
|
1970
|
+
http: httpClose
|
|
1971
|
+
};
|
|
1972
|
+
runtime.emit(
|
|
1973
|
+
result.ok ? 'client.diagnostics.closed' : 'client.diagnostics.close-timeout',
|
|
1974
|
+
{
|
|
1975
|
+
ownerId: finalDiagnostics.ownerId,
|
|
1976
|
+
activeCount: finalDiagnostics.activeCount,
|
|
1977
|
+
redirectedStreamCount: finalDiagnostics.redirectedStreamCount,
|
|
1978
|
+
taskCount: diagnosticTasks.size
|
|
1979
|
+
}
|
|
1980
|
+
);
|
|
1981
|
+
return result;
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
return {
|
|
1398
1985
|
runtime,
|
|
1399
|
-
waitForChoice,
|
|
1400
|
-
start,
|
|
1986
|
+
waitForChoice,
|
|
1987
|
+
start,
|
|
1401
1988
|
get url() { return `http://${host}:${port}/`; },
|
|
1402
1989
|
getLastChoice() { return lastChoice; },
|
|
1403
|
-
|
|
1990
|
+
acceptChoice(choice, message) {
|
|
1991
|
+
complete(choice, message);
|
|
1992
|
+
return lastChoice;
|
|
1993
|
+
},
|
|
1994
|
+
update(patch = {}) {
|
|
1404
1995
|
Object.assign(state, patch);
|
|
1405
1996
|
if (patch.agent && typeof patch.agent === 'object') {
|
|
1406
1997
|
state.agent = { ...state.agent, ...patch.agent };
|
|
@@ -1418,11 +2009,8 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1418
2009
|
if (Array.isArray(patch.endpointCandidates)) state.endpointCandidates = patch.endpointCandidates;
|
|
1419
2010
|
},
|
|
1420
2011
|
close() {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
if (gpuRefreshTimer) clearInterval(gpuRefreshTimer);
|
|
1424
|
-
if (networkRefreshTimer) clearInterval(networkRefreshTimer);
|
|
1425
|
-
try { server?.close(); } catch { /* already closed */ }
|
|
2012
|
+
if (!runtimeClosePromise) runtimeClosePromise = closeRuntime();
|
|
2013
|
+
return runtimeClosePromise;
|
|
1426
2014
|
},
|
|
1427
2015
|
getRoleRestartRequest() { return options.getRoleRestartRequest?.() || null; }
|
|
1428
2016
|
};
|