livedesk 0.1.249 → 0.1.251
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 -6
- package/bin/livedesk.js +37 -12
- package/hub/src/remote-hub.js +71 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,12 +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
|
-
|
|
21
|
-
the
|
|
16
|
+
On startup, the launcher finds stale processes whose command line identifies
|
|
17
|
+
them as a LiveDesk Hub, even if the old Hub is still starting and has not
|
|
18
|
+
finished binding its ports. It also checks the Hub ports (`5179` and `5197`),
|
|
19
|
+
waits for stale LiveDesk processes to exit, and then starts the new Hub. An
|
|
20
|
+
unrelated process is preserved and reported instead of being terminated. If
|
|
21
|
+
the client port remains occupied, the Hub automatically retries on a nearby
|
|
22
|
+
port and publishes that actual endpoint to clients. The HTTP manager port
|
|
23
|
+
remains strict so a web UI collision is reported clearly. Pass `--no-clean` to
|
|
24
|
+
disable the startup cleanup explicitly.
|
|
22
25
|
|
|
23
26
|
## Client
|
|
24
27
|
|
package/bin/livedesk.js
CHANGED
|
@@ -37,7 +37,8 @@ Hub options:
|
|
|
37
37
|
--host <host> Hub HTTP host. Default: 127.0.0.1
|
|
38
38
|
--port <port> Hub HTTP port. Default: 5179
|
|
39
39
|
--remote-port <port>
|
|
40
|
-
|
|
40
|
+
Preferred client connection port. Default: 5197; if busy,
|
|
41
|
+
LiveDesk retries on a nearby port automatically.
|
|
41
42
|
--no-clean Do not stop existing processes on the Hub ports.
|
|
42
43
|
|
|
43
44
|
Common:
|
|
@@ -212,7 +213,7 @@ function runQuiet(command, args) {
|
|
|
212
213
|
});
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
async function stopProcessesOnPorts(ports) {
|
|
216
|
+
async function stopProcessesOnPorts(ports, { allowBusyPorts = false } = {}) {
|
|
216
217
|
const uniquePorts = [...new Set(ports.map(port => normalizePort(port, 0)).filter(Boolean))];
|
|
217
218
|
if (uniquePorts.length === 0) {
|
|
218
219
|
return;
|
|
@@ -223,31 +224,51 @@ async function stopProcessesOnPorts(ports) {
|
|
|
223
224
|
'$ErrorActionPreference = "SilentlyContinue";',
|
|
224
225
|
`$ports = @(${uniquePorts.join(',')});`,
|
|
225
226
|
`$currentNodePid = ${process.pid};`,
|
|
227
|
+
" $hubProcessPattern = '(?i)(?:livedesk|@livedesk).*hub[\\\\/].*server\\.js';",
|
|
228
|
+
'function Get-ListenConnections($port) {',
|
|
229
|
+
' $connections = @(Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue);',
|
|
230
|
+
' if ($connections.Count -gt 0) { return $connections }',
|
|
231
|
+
' $pattern = "^\\s*TCP\\s+\\S+:" + $port + "\\s+\\S+\\s+LISTENING\\s+(\\d+)\\s*$";',
|
|
232
|
+
' foreach ($line in @(netstat -ano -p tcp)) {',
|
|
233
|
+
' $match = [regex]::Match([string]$line, $pattern);',
|
|
234
|
+
' if ($match.Success) {',
|
|
235
|
+
' [pscustomobject]@{ LocalPort = [int]$port; OwningProcess = [int]$match.Groups[1].Value }',
|
|
236
|
+
' }',
|
|
237
|
+
' }',
|
|
238
|
+
'}',
|
|
226
239
|
'$records = foreach ($port in $ports) {',
|
|
227
|
-
' $connections = @(Get-
|
|
240
|
+
' $connections = @(Get-ListenConnections $port | Where-Object { $_.OwningProcess -and $_.OwningProcess -ne $currentNodePid });',
|
|
228
241
|
' foreach ($connection in $connections) {',
|
|
229
242
|
' $owner = [int]$connection.OwningProcess;',
|
|
230
243
|
' $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $owner" -ErrorAction SilentlyContinue;',
|
|
231
244
|
' $processName = if ($proc) { [string]$proc.Name } else { "" };',
|
|
232
245
|
' $commandLine = if ($proc) { [string]$proc.CommandLine } else { "" };',
|
|
233
246
|
' $knownLiveDesk = $commandLine -match "(?i)(?:livedesk|@livedesk).*hub[\\\\/].*server\\.js";',
|
|
234
|
-
' [pscustomobject]@{ Port = [int]$port; Pid = $owner; ProcessName = $processName; CommandLine = $commandLine; KnownLiveDesk = [bool]$knownLiveDesk; Action = "preserved" }',
|
|
247
|
+
' [pscustomobject]@{ Port = [int]$port; Pid = $owner; ProcessName = $processName; CommandLine = $commandLine; KnownLiveDesk = [bool]$knownLiveDesk; Action = "preserved"; PortAvailable = $false; ProcessGone = $false }',
|
|
235
248
|
' }',
|
|
236
249
|
'}',
|
|
237
250
|
'$records = @($records | Sort-Object Port,Pid -Unique);',
|
|
251
|
+
'$hubProcesses = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -and $_.ProcessId -ne $currentNodePid -and ([string]$_.Name) -ieq "node.exe" -and ([string]$_.CommandLine) -match $hubProcessPattern });',
|
|
252
|
+
'foreach ($proc in $hubProcesses) {',
|
|
253
|
+
' if (-not (@($records | Where-Object { $_.Pid -eq [int]$proc.ProcessId }).Count)) {',
|
|
254
|
+
' $records += [pscustomobject]@{ Port = 0; Pid = [int]$proc.ProcessId; ProcessName = [string]$proc.Name; CommandLine = [string]$proc.CommandLine; KnownLiveDesk = $true; Action = "preserved"; PortAvailable = $false; ProcessGone = $false }',
|
|
255
|
+
' }',
|
|
256
|
+
'}',
|
|
238
257
|
'foreach ($record in @($records | Where-Object { $_.KnownLiveDesk })) {',
|
|
239
258
|
' Stop-Process -Id $record.Pid -Force -ErrorAction SilentlyContinue;',
|
|
240
259
|
' $record.Action = "stopped";',
|
|
241
260
|
'}',
|
|
242
261
|
'$deadline = (Get-Date).ToUniversalTime().AddMilliseconds(3000);',
|
|
243
262
|
'do {',
|
|
244
|
-
' $busyPorts = @($ports |
|
|
245
|
-
'
|
|
263
|
+
' $busyPorts = @($ports | ForEach-Object { Get-ListenConnections $_ });',
|
|
264
|
+
' $busyHubPids = @($records | Where-Object { $_.KnownLiveDesk } | ForEach-Object { Get-Process -Id $_.Pid -ErrorAction SilentlyContinue });',
|
|
265
|
+
' if (($busyPorts.Count -eq 0 -and $busyHubPids.Count -eq 0) -or (Get-Date).ToUniversalTime() -ge $deadline) { break }',
|
|
246
266
|
' Start-Sleep -Milliseconds 100;',
|
|
247
267
|
'} while ($true);',
|
|
248
268
|
'$busyPortNumbers = @($busyPorts | Select-Object -ExpandProperty LocalPort -Unique);',
|
|
249
269
|
'foreach ($record in $records) {',
|
|
250
270
|
' $record.PortAvailable = $busyPortNumbers -notcontains $record.Port;',
|
|
271
|
+
' $record.ProcessGone = -not (Get-Process -Id $record.Pid -ErrorAction SilentlyContinue);',
|
|
251
272
|
'}',
|
|
252
273
|
'$records | ConvertTo-Json -Compress'
|
|
253
274
|
].join(' ');
|
|
@@ -265,15 +286,18 @@ async function stopProcessesOnPorts(ports) {
|
|
|
265
286
|
}
|
|
266
287
|
}
|
|
267
288
|
const stopped = records.filter(record => record?.Action === 'stopped');
|
|
268
|
-
const preserved = records.filter(record => record?.Action !== 'stopped');
|
|
269
289
|
if (stopped.length > 0) {
|
|
270
|
-
const summary = stopped.map(record => `${record.Pid}:${record.ProcessName || 'node'}
|
|
290
|
+
const summary = stopped.map(record => `${record.Pid}:${record.ProcessName || 'node'}${record.Port ? `@${record.Port}` : ''}`).join(', ');
|
|
271
291
|
console.log(`Restarting LiveDesk Hub. Stopped stale LiveDesk owner(s): ${summary}`);
|
|
272
292
|
}
|
|
273
|
-
const blocked = records.filter(record => record?.PortAvailable !== true);
|
|
293
|
+
const blocked = records.filter(record => record?.PortAvailable !== true || (record?.Port === 0 && record?.ProcessGone !== true));
|
|
274
294
|
if (blocked.length > 0) {
|
|
275
|
-
const summary = blocked.map(record => `${record.Pid}:${record.ProcessName || 'unknown'}@${record.Port}`).join(', ');
|
|
276
|
-
|
|
295
|
+
const summary = blocked.map(record => `${record.Pid}:${record.ProcessName || 'unknown'}@${record.Port}[port-free=${record.PortAvailable === true},process-gone=${record.ProcessGone === true}]`).join(', ');
|
|
296
|
+
const message = `LiveDesk Hub startup cleanup could not release port/process owner(s): ${summary}`;
|
|
297
|
+
if (!allowBusyPorts) {
|
|
298
|
+
throw new Error(`${message}. If this is an unrelated process, use --port or stop it manually.`);
|
|
299
|
+
}
|
|
300
|
+
console.warn(`${message}. The client endpoint will retry on another port automatically.`);
|
|
277
301
|
}
|
|
278
302
|
return;
|
|
279
303
|
}
|
|
@@ -331,7 +355,8 @@ async function runManager(args) {
|
|
|
331
355
|
|| getStablePairToken();
|
|
332
356
|
|
|
333
357
|
if (options.cleanPortsOnStart) {
|
|
334
|
-
await stopProcessesOnPorts([httpPort
|
|
358
|
+
await stopProcessesOnPorts([httpPort]);
|
|
359
|
+
await stopProcessesOnPorts([remotePort], { allowBusyPorts: true });
|
|
335
360
|
}
|
|
336
361
|
|
|
337
362
|
const internalHubEntry = resolve(packageRoot, 'hub', 'src', 'server.js');
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -4,6 +4,8 @@ import crypto from 'crypto';
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
6
6
|
const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
|
|
7
|
+
const DEFAULT_REMOTE_HUB_PORT_FALLBACK_ATTEMPTS = 32;
|
|
8
|
+
const REMOTE_HUB_AUTH_CALLBACK_PORT = 5198;
|
|
7
9
|
const DEFAULT_HEARTBEAT_MS = 5000;
|
|
8
10
|
const DEFAULT_AGENT_TASK_TIMEOUT_MS = 120000;
|
|
9
11
|
const MAX_LINE_CHARS = 4 * 1024 * 1024;
|
|
@@ -1417,6 +1419,14 @@ export function createRemoteHub(options = {}) {
|
|
|
1417
1419
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
1418
1420
|
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
1419
1421
|
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
1422
|
+
const remotePortFallbackEnabled = isEnabledValue(env.REMOTE_HUB_PORT_FALLBACK, true)
|
|
1423
|
+
&& !isDisabledValue(env.REMOTE_HUB_PORT_FALLBACK_DISABLED);
|
|
1424
|
+
const remotePortFallbackStrict = isEnabledValue(env.REMOTE_HUB_PORT_FALLBACK_STRICT, false);
|
|
1425
|
+
const remotePortFallbackAttempts = clampNumber(
|
|
1426
|
+
env.REMOTE_HUB_PORT_FALLBACK_ATTEMPTS,
|
|
1427
|
+
1,
|
|
1428
|
+
128,
|
|
1429
|
+
DEFAULT_REMOTE_HUB_PORT_FALLBACK_ATTEMPTS);
|
|
1420
1430
|
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
1421
1431
|
const taskTimeoutMs = clampNumber(
|
|
1422
1432
|
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
@@ -4371,21 +4381,31 @@ export function createRemoteHub(options = {}) {
|
|
|
4371
4381
|
});
|
|
4372
4382
|
}
|
|
4373
4383
|
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4384
|
+
function getNextFallbackPort(currentPort, attemptedPorts) {
|
|
4385
|
+
for (let offset = 1; offset <= 65535; offset += 1) {
|
|
4386
|
+
const candidate = (currentPort + offset) % 65536;
|
|
4387
|
+
if (candidate < 1 || candidate === REMOTE_HUB_AUTH_CALLBACK_PORT || attemptedPorts.has(candidate)) {
|
|
4388
|
+
continue;
|
|
4389
|
+
}
|
|
4390
|
+
return candidate;
|
|
4377
4391
|
}
|
|
4392
|
+
return 0;
|
|
4393
|
+
}
|
|
4378
4394
|
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4395
|
+
function listenOnPort(port) {
|
|
4396
|
+
return new Promise((resolve, reject) => {
|
|
4397
|
+
const candidateServer = net.createServer(handleSocket);
|
|
4398
|
+
server = candidateServer;
|
|
4399
|
+
candidateServer.once('error', err => {
|
|
4382
4400
|
lastError = err?.message || String(err);
|
|
4383
|
-
server
|
|
4401
|
+
if (server === candidateServer) {
|
|
4402
|
+
server = null;
|
|
4403
|
+
}
|
|
4384
4404
|
reject(err);
|
|
4385
4405
|
});
|
|
4386
|
-
|
|
4406
|
+
candidateServer.listen(port, host, () => {
|
|
4387
4407
|
started = true;
|
|
4388
|
-
boundPort =
|
|
4408
|
+
boundPort = candidateServer.address()?.port || port;
|
|
4389
4409
|
lastError = '';
|
|
4390
4410
|
logEvent('remote', `LiveDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
4391
4411
|
if (host === '0.0.0.0' || host === '::') {
|
|
@@ -4395,6 +4415,48 @@ export function createRemoteHub(options = {}) {
|
|
|
4395
4415
|
resolve();
|
|
4396
4416
|
});
|
|
4397
4417
|
});
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
async function start() {
|
|
4421
|
+
if (!enabled || started) {
|
|
4422
|
+
return getStatus({ includeSecrets: false });
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
const fallbackAllowed = remotePortFallbackEnabled
|
|
4426
|
+
&& !remotePortFallbackStrict
|
|
4427
|
+
&& requestedPort > 0;
|
|
4428
|
+
const attemptedPorts = new Set();
|
|
4429
|
+
let candidatePort = requestedPort;
|
|
4430
|
+
let lastBindError = null;
|
|
4431
|
+
|
|
4432
|
+
for (let attempt = 0; attempt < remotePortFallbackAttempts; attempt += 1) {
|
|
4433
|
+
attemptedPorts.add(candidatePort);
|
|
4434
|
+
try {
|
|
4435
|
+
await listenOnPort(candidatePort);
|
|
4436
|
+
if (candidatePort !== requestedPort) {
|
|
4437
|
+
logWarn(
|
|
4438
|
+
'remote',
|
|
4439
|
+
`Client port ${requestedPort} is already in use; LiveDesk Hub is using ${candidatePort}.`);
|
|
4440
|
+
}
|
|
4441
|
+
lastBindError = null;
|
|
4442
|
+
break;
|
|
4443
|
+
} catch (error) {
|
|
4444
|
+
lastBindError = error;
|
|
4445
|
+
if (error?.code !== 'EADDRINUSE' || !fallbackAllowed) {
|
|
4446
|
+
throw error;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
const nextPort = getNextFallbackPort(candidatePort, attemptedPorts);
|
|
4450
|
+
if (!nextPort) {
|
|
4451
|
+
break;
|
|
4452
|
+
}
|
|
4453
|
+
candidatePort = nextPort;
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
if (lastBindError) {
|
|
4458
|
+
throw lastBindError;
|
|
4459
|
+
}
|
|
4398
4460
|
|
|
4399
4461
|
return getStatus({ includeSecrets: false });
|
|
4400
4462
|
}
|