livedesk 0.1.253 → 0.1.255
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/bin/livedesk.js +236 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,15 +13,15 @@ This starts the LiveDesk Hub, opens the local screen wall, and accepts clients.
|
|
|
13
13
|
The Hub UI/API listens on `127.0.0.1` by default while the client endpoint
|
|
14
14
|
continues to listen on the LAN.
|
|
15
15
|
|
|
16
|
-
On startup, the launcher
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
client endpoint remains fixed at TCP `5197` by default so
|
|
21
|
-
router forwarding, and direct clients keep a stable contract.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
On startup, the launcher checks the Hub ports (`5179` and `5197`), identifies
|
|
17
|
+
stale LiveDesk Hub processes from their Node command line, stops each PID only
|
|
18
|
+
once, waits for the ports to be released and bindable, and then starts the new
|
|
19
|
+
Hub. Unrelated processes are preserved and reported with their PID, name, and
|
|
20
|
+
command line. The client endpoint remains fixed at TCP `5197` by default so
|
|
21
|
+
firewall rules, router forwarding, and direct clients keep a stable contract.
|
|
22
|
+
Pass `--no-clean` to disable the startup cleanup explicitly. If a child Hub
|
|
23
|
+
still loses a startup race with `EADDRINUSE`, the launcher performs one cleanup
|
|
24
|
+
and startup retry, then exits without looping.
|
|
25
25
|
|
|
26
26
|
## Client
|
|
27
27
|
|
package/bin/livedesk.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import net from 'node:net';
|
|
4
5
|
import { dirname, join, resolve } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { execFile, spawn } from 'node:child_process';
|
|
@@ -14,6 +15,9 @@ const packageRoot = resolve(__dirname, '..');
|
|
|
14
15
|
const DEFAULT_MANAGER_HTTP_PORT = 5179;
|
|
15
16
|
const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
16
17
|
const DEFAULT_MANAGER_URL = `http://127.0.0.1:${DEFAULT_MANAGER_HTTP_PORT}`;
|
|
18
|
+
const PORT_CLEANUP_TIMEOUT_MS = 3000;
|
|
19
|
+
const PORT_CLEANUP_POLL_MS = 100;
|
|
20
|
+
const HUB_STARTUP_RETRY_LIMIT = 1;
|
|
17
21
|
const MANAGER_STATE_DIR = join(os.homedir(), '.livedesk');
|
|
18
22
|
const MANAGER_STATE_PATH = join(MANAGER_STATE_DIR, 'manager.json');
|
|
19
23
|
|
|
@@ -212,6 +216,134 @@ function runQuiet(command, args) {
|
|
|
212
216
|
});
|
|
213
217
|
}
|
|
214
218
|
|
|
219
|
+
function waitMilliseconds(milliseconds) {
|
|
220
|
+
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function canBindPort(port, host = '0.0.0.0') {
|
|
224
|
+
return new Promise(resolve => {
|
|
225
|
+
const server = net.createServer();
|
|
226
|
+
let settled = false;
|
|
227
|
+
const finish = value => {
|
|
228
|
+
if (settled) return;
|
|
229
|
+
settled = true;
|
|
230
|
+
resolve(value);
|
|
231
|
+
};
|
|
232
|
+
server.once('error', () => finish(false));
|
|
233
|
+
server.listen(port, host, () => {
|
|
234
|
+
server.close(() => finish(true));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isKnownLiveDeskProcess(processName, commandLine) {
|
|
240
|
+
const normalizedName = String(processName || '').trim();
|
|
241
|
+
const normalizedCommandLine = String(commandLine || '').replaceAll('\\', '/');
|
|
242
|
+
return /^(?:node|node\.exe)$/i.test(normalizedName)
|
|
243
|
+
&& /livedesk/i.test(normalizedCommandLine)
|
|
244
|
+
&& /hub\/src\/server\.js/i.test(normalizedCommandLine)
|
|
245
|
+
&& /(?:node_modules\/livedesk|packages\/livedesk)\/hub\/src\/server\.js/i.test(normalizedCommandLine);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isPortConflictOutput(output) {
|
|
249
|
+
return /\bEADDRINUSE\b|address already in use|requires client endpoint TCP \d+/i.test(String(output || ''));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatPortOwner(record) {
|
|
253
|
+
return `${record?.Pid || 'unknown'}:${record?.ProcessName || 'unknown'} cmd=${String(record?.CommandLine || 'unavailable').replace(/\s+/g, ' ').slice(0, 240)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function assertPortCleanupReady(finalRecords, portStates) {
|
|
257
|
+
const blockedPorts = portStates.filter(port => port?.Ready !== true);
|
|
258
|
+
if (blockedPorts.length === 0) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const summary = blockedPorts.map(port => {
|
|
263
|
+
const owners = finalRecords
|
|
264
|
+
.filter(record => Number(record?.Port) === Number(port?.Port))
|
|
265
|
+
.map(formatPortOwner);
|
|
266
|
+
return `TCP ${port.Port}[available=${port.PortAvailable === true},bindable=${port.PortBindable === true},owners=${owners.length ? owners.join('; ') : 'owner not visible'}`;
|
|
267
|
+
}).join('], ');
|
|
268
|
+
throw new Error(`LiveDesk Hub startup cleanup could not release the configured port(s): ${summary}]. Unrelated processes are preserved; stop the owning process manually or choose an explicit --port/--remote-port value.`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function collectUnixPortRecords(ports) {
|
|
272
|
+
const records = [];
|
|
273
|
+
for (const port of ports) {
|
|
274
|
+
const lsof = await runQuiet('lsof', ['-nP', '-ti', `tcp:${port}`, '-sTCP:LISTEN']);
|
|
275
|
+
if (!lsof.ok && !lsof.stdout) {
|
|
276
|
+
throw new Error(`Could not inspect listening port TCP ${port}: ${lsof.stderr || lsof.error?.message || 'lsof failed'}`);
|
|
277
|
+
}
|
|
278
|
+
const pids = [...new Set(lsof.stdout.split(/\r?\n/).map(value => Number(value.trim())).filter(Number.isInteger))];
|
|
279
|
+
for (const pid of pids) {
|
|
280
|
+
const [nameResult, commandResult] = await Promise.all([
|
|
281
|
+
runQuiet('ps', ['-p', String(pid), '-o', 'comm=']),
|
|
282
|
+
runQuiet('ps', ['-p', String(pid), '-o', 'args='])
|
|
283
|
+
]);
|
|
284
|
+
const processName = nameResult.stdout.trim();
|
|
285
|
+
const commandLine = commandResult.stdout.trim();
|
|
286
|
+
records.push({
|
|
287
|
+
Port: port,
|
|
288
|
+
Pid: pid,
|
|
289
|
+
ProcessName: processName,
|
|
290
|
+
CommandLine: commandLine,
|
|
291
|
+
KnownLiveDesk: isKnownLiveDeskProcess(processName, commandLine),
|
|
292
|
+
Action: 'preserved'
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const unique = new Map();
|
|
297
|
+
for (const record of records) {
|
|
298
|
+
unique.set(`${record.Port}:${record.Pid}`, record);
|
|
299
|
+
}
|
|
300
|
+
return [...unique.values()];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function stopUnixProcessesOnPorts(ports) {
|
|
304
|
+
const records = await collectUnixPortRecords(ports);
|
|
305
|
+
const killedPids = new Set();
|
|
306
|
+
for (const record of records) {
|
|
307
|
+
if (!record.KnownLiveDesk || killedPids.has(record.Pid)) continue;
|
|
308
|
+
killedPids.add(record.Pid);
|
|
309
|
+
await runQuiet('kill', ['-KILL', String(record.Pid)]);
|
|
310
|
+
for (const matching of records.filter(candidate => candidate.Pid === record.Pid)) {
|
|
311
|
+
matching.Action = 'stopped';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const deadline = Date.now() + PORT_CLEANUP_TIMEOUT_MS;
|
|
316
|
+
let finalRecords = [];
|
|
317
|
+
let portStates = [];
|
|
318
|
+
do {
|
|
319
|
+
finalRecords = await collectUnixPortRecords(ports);
|
|
320
|
+
portStates = [];
|
|
321
|
+
for (const port of ports) {
|
|
322
|
+
const busy = finalRecords.some(record => Number(record.Port) === Number(port));
|
|
323
|
+
const bindable = busy ? false : await canBindPort(port);
|
|
324
|
+
portStates.push({
|
|
325
|
+
Port: port,
|
|
326
|
+
PortAvailable: !busy,
|
|
327
|
+
PortBindable: bindable,
|
|
328
|
+
Ready: !busy && bindable
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (portStates.every(state => state.Ready) || Date.now() >= deadline) break;
|
|
332
|
+
await waitMilliseconds(PORT_CLEANUP_POLL_MS);
|
|
333
|
+
} while (true);
|
|
334
|
+
|
|
335
|
+
const stopped = records.filter(record => record.Action === 'stopped');
|
|
336
|
+
if (stopped.length > 0) {
|
|
337
|
+
const stoppedOwners = new Map();
|
|
338
|
+
for (const record of stopped) {
|
|
339
|
+
if (!stoppedOwners.has(String(record.Pid))) stoppedOwners.set(String(record.Pid), record);
|
|
340
|
+
}
|
|
341
|
+
const summary = [...stoppedOwners.values()].map(record => `${record.Pid}:${record.ProcessName || 'node'}`).join(', ');
|
|
342
|
+
console.log(`Restarting LiveDesk Hub. Stopped stale LiveDesk owner(s) once: ${summary}`);
|
|
343
|
+
}
|
|
344
|
+
assertPortCleanupReady(finalRecords, portStates);
|
|
345
|
+
}
|
|
346
|
+
|
|
215
347
|
async function stopProcessesOnPorts(ports) {
|
|
216
348
|
const uniquePorts = [...new Set(ports.map(port => normalizePort(port, 0)).filter(Boolean))];
|
|
217
349
|
if (uniquePorts.length === 0) {
|
|
@@ -223,19 +355,6 @@ async function stopProcessesOnPorts(ports) {
|
|
|
223
355
|
'$ErrorActionPreference = "SilentlyContinue";',
|
|
224
356
|
`$ports = @(${uniquePorts.join(',')});`,
|
|
225
357
|
`$currentNodePid = ${process.pid};`,
|
|
226
|
-
' $liveDeskProcessPattern = "(?i)(?:livedesk|@livedesk)";',
|
|
227
|
-
' $hubServerProcessPattern = "(?i)(?:hub[\\\\/].*server\\.js|server\\.js)";',
|
|
228
|
-
'function Get-ProcessCommandChain($proc) {',
|
|
229
|
-
' $chain = @();',
|
|
230
|
-
' $current = $proc;',
|
|
231
|
-
' for ($depth = 0; $current -and $depth -lt 8; $depth++) {',
|
|
232
|
-
' $chain += [string]$current.CommandLine;',
|
|
233
|
-
' $parentId = [int]$current.ParentProcessId;',
|
|
234
|
-
' if ($parentId -le 0) { break }',
|
|
235
|
-
' $current = Get-CimInstance Win32_Process -Filter "ProcessId = $parentId" -ErrorAction SilentlyContinue;',
|
|
236
|
-
' }',
|
|
237
|
-
' return ($chain -join " ");',
|
|
238
|
-
'}',
|
|
239
358
|
'function Get-ListenConnections($port) {',
|
|
240
359
|
' $connections = @(Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue);',
|
|
241
360
|
' if ($connections.Count -gt 0) { return $connections }',
|
|
@@ -247,87 +366,93 @@ async function stopProcessesOnPorts(ports) {
|
|
|
247
366
|
' }',
|
|
248
367
|
' }',
|
|
249
368
|
'}',
|
|
250
|
-
'
|
|
251
|
-
' $connections = @(Get-ListenConnections $port | Where-Object { $_.OwningProcess -and $_.OwningProcess -ne $currentNodePid });',
|
|
369
|
+
'function Get-PortRecords($port) {',
|
|
370
|
+
' $connections = @(Get-ListenConnections $port | Where-Object { $_.OwningProcess -and $_.OwningProcess -ne $currentNodePid -and $_.OwningProcess -ne $PID });',
|
|
252
371
|
' foreach ($connection in $connections) {',
|
|
253
372
|
' $owner = [int]$connection.OwningProcess;',
|
|
254
373
|
' $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $owner" -ErrorAction SilentlyContinue;',
|
|
255
374
|
' $processName = if ($proc) { [string]$proc.Name } else { "" };',
|
|
256
375
|
' $commandLine = if ($proc) { [string]$proc.CommandLine } else { "" };',
|
|
257
|
-
' $
|
|
258
|
-
' $
|
|
259
|
-
'
|
|
376
|
+
' $isNodeProcess = $processName -match "(?i)^node(?:\\.exe)?$";',
|
|
377
|
+
' $hasLiveDeskToken = $commandLine -match "(?i)livedesk";',
|
|
378
|
+
' $hasHubServerPath = $commandLine -match "(?i)hub[\\\\/]src[\\\\/]server\\.js";',
|
|
379
|
+
' $hasPackagedHubPath = $commandLine -match "(?i)(?:node_modules|packages)[\\\\/]livedesk[\\\\/]hub[\\\\/]src[\\\\/]server\\.js";',
|
|
380
|
+
' $knownLiveDesk = [bool]($isNodeProcess -and $hasLiveDeskToken -and $hasHubServerPath -and $hasPackagedHubPath);',
|
|
381
|
+
' [pscustomobject]@{ Port = [int]$port; Pid = $owner; ProcessName = $processName; CommandLine = $commandLine; KnownLiveDesk = $knownLiveDesk; Action = "preserved"; KillAttempted = $false }',
|
|
260
382
|
' }',
|
|
261
383
|
'}',
|
|
262
|
-
'
|
|
263
|
-
'$
|
|
264
|
-
'
|
|
265
|
-
'
|
|
266
|
-
'
|
|
267
|
-
'
|
|
268
|
-
'}
|
|
269
|
-
'
|
|
270
|
-
'
|
|
271
|
-
' $
|
|
384
|
+
'function Test-PortBindable($port) {',
|
|
385
|
+
' $listener = $null;',
|
|
386
|
+
' try {',
|
|
387
|
+
' $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, [int]$port);',
|
|
388
|
+
' $listener.Start();',
|
|
389
|
+
' return $true;',
|
|
390
|
+
' } catch {',
|
|
391
|
+
' return $false;',
|
|
392
|
+
' } finally {',
|
|
393
|
+
' if ($listener) { $listener.Stop(); }',
|
|
272
394
|
' }',
|
|
273
395
|
'}',
|
|
274
|
-
'
|
|
275
|
-
'
|
|
276
|
-
'
|
|
396
|
+
'$records = @($ports | ForEach-Object { Get-PortRecords $_ } | Sort-Object Port,Pid -Unique);',
|
|
397
|
+
'$killableOwners = @($records | Where-Object { $_.KnownLiveDesk } | Group-Object Pid | ForEach-Object { @($_.Group)[0] });',
|
|
398
|
+
'foreach ($owner in $killableOwners) {',
|
|
399
|
+
' Stop-Process -Id $owner.Pid -Force -ErrorAction SilentlyContinue;',
|
|
400
|
+
' foreach ($record in @($records | Where-Object { [int]$_.Pid -eq [int]$owner.Pid })) {',
|
|
401
|
+
' $record.Action = "stopped";',
|
|
402
|
+
' $record.KillAttempted = $true;',
|
|
403
|
+
' }',
|
|
277
404
|
'}',
|
|
278
|
-
|
|
405
|
+
`$deadline = (Get-Date).ToUniversalTime().AddMilliseconds(${PORT_CLEANUP_TIMEOUT_MS});`,
|
|
406
|
+
'$finalRecords = @();',
|
|
407
|
+
'$portStates = @();',
|
|
279
408
|
'do {',
|
|
280
|
-
' $
|
|
281
|
-
' $
|
|
282
|
-
'
|
|
283
|
-
'
|
|
409
|
+
' $finalRecords = @($ports | ForEach-Object { Get-PortRecords $_ });',
|
|
410
|
+
' $busyPorts = @($finalRecords | Select-Object -ExpandProperty Port -Unique);',
|
|
411
|
+
' $portStates = @($ports | ForEach-Object {',
|
|
412
|
+
' $port = [int]$_;',
|
|
413
|
+
' $busy = $busyPorts -contains $port;',
|
|
414
|
+
' $bindable = if ($busy) { $false } else { Test-PortBindable $port };',
|
|
415
|
+
' [pscustomobject]@{ Port = $port; PortAvailable = [bool](-not $busy); PortBindable = [bool]$bindable; Ready = [bool](-not $busy -and $bindable) }',
|
|
416
|
+
' });',
|
|
417
|
+
' if ((@($portStates | Where-Object { -not $_.Ready }).Count -eq 0) -or (Get-Date).ToUniversalTime() -ge $deadline) { break }',
|
|
418
|
+
` Start-Sleep -Milliseconds ${PORT_CLEANUP_POLL_MS};`,
|
|
284
419
|
'} while ($true);',
|
|
285
|
-
'$
|
|
286
|
-
'foreach ($record in $records) {',
|
|
287
|
-
' $record.PortAvailable = $busyPortNumbers -notcontains $record.Port;',
|
|
288
|
-
' $record.ProcessGone = -not (Get-Process -Id $record.Pid -ErrorAction SilentlyContinue);',
|
|
289
|
-
'}',
|
|
290
|
-
'$records | ConvertTo-Json -Compress'
|
|
420
|
+
'[pscustomobject]@{ Records = @($records); FinalRecords = @($finalRecords); Ports = @($portStates) } | ConvertTo-Json -Compress -Depth 8'
|
|
291
421
|
].join(' ');
|
|
292
422
|
const result = await runQuiet('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script]);
|
|
293
423
|
if (!result.ok && !result.stdout) {
|
|
294
424
|
throw new Error(`Could not inspect existing LiveDesk Hub ports: ${result.stderr || result.error?.message || 'PowerShell failed'}`);
|
|
295
425
|
}
|
|
296
|
-
let
|
|
426
|
+
let payload = { Records: [], FinalRecords: [], Ports: [] };
|
|
297
427
|
if (result.stdout) {
|
|
298
428
|
try {
|
|
299
429
|
const parsed = JSON.parse(result.stdout);
|
|
300
|
-
|
|
430
|
+
payload = Array.isArray(parsed)
|
|
431
|
+
? { Records: parsed, FinalRecords: parsed, Ports: [] }
|
|
432
|
+
: parsed;
|
|
301
433
|
} catch {
|
|
302
434
|
throw new Error(`Could not parse existing LiveDesk Hub port state: ${result.stdout}`);
|
|
303
435
|
}
|
|
304
436
|
}
|
|
437
|
+
const records = Array.isArray(payload?.Records) ? payload.Records : [];
|
|
438
|
+
const finalRecords = Array.isArray(payload?.FinalRecords) ? payload.FinalRecords : [];
|
|
439
|
+
const portStates = Array.isArray(payload?.Ports) ? payload.Ports : [];
|
|
305
440
|
const stopped = records.filter(record => record?.Action === 'stopped');
|
|
306
441
|
if (stopped.length > 0) {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
442
|
+
const stoppedOwners = new Map();
|
|
443
|
+
for (const record of stopped) {
|
|
444
|
+
if (!stoppedOwners.has(String(record.Pid))) {
|
|
445
|
+
stoppedOwners.set(String(record.Pid), record);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const summary = [...stoppedOwners.values()].map(record => `${record.Pid}:${record.ProcessName || 'node'}`).join(', ');
|
|
449
|
+
console.log(`Restarting LiveDesk Hub. Stopped stale LiveDesk owner(s) once: ${summary}`);
|
|
314
450
|
}
|
|
451
|
+
assertPortCleanupReady(finalRecords, portStates);
|
|
315
452
|
return;
|
|
316
453
|
}
|
|
317
454
|
|
|
318
|
-
|
|
319
|
-
`pids="$(lsof -ti tcp:${port} -sTCP:LISTEN 2>/dev/null || true)"`,
|
|
320
|
-
'for pid in $pids; do',
|
|
321
|
-
' if [ "$pid" != "$$" ]; then',
|
|
322
|
-
' kill "$pid" 2>/dev/null || true',
|
|
323
|
-
' echo "$pid"',
|
|
324
|
-
' fi',
|
|
325
|
-
'done'
|
|
326
|
-
].join('; ')).join('; ');
|
|
327
|
-
const result = await runQuiet('sh', ['-c', script]);
|
|
328
|
-
if (result.stdout) {
|
|
329
|
-
console.log(`Restarting LiveDesk Hub. Cleared port owner(s): ${result.stdout.replace(/\r?\n/g, ', ')}`);
|
|
330
|
-
}
|
|
455
|
+
await stopUnixProcessesOnPorts(uniquePorts);
|
|
331
456
|
}
|
|
332
457
|
|
|
333
458
|
async function waitForManager(url = DEFAULT_MANAGER_URL, timeoutMs = 10000) {
|
|
@@ -386,23 +511,54 @@ async function runManager(args) {
|
|
|
386
511
|
LIVEDESK_WEB_DIST: existsSync(resolve(packagedWebDist, 'index.html')) ? packagedWebDist : (process.env.LIVEDESK_WEB_DIST || '')
|
|
387
512
|
};
|
|
388
513
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
514
|
+
let startupRetryCount = 0;
|
|
515
|
+
const startHubProcess = () => {
|
|
516
|
+
const stderrChunks = [];
|
|
517
|
+
const child = spawn(process.execPath, [hubEntry, ...options.forwarded], {
|
|
518
|
+
env,
|
|
519
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
520
|
+
windowsHide: false
|
|
521
|
+
});
|
|
394
522
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
process.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
523
|
+
child.stderr.on('data', chunk => {
|
|
524
|
+
const text = chunk.toString();
|
|
525
|
+
stderrChunks.push(text);
|
|
526
|
+
while (stderrChunks.join('').length > 16_000) {
|
|
527
|
+
stderrChunks.shift();
|
|
528
|
+
}
|
|
529
|
+
process.stderr.write(chunk);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
child.once('error', error => {
|
|
533
|
+
console.error(`Failed to start LiveDesk Hub: ${error.message}`);
|
|
534
|
+
process.exitCode = 1;
|
|
535
|
+
});
|
|
536
|
+
child.once('exit', (code, signal) => {
|
|
537
|
+
const startupOutput = stderrChunks.join('');
|
|
538
|
+
if (!signal
|
|
539
|
+
&& code !== 0
|
|
540
|
+
&& options.cleanPortsOnStart
|
|
541
|
+
&& startupRetryCount < HUB_STARTUP_RETRY_LIMIT
|
|
542
|
+
&& isPortConflictOutput(startupOutput)) {
|
|
543
|
+
startupRetryCount += 1;
|
|
544
|
+
console.warn('[LiveDesk Hub] Startup port conflict detected. Cleaning Hub ports and retrying once.');
|
|
545
|
+
void stopProcessesOnPorts([httpPort, remotePort])
|
|
546
|
+
.then(() => startHubProcess())
|
|
547
|
+
.catch(error => {
|
|
548
|
+
console.error(error?.message || error);
|
|
549
|
+
process.exitCode = 1;
|
|
550
|
+
});
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (signal) {
|
|
554
|
+
process.kill(process.pid, signal);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
process.exit(code ?? 0);
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
startHubProcess();
|
|
406
562
|
|
|
407
563
|
if (options.openBrowserOnStart) {
|
|
408
564
|
void waitForManager(openUrl).then(ok => {
|