livedesk 0.1.563 → 0.1.565

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.js CHANGED
@@ -1616,9 +1616,9 @@ function isPortConflictOutput(output) {
1616
1616
  return /\bEADDRINUSE\b|address already in use|requires client endpoint TCP \d+/i.test(String(output || ''));
1617
1617
  }
1618
1618
 
1619
- function formatPortOwner(record) {
1620
- return `${record?.Pid || 'unknown'}:${record?.ProcessName || 'unknown'} cmd=${String(record?.CommandLine || 'unavailable').replace(/\s+/g, ' ').slice(0, 240)}`;
1621
- }
1619
+ function formatPortOwner(record) {
1620
+ return `${record?.Pid || 'unknown'}:${record?.ProcessName || 'unknown'} state=${record?.State || 'unknown'} cmd=${String(record?.CommandLine || 'unavailable').replace(/\s+/g, ' ').slice(0, 240)}`;
1621
+ }
1622
1622
 
1623
1623
  function assertPortCleanupReady(finalRecords, portStates) {
1624
1624
  const blockedPorts = portStates.filter(port => port?.Ready !== true);
@@ -1640,35 +1640,37 @@ async function collectWindowsPortRecords(ports) {
1640
1640
  '$ErrorActionPreference = "SilentlyContinue";',
1641
1641
  `$ports = @(${ports.join(',')});`,
1642
1642
  `$currentNodePid = ${process.pid};`,
1643
- 'function Get-ListenConnections($port) {',
1644
- ' $connections = @(Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue);',
1645
- ' if ($connections.Count -gt 0) { return $connections }',
1646
- ' $pattern = "^\\s*TCP\\s+\\S+:" + $port + "\\s+\\S+\\s+LISTENING\\s+(\\d+)\\s*$";',
1647
- ' foreach ($line in @(netstat -ano -p tcp)) {',
1648
- ' $match = [regex]::Match([string]$line, $pattern);',
1649
- ' if ($match.Success) {',
1650
- ' [pscustomobject]@{ LocalPort = [int]$port; OwningProcess = [int]$match.Groups[1].Value }',
1651
- ' }',
1652
- ' }',
1643
+ 'function Get-PortConnections($port) {',
1644
+ ' $connections = @(Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue);',
1645
+ ' if ($connections.Count -gt 0) { return $connections }',
1646
+ ' $pattern = "^\\s*TCP\\s+\\S+:" + $port + "\\s+\\S+\\s+(\\S+)\\s+(\\d+)\\s*$";',
1647
+ ' foreach ($line in @(netstat -ano -p tcp)) {',
1648
+ ' $match = [regex]::Match([string]$line, $pattern);',
1649
+ ' if ($match.Success) {',
1650
+ ' [pscustomobject]@{ LocalPort = [int]$port; State = [string]$match.Groups[1].Value; OwningProcess = [int]$match.Groups[2].Value }',
1651
+ ' }',
1652
+ ' }',
1653
1653
  '}',
1654
- '$records = @();',
1655
- 'foreach ($port in $ports) {',
1656
- ' $connections = @(Get-ListenConnections $port | Where-Object { $_.OwningProcess -and $_.OwningProcess -ne $currentNodePid -and $_.OwningProcess -ne $PID });',
1657
- ' foreach ($connection in $connections) {',
1658
- ' $owner = [int]$connection.OwningProcess;',
1654
+ '$records = @();',
1655
+ 'foreach ($port in $ports) {',
1656
+ ' $connections = @(Get-PortConnections $port | Where-Object { $_.OwningProcess -and $_.OwningProcess -ne $currentNodePid -and $_.OwningProcess -ne $PID });',
1657
+ ' foreach ($connection in $connections) {',
1658
+ ' $owner = [int]$connection.OwningProcess;',
1659
1659
  ' $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $owner" -Property ProcessId,Name,CommandLine,CreationDate -ErrorAction SilentlyContinue;',
1660
1660
  ' $processName = if ($proc) { [string]$proc.Name } else { "" };',
1661
1661
  ' $commandLine = if ($proc) { [string]$proc.CommandLine } else { "" };',
1662
1662
  ' $creationUtcTicks = if ($proc -and $proc.CreationDate) { [string]$proc.CreationDate.ToUniversalTime().Ticks } else { "" };',
1663
1663
  ' $isNodeProcess = $processName -match "(?i)^node(?:\\.exe)?$";',
1664
1664
  ' $hasKnownLiveDeskHubPath = $commandLine -match "(?i)(?:packages[\\\\/](?:livedesk[\\\\/])?hub|node_modules[\\\\/]@?livedesk[\\\\/]hub)[\\\\/]src[\\\\/]server\\.js";',
1665
- ' $hasUnifiedLauncherPath = $commandLine -match "(?i)(?:node_modules[\\\\/]livedesk|packages[\\\\/]livedesk)[\\\\/]bin[\\\\/]livedesk\\.js";',
1666
- ' $hasLegacyClientPath = $commandLine -match "(?i)(?:node_modules[\\\\/]@livedesk[\\\\/]client|packages[\\\\/]client|packages[\\\\/]livedesk[\\\\/]client)[\\\\/]bin[\\\\/]livedesk-client\\.js";',
1667
- ' $knownLiveDesk = [bool]($isNodeProcess -and ($hasKnownLiveDeskHubPath -or $hasUnifiedLauncherPath -or $hasLegacyClientPath));',
1668
- ' $records += [pscustomobject]@{ Port = [int]$port; Pid = $owner; ProcessName = $processName; CommandLine = $commandLine; CreationUtcTicks = $creationUtcTicks; KnownLiveDesk = $knownLiveDesk; Action = "preserved"; KillAttempted = $false };',
1669
- ' }',
1670
- '}',
1671
- '$records = @($records | Sort-Object Port,Pid -Unique);',
1665
+ ' $hasUnifiedLauncherPath = $commandLine -match "(?i)(?:node_modules[\\\\/]livedesk|packages[\\\\/]livedesk)[\\\\/]bin[\\\\/]livedesk\\.js";',
1666
+ ' $hasLegacyClientPath = $commandLine -match "(?i)(?:node_modules[\\\\/]@livedesk[\\\\/]client|packages[\\\\/]client|packages[\\\\/]livedesk[\\\\/]client)[\\\\/]bin[\\\\/]livedesk-client\\.js";',
1667
+ ' $knownLiveDesk = [bool]($isNodeProcess -and ($hasKnownLiveDeskHubPath -or $hasUnifiedLauncherPath -or $hasLegacyClientPath));',
1668
+ ' $state = [string]$connection.State;',
1669
+ ' $isListener = $state -match "^(?i:Listen|Listening)$";',
1670
+ ' $records += [pscustomobject]@{ Port = [int]$port; Pid = $owner; ProcessName = $processName; CommandLine = $commandLine; CreationUtcTicks = $creationUtcTicks; State = $state; IsListener = [bool]$isListener; KnownLiveDesk = $knownLiveDesk; Action = "preserved"; KillAttempted = $false };',
1671
+ ' }',
1672
+ '}',
1673
+ '$records = @($records | Sort-Object Port,Pid,State -Unique);',
1672
1674
  '[pscustomobject]@{ Records = $records } | ConvertTo-Json -Compress -Depth 5'
1673
1675
  ].join(' ');
1674
1676
  const result = await runQuiet(
@@ -1701,8 +1703,9 @@ async function terminateExactWindowsPortOwners(records) {
1701
1703
  for (const record of Array.isArray(records) ? records : []) {
1702
1704
  const pid = Number(record?.Pid || 0);
1703
1705
  const startOrder = String(record?.CreationUtcTicks || '').trim();
1704
- if (record?.KnownLiveDesk !== true
1705
- || !Number.isInteger(pid)
1706
+ if (record?.KnownLiveDesk !== true
1707
+ || record?.IsListener !== true
1708
+ || !Number.isInteger(pid)
1706
1709
  || pid <= 1
1707
1710
  || !/^\d+$/.test(startOrder)
1708
1711
  || seenRoots.has(`${pid}:${startOrder}`)) {
@@ -1839,7 +1842,7 @@ async function terminateExactWindowsPortOwners(records) {
1839
1842
 
1840
1843
  async function stopWindowsProcessesOnPorts(ports) {
1841
1844
  const records = await collectWindowsPortRecords(ports);
1842
- const killable = records.filter(record => record?.KnownLiveDesk === true);
1845
+ const killable = records.filter(record => record?.KnownLiveDesk === true && record?.IsListener === true);
1843
1846
  if (killable.length > 0) {
1844
1847
  await terminateExactWindowsPortOwners(killable);
1845
1848
  for (const record of killable) {
@@ -3186,7 +3189,15 @@ async function runClient(args, resolvedRole = null, runtimeLock = null) {
3186
3189
  await new Promise(() => undefined);
3187
3190
  });
3188
3191
  try {
3189
- await clientModule.runClientRuntime(clientArgs);
3192
+ await clientModule.runClientRuntime(clientArgs, {
3193
+ recoverHubClientPort: async port => {
3194
+ const normalizedPort = normalizePort(port, 0);
3195
+ if (!normalizedPort) {
3196
+ throw new Error(`LiveDesk cannot recover an invalid Hub client port: ${port}.`);
3197
+ }
3198
+ await stopProcessesOnPorts([normalizedPort]);
3199
+ }
3200
+ });
3190
3201
  } finally {
3191
3202
  runtimeShutdownWatcher?.stop();
3192
3203
  }
@@ -588,7 +588,7 @@ export function resolveHubClientPort(env = process.env) {
588
588
  return normalizePort(env.REMOTE_HUB_PORT || env.LIVEDESK_HUB_REMOTE_PORT) || DEFAULT_HUB_CLIENT_PORT;
589
589
  }
590
590
 
591
- export function preflightHubClientPort(port = resolveHubClientPort()) {
591
+ export function preflightHubClientPort(port = resolveHubClientPort()) {
592
592
  const normalizedPort = normalizePort(port);
593
593
  if (!normalizedPort) {
594
594
  return Promise.resolve({ ok: false, port: Number(port) || 0, code: 'invalid-hub-client-port' });
@@ -618,18 +618,46 @@ export function preflightHubClientPort(port = resolveHubClientPort()) {
618
618
  finish({ ok: true, code: 'ok' });
619
619
  });
620
620
  });
621
- });
622
- }
623
-
624
- function hubClientPortPreflightError(preflight) {
625
- const port = Number(preflight?.port || resolveHubClientPort());
626
- return {
627
- ok: false,
628
- code: String(preflight?.code || 'hub-client-port-unavailable'),
629
- port,
630
- error: preflight?.code === 'hub-client-port-in-use'
631
- ? `LiveDesk cannot switch this computer to Hub because TCP ${port} is already in use. Change or stop the owning service, then try Switch to Hub again. The current Hub was not changed.`
632
- : `LiveDesk cannot switch this computer to Hub because TCP ${port} is unavailable. Check the local port configuration, then try again. The current Hub was not changed.`
621
+ });
622
+ }
623
+
624
+ export async function recoverHubClientPortConflict(preflight, recoverPortOwner) {
625
+ if (preflight?.ok
626
+ || preflight?.code !== 'hub-client-port-in-use'
627
+ || typeof recoverPortOwner !== 'function') {
628
+ return preflight;
629
+ }
630
+
631
+ try {
632
+ await recoverPortOwner(Number(preflight.port || resolveHubClientPort()));
633
+ } catch (error) {
634
+ return {
635
+ ...preflight,
636
+ recoveryAttempted: true,
637
+ recoveryError: error instanceof Error ? error.message : String(error)
638
+ };
639
+ }
640
+
641
+ const verified = await preflightHubClientPort(preflight.port);
642
+ return {
643
+ ...verified,
644
+ recoveryAttempted: true,
645
+ recovered: verified.ok === true
646
+ };
647
+ }
648
+
649
+ function hubClientPortPreflightError(preflight) {
650
+ const port = Number(preflight?.port || resolveHubClientPort());
651
+ const recoveryError = String(preflight?.recoveryError || '').trim();
652
+ return {
653
+ ok: false,
654
+ code: String(preflight?.code || 'hub-client-port-unavailable'),
655
+ port,
656
+ error: recoveryError
657
+ ? `${recoveryError} The current Hub was not changed.`
658
+ : preflight?.code === 'hub-client-port-in-use'
659
+ ? `LiveDesk cannot switch this computer to Hub because TCP ${port} is already in use. Change or stop the owning service, then try Switch to Hub again. The current Hub was not changed.`
660
+ : `LiveDesk cannot switch this computer to Hub because TCP ${port} is unavailable. Check the local port configuration, then try again. The current Hub was not changed.`
633
661
  };
634
662
  }
635
663
 
@@ -3082,12 +3110,16 @@ async function startConnectionChoiceServer(supabase, options = {}) {
3082
3110
  res.end(JSON.stringify({ ok: false, error: 'client-can-only-transition-to-hub' }));
3083
3111
  return;
3084
3112
  }
3085
- const portPreflight = await preflightHubClientPort();
3086
- if (!portPreflight.ok) {
3087
- res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
3088
- res.end(JSON.stringify(hubClientPortPreflightError(portPreflight)));
3089
- return;
3090
- }
3113
+ const portPreflight = await preflightHubClientPort();
3114
+ const readyPortPreflight = await recoverHubClientPortConflict(
3115
+ portPreflight,
3116
+ options.recoverHubClientPort
3117
+ );
3118
+ if (!readyPortPreflight.ok) {
3119
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
3120
+ res.end(JSON.stringify(hubClientPortPreflightError(readyPortPreflight)));
3121
+ return;
3122
+ }
3091
3123
  const session = await refreshSessionIfNeeded(supabase);
3092
3124
  const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
3093
3125
  const { data, error } = await supabase.rpc('set_livedesk_device_role', {
@@ -3403,10 +3435,14 @@ async function chooseClientConnection(supabase, options = {}) {
3403
3435
  if (!activeSupabase) {
3404
3436
  return { ok: false, error: 'supabase-session-required' };
3405
3437
  }
3406
- const portPreflight = await preflightHubClientPort();
3407
- if (!portPreflight.ok) {
3408
- return hubClientPortPreflightError(portPreflight);
3409
- }
3438
+ const portPreflight = await preflightHubClientPort();
3439
+ const readyPortPreflight = await recoverHubClientPortConflict(
3440
+ portPreflight,
3441
+ options.recoverHubClientPort
3442
+ );
3443
+ if (!readyPortPreflight.ok) {
3444
+ return hubClientPortPreflightError(readyPortPreflight);
3445
+ }
3410
3446
  const session = await refreshSessionIfNeeded(activeSupabase);
3411
3447
  if (!session?.access_token) {
3412
3448
  return { ok: false, error: 'supabase-session-required' };
@@ -5085,9 +5121,10 @@ export async function runClientRuntime(argv = process.argv.slice(2), runtimeOpti
5085
5121
  savedSession: null,
5086
5122
  loadSavedSession: false,
5087
5123
  savedPin: null,
5088
- allowRelayFallback: transportAllowsRelay(parsed.transport),
5089
- relayEndpoint: parsed.relay,
5090
- openBrowser: parsed.openBrowserOnStart,
5124
+ allowRelayFallback: transportAllowsRelay(parsed.transport),
5125
+ relayEndpoint: parsed.relay,
5126
+ recoverHubClientPort: runtimeOptions.recoverHubClientPort,
5127
+ openBrowser: parsed.openBrowserOnStart,
5091
5128
  onStarted: page => {
5092
5129
  connectionPage = page;
5093
5130
  resolveStarted(page);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.248",
3
+ "version": "0.1.249",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.563",
4
- "livedeskClientVersion": "0.1.248",
3
+ "version": "0.1.565",
4
+ "livedeskClientVersion": "0.1.249",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",