@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.
- 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 +775 -127
- 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';
|
|
@@ -165,6 +172,12 @@ function readCpuTotals() {
|
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
function finiteNumber(value) {
|
|
175
|
+
if (value === null
|
|
176
|
+
|| value === undefined
|
|
177
|
+
|| typeof value === 'boolean'
|
|
178
|
+
|| (typeof value === 'string' && value.trim() === '')) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
168
181
|
const number = Number(value);
|
|
169
182
|
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
170
183
|
}
|
|
@@ -175,15 +188,406 @@ function percentFromRatio(value) {
|
|
|
175
188
|
return Math.round(Math.max(0, Math.min(100, number)) * 10) / 10;
|
|
176
189
|
}
|
|
177
190
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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)
|
|
186
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
|
+
};
|
|
187
591
|
}
|
|
188
592
|
|
|
189
593
|
export function parseNvidiaSmiOutput(output) {
|
|
@@ -274,17 +678,30 @@ function activeNetworkInterfaces() {
|
|
|
274
678
|
return interfaces;
|
|
275
679
|
}
|
|
276
680
|
|
|
277
|
-
async function
|
|
278
|
-
if (
|
|
279
|
-
|
|
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' };
|
|
280
687
|
}
|
|
281
|
-
if (
|
|
282
|
-
|
|
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' };
|
|
283
693
|
}
|
|
284
694
|
try {
|
|
285
|
-
return
|
|
695
|
+
return {
|
|
696
|
+
ok: true,
|
|
697
|
+
value: parseLinuxNetworkOutput(readFileSync('/proc/net/dev', 'utf8'))
|
|
698
|
+
};
|
|
286
699
|
} catch {
|
|
287
|
-
return {
|
|
700
|
+
return {
|
|
701
|
+
ok: false,
|
|
702
|
+
value: { receivedBytes: 0, sentBytes: 0 },
|
|
703
|
+
error: 'network-counters-unavailable'
|
|
704
|
+
};
|
|
288
705
|
}
|
|
289
706
|
}
|
|
290
707
|
|
|
@@ -300,6 +717,53 @@ function normalizeGpu(value) {
|
|
|
300
717
|
};
|
|
301
718
|
}
|
|
302
719
|
|
|
720
|
+
export function normalizeGpuIdentityName(value) {
|
|
721
|
+
return normalizeString(value, 160)
|
|
722
|
+
.normalize('NFKC')
|
|
723
|
+
.replace(/\((?:r|tm)\)/giu, '')
|
|
724
|
+
.replace(/[®™]/gu, '')
|
|
725
|
+
.toLocaleLowerCase('en-US')
|
|
726
|
+
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
|
727
|
+
.trim();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function mergeGpuSnapshots(platformGpus = [], telemetryGpus = []) {
|
|
731
|
+
const merged = [];
|
|
732
|
+
const indexesByIdentity = new Map();
|
|
733
|
+
for (const value of Array.isArray(platformGpus) ? platformGpus : []) {
|
|
734
|
+
const gpu = normalizeGpu(value);
|
|
735
|
+
const identity = normalizeGpuIdentityName(gpu?.name);
|
|
736
|
+
if (!gpu || !identity) continue;
|
|
737
|
+
const index = merged.length;
|
|
738
|
+
merged.push(gpu);
|
|
739
|
+
const indexes = indexesByIdentity.get(identity) || [];
|
|
740
|
+
indexes.push(index);
|
|
741
|
+
indexesByIdentity.set(identity, indexes);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const matchedIndexes = new Set();
|
|
745
|
+
for (const value of Array.isArray(telemetryGpus) ? telemetryGpus : []) {
|
|
746
|
+
const gpu = normalizeGpu(value);
|
|
747
|
+
const identity = normalizeGpuIdentityName(gpu?.name);
|
|
748
|
+
if (!gpu || !identity) continue;
|
|
749
|
+
const existingIndex = (indexesByIdentity.get(identity) || [])
|
|
750
|
+
.find(index => !matchedIndexes.has(index));
|
|
751
|
+
if (existingIndex === undefined) {
|
|
752
|
+
merged.push(gpu);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
matchedIndexes.add(existingIndex);
|
|
756
|
+
const existing = merged[existingIndex];
|
|
757
|
+
merged[existingIndex] = {
|
|
758
|
+
...existing,
|
|
759
|
+
usagePercent: gpu.usagePercent ?? existing.usagePercent,
|
|
760
|
+
memoryTotalBytes: gpu.memoryTotalBytes ?? existing.memoryTotalBytes,
|
|
761
|
+
memoryUsedBytes: gpu.memoryUsedBytes ?? existing.memoryUsedBytes
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
return merged;
|
|
765
|
+
}
|
|
766
|
+
|
|
303
767
|
function normalizeDisk(value) {
|
|
304
768
|
if (!value || typeof value !== 'object') return null;
|
|
305
769
|
const mount = normalizeString(value.mount, 80);
|
|
@@ -391,7 +855,7 @@ export function readMacDiskSnapshots(options = {}) {
|
|
|
391
855
|
return disks;
|
|
392
856
|
}
|
|
393
857
|
|
|
394
|
-
async function readWindowsHardwareSnapshot() {
|
|
858
|
+
async function readWindowsHardwareSnapshot(commandOwner) {
|
|
395
859
|
const script = [
|
|
396
860
|
'$ErrorActionPreference="Stop"',
|
|
397
861
|
'$OutputEncoding=[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false)',
|
|
@@ -400,7 +864,7 @@ async function readWindowsHardwareSnapshot() {
|
|
|
400
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 } })',
|
|
401
865
|
'[pscustomobject]@{ gpus=$gpus; disks=$disks } | ConvertTo-Json -Compress -Depth 5'
|
|
402
866
|
].join('; ');
|
|
403
|
-
return parseWindowsHardwareOutput(await
|
|
867
|
+
return parseWindowsHardwareOutput(await commandOwner.capture('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], 5000));
|
|
404
868
|
}
|
|
405
869
|
|
|
406
870
|
function parseMacGpuOutput(output) {
|
|
@@ -414,34 +878,45 @@ function parseMacGpuOutput(output) {
|
|
|
414
878
|
}
|
|
415
879
|
}
|
|
416
880
|
|
|
417
|
-
async function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
} else {
|
|
424
|
-
const disk = process.platform === 'darwin' ? null : readRootDiskSnapshot();
|
|
425
|
-
const gpus = process.platform === 'darwin'
|
|
426
|
-
? parseMacGpuOutput(await captureCommand('system_profiler', ['SPDisplaysDataType', '-json'], 5000))
|
|
427
|
-
: [];
|
|
428
|
-
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' };
|
|
429
887
|
}
|
|
430
|
-
|
|
431
|
-
|
|
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();
|
|
432
899
|
return {
|
|
433
|
-
|
|
434
|
-
disks:
|
|
435
|
-
|
|
900
|
+
ok: Boolean(disk),
|
|
901
|
+
value: { gpus: [], disks: disk ? [disk] : [] },
|
|
902
|
+
error: disk ? '' : 'root-disk-unavailable'
|
|
436
903
|
};
|
|
437
904
|
}
|
|
438
905
|
|
|
439
|
-
async function
|
|
440
|
-
|
|
441
|
-
|
|
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,
|
|
442
913
|
['--query-gpu=name,utilization.gpu,memory.total,memory.used', '--format=csv,noheader,nounits'],
|
|
443
914
|
3000
|
|
444
|
-
)
|
|
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' };
|
|
445
920
|
}
|
|
446
921
|
|
|
447
922
|
function normalizeOrigin(value) {
|
|
@@ -641,19 +1116,76 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
641
1116
|
const deviceId = normalizeString(options.deviceId || process.env.LIVEDESK_DEVICE_ID, 160);
|
|
642
1117
|
const deviceName = normalizeString(options.deviceName || os.hostname(), 160) || os.hostname();
|
|
643
1118
|
const appVersion = normalizeString(options.appVersion || process.env.LIVEDESK_NPM_LAUNCHER_VERSION, 80) || 'dev';
|
|
644
|
-
const savedSession = options.
|
|
1119
|
+
const savedSession = options.loadSavedSession === false
|
|
1120
|
+
? null
|
|
1121
|
+
: options.savedSession?.refresh_token
|
|
1122
|
+
? options.savedSession
|
|
1123
|
+
: readSavedSession();
|
|
645
1124
|
const savedSessionPersisted = Boolean(savedSession?.refresh_token);
|
|
646
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
|
+
);
|
|
647
1134
|
let previousCpuTotals = readCpuTotals();
|
|
648
|
-
let hardwareSnapshot = {
|
|
1135
|
+
let hardwareSnapshot = {
|
|
1136
|
+
gpus: [],
|
|
1137
|
+
disks: [],
|
|
1138
|
+
collectedAt: '',
|
|
1139
|
+
collecting: false,
|
|
1140
|
+
lastError: options.diagnosticsEnabled === false ? 'diagnostics-disabled' : 'not-requested'
|
|
1141
|
+
};
|
|
649
1142
|
let networkSnapshot = { interfaces: activeNetworkInterfaces(), receivedBytes: 0, sentBytes: 0, receiveBytesPerSecond: 0, sendBytesPerSecond: 0, sampledAt: '' };
|
|
650
1143
|
let previousNetworkTotals = null;
|
|
651
|
-
let
|
|
652
|
-
let
|
|
653
|
-
let
|
|
654
|
-
let
|
|
655
|
-
let
|
|
656
|
-
|
|
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
|
+
});
|
|
657
1189
|
const csrfToken = randomBytes(32).toString('hex');
|
|
658
1190
|
const state = {
|
|
659
1191
|
route: '/computer',
|
|
@@ -703,72 +1235,81 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
703
1235
|
});
|
|
704
1236
|
runtime.emit('runtime.waiting-auth', { role: 'client' });
|
|
705
1237
|
|
|
706
|
-
const
|
|
707
|
-
if (
|
|
708
|
-
|
|
1238
|
+
const requestDiagnosticRefresh = () => {
|
|
1239
|
+
if (closed || options.diagnosticsEnabled === false) return Promise.resolve();
|
|
1240
|
+
if (diagnosticRefreshInFlight) return diagnosticRefreshInFlight;
|
|
1241
|
+
lastDiagnosticRequestAt = new Date().toISOString();
|
|
709
1242
|
hardwareSnapshot = { ...hardwareSnapshot, collecting: true };
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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));
|
|
717
1258
|
hardwareSnapshot = {
|
|
718
|
-
|
|
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(),
|
|
719
1265
|
collecting: false,
|
|
720
|
-
lastError:
|
|
1266
|
+
lastError: errors.join(', ')
|
|
721
1267
|
};
|
|
722
|
-
} finally {
|
|
723
|
-
hardwareRefreshInFlight = false;
|
|
724
|
-
}
|
|
725
|
-
};
|
|
726
1268
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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 };
|
|
734
1290
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
sampledAt: new Date(sampledAtMs).toISOString()
|
|
756
|
-
};
|
|
757
|
-
previousNetworkTotals = { ...totals, sampledAtMs };
|
|
758
|
-
} finally {
|
|
759
|
-
networkRefreshInFlight = false;
|
|
760
|
-
}
|
|
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) {
|
|
1303
|
+
hardwareSnapshot = {
|
|
1304
|
+
...hardwareSnapshot,
|
|
1305
|
+
collecting: false,
|
|
1306
|
+
lastError: hardwareSnapshot.lastError || 'diagnostic-refresh-failed'
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
return diagnosticRefreshInFlight;
|
|
761
1311
|
};
|
|
762
1312
|
|
|
763
|
-
void refreshHardwareSnapshot();
|
|
764
|
-
void refreshNetworkSnapshot();
|
|
765
|
-
hardwareRefreshTimer = setInterval(() => { void refreshHardwareSnapshot(); }, 60_000);
|
|
766
|
-
hardwareRefreshTimer.unref?.();
|
|
767
|
-
gpuRefreshTimer = setInterval(() => { void refreshGpuSnapshot(); }, 2500);
|
|
768
|
-
gpuRefreshTimer.unref?.();
|
|
769
|
-
networkRefreshTimer = setInterval(() => { void refreshNetworkSnapshot(); }, 2000);
|
|
770
|
-
networkRefreshTimer.unref?.();
|
|
771
|
-
|
|
772
1313
|
const recordAuthAttempt = (result, error = '') => {
|
|
773
1314
|
const message = normalizeString(error, 240);
|
|
774
1315
|
state.auth = { ...state.auth, lastAttemptAt: new Date().toISOString(), lastResult: normalizeString(result, 80), lastError: message };
|
|
@@ -820,6 +1361,7 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
820
1361
|
lastAttemptAt: state.connectedAt,
|
|
821
1362
|
lastResult: 'accepted',
|
|
822
1363
|
lastError: '',
|
|
1364
|
+
persisted: Boolean(choice?.session?.refresh_token) || state.auth.persisted,
|
|
823
1365
|
email: accountProfile.email || state.auth.email,
|
|
824
1366
|
name: accountProfile.name || state.auth.name,
|
|
825
1367
|
avatarUrl: accountProfile.avatarUrl || state.auth.avatarUrl
|
|
@@ -885,7 +1427,17 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
885
1427
|
disks: hardwareSnapshot.disks,
|
|
886
1428
|
network: networkSnapshot,
|
|
887
1429
|
hardwareCollectedAt: hardwareSnapshot.collectedAt,
|
|
888
|
-
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
|
+
}
|
|
889
1441
|
},
|
|
890
1442
|
permissions: {
|
|
891
1443
|
screenCapture: state.agent.state === 'running',
|
|
@@ -954,11 +1506,16 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
954
1506
|
respondJson(200, { ok: true, product: 'LiveDesk', role: 'client', timestamp: new Date().toISOString() });
|
|
955
1507
|
return;
|
|
956
1508
|
}
|
|
957
|
-
if (pathname === '/api/
|
|
1509
|
+
if (pathname === '/api/client/diagnostics') {
|
|
1510
|
+
await requestDiagnosticRefresh();
|
|
958
1511
|
respondJson(200, getState());
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
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') {
|
|
962
1519
|
respondJson(200, { ok: true, events: runtime.getEvents() });
|
|
963
1520
|
return;
|
|
964
1521
|
}
|
|
@@ -1295,14 +1852,33 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1295
1852
|
|
|
1296
1853
|
const start = () => new Promise((resolveStart, rejectStart) => {
|
|
1297
1854
|
if (process.env.LIVEDESK_DESKTOP_HOST === '1') clearSavedSession();
|
|
1855
|
+
let settled = false;
|
|
1298
1856
|
server = createServer((req, res) => {
|
|
1299
1857
|
void handleRequest(req, res).catch(error => {
|
|
1300
1858
|
if (!res.headersSent) sendJson(res, 500, { ok: false, error: normalizeString(error?.message || error) }, req.headers.origin, port, trustedWebOrigins);
|
|
1301
1859
|
else res.end();
|
|
1302
|
-
});
|
|
1303
|
-
});
|
|
1304
|
-
|
|
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
|
+
});
|
|
1305
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);
|
|
1306
1882
|
runtime.emit('runtime.http.started', { host, port });
|
|
1307
1883
|
if (initialChoice) {
|
|
1308
1884
|
setImmediate(() => {
|
|
@@ -1332,15 +1908,90 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1332
1908
|
}
|
|
1333
1909
|
resolveStart();
|
|
1334
1910
|
});
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
|
|
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 {
|
|
1338
1985
|
runtime,
|
|
1339
|
-
waitForChoice,
|
|
1340
|
-
start,
|
|
1986
|
+
waitForChoice,
|
|
1987
|
+
start,
|
|
1341
1988
|
get url() { return `http://${host}:${port}/`; },
|
|
1342
1989
|
getLastChoice() { return lastChoice; },
|
|
1343
|
-
|
|
1990
|
+
acceptChoice(choice, message) {
|
|
1991
|
+
complete(choice, message);
|
|
1992
|
+
return lastChoice;
|
|
1993
|
+
},
|
|
1994
|
+
update(patch = {}) {
|
|
1344
1995
|
Object.assign(state, patch);
|
|
1345
1996
|
if (patch.agent && typeof patch.agent === 'object') {
|
|
1346
1997
|
state.agent = { ...state.agent, ...patch.agent };
|
|
@@ -1358,11 +2009,8 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1358
2009
|
if (Array.isArray(patch.endpointCandidates)) state.endpointCandidates = patch.endpointCandidates;
|
|
1359
2010
|
},
|
|
1360
2011
|
close() {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (gpuRefreshTimer) clearInterval(gpuRefreshTimer);
|
|
1364
|
-
if (networkRefreshTimer) clearInterval(networkRefreshTimer);
|
|
1365
|
-
try { server?.close(); } catch { /* already closed */ }
|
|
2012
|
+
if (!runtimeClosePromise) runtimeClosePromise = closeRuntime();
|
|
2013
|
+
return runtimeClosePromise;
|
|
1366
2014
|
},
|
|
1367
2015
|
getRoleRestartRequest() { return options.getRoleRestartRequest?.() || null; }
|
|
1368
2016
|
};
|