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 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 checks the Hub ports (`5179` and `5197`). It stops
17
- only a stale process whose command line identifies it as a LiveDesk Hub, waits
18
- for both ports to be released, and then starts the new Hub. An unrelated
19
- process is preserved and reported instead of being terminated; use `--port`,
20
- `--remote-port`, or stop that process manually. Pass `--no-clean` to disable
21
- the startup cleanup explicitly.
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
- Client connection port. Default: 5197
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-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -and $_.OwningProcess -ne $currentNodePid });',
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 | Where-Object { Get-NetTCPConnection -LocalPort $_ -State Listen -ErrorAction SilentlyContinue });',
245
- ' if ($busyPorts.Count -eq 0 -or (Get-Date).ToUniversalTime() -ge $deadline) { break }',
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'}@${record.Port}`).join(', ');
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
- throw new Error(`LiveDesk Hub port(s) already in use by an unrelated process: ${summary}. Use --port/--remote-port or stop that process manually.`);
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, remotePort]);
358
+ await stopProcessesOnPorts([httpPort]);
359
+ await stopProcessesOnPorts([remotePort], { allowBusyPorts: true });
335
360
  }
336
361
 
337
362
  const internalHubEntry = resolve(packageRoot, 'hub', 'src', 'server.js');
@@ -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
- async function start() {
4375
- if (!enabled || started) {
4376
- return getStatus({ includeSecrets: false });
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
- await new Promise((resolve, reject) => {
4380
- server = net.createServer(handleSocket);
4381
- server.once('error', err => {
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 = null;
4401
+ if (server === candidateServer) {
4402
+ server = null;
4403
+ }
4384
4404
  reject(err);
4385
4405
  });
4386
- server.listen(requestedPort, host, () => {
4406
+ candidateServer.listen(port, host, () => {
4387
4407
  started = true;
4388
- boundPort = server.address()?.port || requestedPort;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.249",
3
+ "version": "0.1.251",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {