livedesk 0.1.441 → 0.1.443

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
@@ -13,7 +13,10 @@ import { acquireRuntimeLock } from '../bootstrap/runtime-lock.js';
13
13
  import { migrateLegacyClientState } from '../bootstrap/state-migration.js';
14
14
  import { createRoleBootstrapServer } from '../bootstrap/role-bootstrap-server.js';
15
15
  import { parseLsofPids } from '../bootstrap/port-inspection.js';
16
- import { isKnownLiveDeskClientAgentProcess } from '../bootstrap/client-process-identity.js';
16
+ import {
17
+ isKnownLiveDeskCaptureHelperProcess,
18
+ isKnownLiveDeskClientAgentProcess
19
+ } from '../bootstrap/client-process-identity.js';
17
20
  import { runLegacyClientUpdateSupervisorCli } from '../bootstrap/legacy-client-update.js';
18
21
  import { transferSavedClientSessionToHub } from '../bootstrap/hub-auth-handoff.js';
19
22
 
@@ -569,11 +572,11 @@ function runQuiet(command, args) {
569
572
  }
570
573
 
571
574
  function isKnownLiveDeskLauncherProcess(processName, commandLine) {
572
- const normalizedName = String(processName || '').trim();
575
+ const normalizedName = String(processName || '').trim().replaceAll('\\', '/').split('/').pop() || '';
573
576
  const normalizedCommandLine = String(commandLine || '').replaceAll('\\', '/');
574
577
  return /^(?:node|node\.exe)$/i.test(normalizedName)
575
- && /livedesk/i.test(normalizedCommandLine)
576
- && /(?:^|\/)bin\/livedesk\.js(?:\s|["']|$)/i.test(normalizedCommandLine);
578
+ && /(?:^|\/)(?:node_modules\/livedesk\/bin\/livedesk\.js|packages\/livedesk\/bin\/livedesk\.js|node_modules\/\.bin\/livedesk)(?:\s|["']|$)/i
579
+ .test(normalizedCommandLine);
577
580
  }
578
581
 
579
582
  function existingRuntimeProbePorts() {
@@ -595,7 +598,7 @@ async function probeExistingRuntimeIdentity(existing) {
595
598
 
596
599
  for (const port of existingRuntimeProbePorts()) {
597
600
  const controller = new AbortController();
598
- const timer = setTimeout(() => controller.abort(), 1200);
601
+ const timer = setTimeout(() => controller.abort(), 2500);
599
602
  try {
600
603
  const baseUrl = `http://127.0.0.1:${port}`;
601
604
  const [healthResponse, statusResponse] = await Promise.all([
@@ -707,7 +710,19 @@ async function stopExistingRuntime(existing) {
707
710
  if (error?.code !== 'ESRCH') throw error;
708
711
  }
709
712
  }
710
- await waitForProcessExit(processId);
713
+ try {
714
+ await waitForProcessExit(processId);
715
+ } catch (error) {
716
+ if (os.platform() === 'win32') throw error;
717
+ console.warn(
718
+ `[LiveDesk] Confirmed launcher pid ${processId} did not finish graceful Client cleanup; `
719
+ + 'force-stopping that launcher only. Verified orphan agents/helpers will be audited before restart.'
720
+ );
721
+ try { process.kill(processId, 'SIGKILL'); } catch (killError) {
722
+ if (killError?.code !== 'ESRCH') throw killError;
723
+ }
724
+ await waitForProcessExit(processId, 2000);
725
+ }
711
726
  return runtimeProof;
712
727
  }
713
728
 
@@ -881,7 +896,7 @@ async function stopUnixProcessesOnPorts(ports) {
881
896
  async function stopStaleUnixClientAgents() {
882
897
  if (os.platform() === 'win32') return;
883
898
  const candidatePids = new Set();
884
- for (const pattern of ['livedesk-client-fast', 'livedesk-client-node.js']) {
899
+ for (const pattern of ['livedesk-client-fast', 'livedesk-client-node.js', 'livedesk-sck-h264']) {
885
900
  const result = await runQuiet('pgrep', ['-f', pattern]);
886
901
  if (result.error?.code === 'ENOENT') return;
887
902
  for (const pid of parseLsofPids(result.stdout)) {
@@ -889,22 +904,29 @@ async function stopStaleUnixClientAgents() {
889
904
  }
890
905
  }
891
906
 
892
- const stalePids = [];
907
+ const staleProcesses = [];
893
908
  for (const pid of candidatePids) {
894
909
  const details = await getProcessDetails(pid);
895
910
  if (details && isKnownLiveDeskClientAgentProcess(details.processName, details.commandLine)) {
896
- stalePids.push(pid);
911
+ staleProcesses.push({ pid, type: 'agent' });
912
+ } else if (details && isKnownLiveDeskCaptureHelperProcess(details.processName, details.commandLine)) {
913
+ staleProcesses.push({ pid, type: 'capture-helper' });
897
914
  }
898
915
  }
899
- if (stalePids.length === 0) return;
916
+ if (staleProcesses.length === 0) {
917
+ if (process.env.LIVEDESK_DEBUG === '1') {
918
+ console.log('[LiveDesk] Client lifecycle audit: verified stale agents=0 capture-helpers=0.');
919
+ }
920
+ return;
921
+ }
900
922
 
901
- for (const pid of stalePids) {
923
+ for (const { pid } of staleProcesses) {
902
924
  try { process.kill(pid, 'SIGTERM'); } catch (error) {
903
925
  if (error?.code !== 'ESRCH') throw error;
904
926
  }
905
927
  }
906
928
  await waitMilliseconds(500);
907
- for (const pid of stalePids) {
929
+ for (const { pid } of staleProcesses) {
908
930
  try {
909
931
  process.kill(pid, 0);
910
932
  process.kill(pid, 'SIGKILL');
@@ -912,7 +934,12 @@ async function stopStaleUnixClientAgents() {
912
934
  if (error?.code !== 'ESRCH') throw error;
913
935
  }
914
936
  }
915
- console.log(`[LiveDesk] Stopped stale Client agent process(es): ${stalePids.join(', ')}.`);
937
+ const agentPids = staleProcesses.filter(item => item.type === 'agent').map(item => item.pid);
938
+ const helperPids = staleProcesses.filter(item => item.type === 'capture-helper').map(item => item.pid);
939
+ console.log(
940
+ `[LiveDesk] Client lifecycle audit: verified stale agents=${agentPids.length} `
941
+ + `capture-helpers=${helperPids.length}; stopped pids=${staleProcesses.map(item => item.pid).join(', ')}.`
942
+ );
916
943
  }
917
944
 
918
945
  async function stopProcessesOnPorts(ports) {
@@ -2,8 +2,29 @@ export function isKnownLiveDeskClientAgentProcess(processName, commandLine) {
2
2
  const normalizedName = String(processName || '').trim().replaceAll('\\', '/').split('/').pop() || '';
3
3
  const normalizedCommandLine = String(commandLine || '').replaceAll('\\', '/');
4
4
  const isNode = /^(?:node|node\.exe)$/i.test(normalizedName);
5
+ const isDotnet = /^(?:dotnet|dotnet\.exe)$/i.test(normalizedName);
5
6
  const isFast = /^livedesk-client-fast(?:\.exe)?$/i.test(normalizedName);
6
7
  const isKnownNodeAgent = /(?:node_modules\/(?:livedesk\/client|@livedesk\/client)|packages\/(?:livedesk\/client|client))\/bin\/livedesk-client-node\.js(?:\s|["']|$)/i.test(normalizedCommandLine);
7
- const isKnownFastAgent = /(?:node_modules\/(?:livedesk\/client|@livedesk\/client)|packages\/(?:livedesk\/client|client))\/fast\/[^/\s]+\/livedesk-client-fast(?:\.exe)?(?:\s|["']|$)/i.test(normalizedCommandLine);
8
- return (isNode && isKnownNodeAgent) || (isFast && isKnownFastAgent);
8
+ const bundledFastRoot = '(?:node_modules/(?:livedesk/client|@livedesk/client)|packages/(?:livedesk/client|client))/fast/[^/\\s]+';
9
+ const optionalFastRoot = 'node_modules/@livedesk/fast-(?:linux-x64|osx-arm64|osx-x64|win-x64)/fast';
10
+ const fastRoot = `(?:${bundledFastRoot}|${optionalFastRoot})`;
11
+ const isKnownFastExecutable = new RegExp(
12
+ `${fastRoot}/livedesk-client-fast(?:\\.exe)?(?:\\s|["']|$)`,
13
+ 'i'
14
+ ).test(normalizedCommandLine);
15
+ const isKnownFastDll = new RegExp(
16
+ `${fastRoot}/livedesk-client-fast\\.dll(?:\\s|["']|$)`,
17
+ 'i'
18
+ ).test(normalizedCommandLine);
19
+ return (isNode && isKnownNodeAgent)
20
+ || (isFast && isKnownFastExecutable)
21
+ || (isDotnet && isKnownFastDll);
22
+ }
23
+
24
+ export function isKnownLiveDeskCaptureHelperProcess(processName, commandLine) {
25
+ const normalizedName = String(processName || '').trim().replaceAll('\\', '/').split('/').pop() || '';
26
+ const normalizedCommandLine = String(commandLine || '').replaceAll('\\', '/');
27
+ return /^livedesk-sck-h264$/i.test(normalizedName)
28
+ && /(?:^|\/)\.livedesk\/helpers\/macos-sck-h264\/livedesk-sck-h264(?:\s|["']|$)/i
29
+ .test(normalizedCommandLine);
9
30
  }
@@ -4102,11 +4102,17 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4102
4102
  agentPid: 0,
4103
4103
  restartVerified: false
4104
4104
  });
4105
+ const ownsProcessGroup = process.platform !== 'win32';
4105
4106
  const child = spawn(command, args, {
4106
4107
  env,
4107
4108
  stdio: 'inherit',
4109
+ // A dedicated Unix process group lets Ctrl+C, replacement startup, and
4110
+ // forced shutdown terminate RemoteFast together with its SCK/FFmpeg
4111
+ // descendants without touching npm, the terminal, or unrelated jobs.
4112
+ detached: ownsProcessGroup,
4108
4113
  windowsHide: true
4109
4114
  });
4115
+ child.livedeskOwnsProcessGroup = ownsProcessGroup;
4110
4116
  activeAgentProcess = child;
4111
4117
  if (typeof onStart === 'function') {
4112
4118
  onStart(child);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.196",
3
+ "version": "0.1.198",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,10 @@
41
41
  "ws": "^8.18.3"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@livedesk/fast-linux-x64": "0.1.403",
45
- "@livedesk/fast-osx-arm64": "0.1.403",
46
- "@livedesk/fast-osx-x64": "0.1.403",
47
- "@livedesk/fast-win-x64": "0.1.403"
44
+ "@livedesk/fast-linux-x64": "0.1.405",
45
+ "@livedesk/fast-osx-arm64": "0.1.405",
46
+ "@livedesk/fast-osx-x64": "0.1.405",
47
+ "@livedesk/fast-win-x64": "0.1.405"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"
@@ -1,9 +1,76 @@
1
+ import { execFile } from 'node:child_process';
2
+
1
3
  const SIGNAL_EXIT_CODES = Object.freeze({ SIGINT: 130, SIGTERM: 143 });
2
4
 
5
+ function terminateWindowsProcessTree(processId) {
6
+ return new Promise(resolve => {
7
+ execFile(
8
+ 'taskkill.exe',
9
+ ['/PID', String(processId), '/T', '/F'],
10
+ {
11
+ windowsHide: true,
12
+ timeout: 10000,
13
+ killSignal: 'SIGKILL'
14
+ },
15
+ error => resolve({ ok: !error, error: error || null })
16
+ );
17
+ });
18
+ }
19
+
20
+ function isOwnedUnixProcessGroupAlive(child, platform, signalProcess) {
21
+ const processId = Number(child?.pid || 0);
22
+ if (platform === 'win32'
23
+ || child?.livedeskOwnsProcessGroup !== true
24
+ || !Number.isInteger(processId)
25
+ || processId <= 1) {
26
+ return false;
27
+ }
28
+ try {
29
+ signalProcess(-processId, 0);
30
+ return true;
31
+ } catch (error) {
32
+ return error?.code === 'EPERM';
33
+ }
34
+ }
35
+
36
+ function isProcessIdAlive(processId, signalProcess) {
37
+ if (!Number.isInteger(processId) || processId <= 1) {
38
+ return false;
39
+ }
40
+ try {
41
+ signalProcess(processId, 0);
42
+ return true;
43
+ } catch (error) {
44
+ return error?.code === 'EPERM';
45
+ }
46
+ }
47
+
48
+ function signalAgentTree(child, signal, platform, signalProcess) {
49
+ const processId = Number(child?.pid || 0);
50
+ if (platform !== 'win32'
51
+ && child?.livedeskOwnsProcessGroup === true
52
+ && Number.isInteger(processId)
53
+ && processId > 1) {
54
+ try {
55
+ signalProcess(-processId, signal);
56
+ return;
57
+ } catch (error) {
58
+ if (error?.code !== 'ESRCH') throw error;
59
+ }
60
+ }
61
+ child.kill(signal);
62
+ }
63
+
3
64
  export function installAgentTerminationHandlers({
4
65
  hostProcess = process,
5
66
  getAgentProcess,
6
- shutdownTimeoutMs = 3000,
67
+ shutdownTimeoutMs = 8000,
68
+ platform = process.platform,
69
+ signalProcess = (pid, signal) => process.kill(pid, signal),
70
+ terminateWindowsTree = terminateWindowsProcessTree,
71
+ windowsTerminateAttemptLimit = 3,
72
+ windowsTerminateRetryMs = 100,
73
+ reportTerminationFailure = message => console.error(message),
7
74
  exitProcess = code => hostProcess.exit(code)
8
75
  } = {}) {
9
76
  if (typeof getAgentProcess !== 'function') {
@@ -18,32 +85,110 @@ export function installAgentTerminationHandlers({
18
85
  terminating = true;
19
86
  const exitCode = SIGNAL_EXIT_CODES[signal] || 1;
20
87
  const child = getAgentProcess();
21
- if (!child || child.exitCode !== null || child.killed === true) {
88
+ if (!child || child.exitCode !== null) {
22
89
  exitProcess(exitCode);
23
90
  return;
24
91
  }
25
92
 
26
93
  let finished = false;
94
+ let poll = null;
95
+ let timeout = null;
96
+ let hardStop = null;
97
+ let windowsRetryTimer = null;
27
98
  const finish = () => {
28
99
  if (finished) return;
29
100
  finished = true;
101
+ if (poll) clearInterval(poll);
102
+ if (timeout) clearTimeout(timeout);
103
+ if (hardStop) clearTimeout(hardStop);
104
+ if (windowsRetryTimer) clearTimeout(windowsRetryTimer);
30
105
  exitProcess(exitCode);
31
106
  };
32
- child.once('exit', finish);
107
+ const processId = Number(child?.pid || 0);
108
+ if (platform === 'win32') {
109
+ if (!Number.isInteger(processId) || processId <= 1) {
110
+ finish();
111
+ return;
112
+ }
113
+ // On Windows child.kill() only gives us the Agent leader's
114
+ // lifecycle. It does not provide an authoritative contract
115
+ // that FFmpeg descendants have stopped. Keep the launcher
116
+ // alive until taskkill has completed against this exact,
117
+ // launcher-owned process tree. Never search for or terminate
118
+ // unrelated ffmpeg.exe processes globally.
119
+ const attemptLimit = Math.max(1, Number(windowsTerminateAttemptLimit) || 3);
120
+ const retryMs = Math.max(25, Number(windowsTerminateRetryMs) || 100);
121
+ let attempt = 0;
122
+ let failureReported = false;
123
+ const terminateOwnedTree = () => {
124
+ if (finished) return;
125
+ attempt += 1;
126
+ let treeTermination;
127
+ try {
128
+ treeTermination = terminateWindowsTree(processId);
129
+ } catch (error) {
130
+ treeTermination = { ok: false, error };
131
+ }
132
+ Promise.resolve(treeTermination)
133
+ .catch(error => ({ ok: false, error }))
134
+ .then(result => {
135
+ if (finished) return;
136
+ if (result?.ok !== false) {
137
+ finish();
138
+ return;
139
+ }
140
+ if (!isProcessIdAlive(processId, signalProcess)) {
141
+ finish();
142
+ return;
143
+ }
144
+
145
+ const burstExhausted = attempt >= attemptLimit;
146
+ if (burstExhausted && !failureReported) {
147
+ failureReported = true;
148
+ const reason = result?.error?.message || 'taskkill returned an error';
149
+ reportTerminationFailure(
150
+ `[LiveDesk Client] Agent tree pid=${processId} is still alive after `
151
+ + `${attemptLimit} exact-PID taskkill attempts (${reason}). `
152
+ + 'The launcher will remain alive and keep retrying so capture descendants are not orphaned.'
153
+ );
154
+ }
155
+ if (burstExhausted) {
156
+ attempt = 0;
157
+ }
158
+ windowsRetryTimer = setTimeout(
159
+ terminateOwnedTree,
160
+ burstExhausted ? Math.max(1000, retryMs) : retryMs
161
+ );
162
+ });
163
+ };
164
+ terminateOwnedTree();
165
+ return;
166
+ }
167
+ const finishWhenTreeStops = () => {
168
+ const ownsUnixGroup = child?.livedeskOwnsProcessGroup === true
169
+ && Number(child?.pid || 0) > 1;
170
+ const childAlive = child.exitCode === null && child.signalCode == null;
171
+ const groupAlive = ownsUnixGroup
172
+ && isOwnedUnixProcessGroupAlive(child, platform, signalProcess);
173
+ if (!childAlive && !groupAlive) {
174
+ finish();
175
+ }
176
+ };
177
+ child.once('exit', finishWhenTreeStops);
33
178
  try {
34
- child.kill(signal);
179
+ signalAgentTree(child, signal, platform, signalProcess);
35
180
  } catch {
36
181
  finish();
37
182
  return;
38
183
  }
39
- const timeout = setTimeout(() => {
184
+ poll = setInterval(finishWhenTreeStops, 50);
185
+ timeout = setTimeout(() => {
40
186
  try {
41
- if (child.exitCode === null) child.kill('SIGKILL');
187
+ signalAgentTree(child, 'SIGKILL', platform, signalProcess);
42
188
  } catch {
43
189
  }
44
- finish();
190
+ hardStop = setTimeout(finish, 250);
45
191
  }, Math.max(100, Number(shutdownTimeoutMs) || 3000));
46
- timeout.unref?.();
47
192
  };
48
193
  handlers.set(signal, handler);
49
194
  hostProcess.once(signal, handler);
package/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -3881,6 +3881,9 @@ export function createRemoteHub(options = {}) {
3881
3881
  nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
3882
3882
  nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
3883
3883
  nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
3884
+ nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
3885
+ nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
3886
+ nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
3884
3887
  nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
3885
3888
  droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
3886
3889
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
@@ -4258,6 +4261,9 @@ export function createRemoteHub(options = {}) {
4258
4261
  nativeEncodableFrameCount: Number.isFinite(Number(message.nativeEncodableFrameCount)) ? Number(message.nativeEncodableFrameCount) : 0,
4259
4262
  nativeSkippedFrameCount: Number.isFinite(Number(message.nativeSkippedFrameCount)) ? Number(message.nativeSkippedFrameCount) : 0,
4260
4263
  nativeEncodedFrameCount: Number.isFinite(Number(message.nativeEncodedFrameCount)) ? Number(message.nativeEncodedFrameCount) : 0,
4264
+ nativeEncodeFramesInFlight: Number.isFinite(Number(message.nativeEncodeFramesInFlight)) ? Number(message.nativeEncodeFramesInFlight) : 0,
4265
+ nativeBackpressureSkippedFrameCount: Number.isFinite(Number(message.nativeBackpressureSkippedFrameCount)) ? Number(message.nativeBackpressureSkippedFrameCount) : 0,
4266
+ nativeIdleSkippedFrameCount: Number.isFinite(Number(message.nativeIdleSkippedFrameCount)) ? Number(message.nativeIdleSkippedFrameCount) : 0,
4261
4267
  nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
4262
4268
  droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
4263
4269
  hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
package/hub/src/server.js CHANGED
@@ -2070,6 +2070,9 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2070
2070
  nativeEncodableFrameCount: Number(frame.nativeEncodableFrameCount || 0) || 0,
2071
2071
  nativeSkippedFrameCount: Number(frame.nativeSkippedFrameCount || 0) || 0,
2072
2072
  nativeEncodedFrameCount: Number(frame.nativeEncodedFrameCount || 0) || 0,
2073
+ nativeEncodeFramesInFlight: Number(frame.nativeEncodeFramesInFlight || 0) || 0,
2074
+ nativeBackpressureSkippedFrameCount: Number(frame.nativeBackpressureSkippedFrameCount || 0) || 0,
2075
+ nativeIdleSkippedFrameCount: Number(frame.nativeIdleSkippedFrameCount || 0) || 0,
2073
2076
  nativeCaptureTelemetryAt: frame.nativeCaptureTelemetryAt || '',
2074
2077
  droppedByAgent: Number(frame.droppedByAgent || 0) || 0,
2075
2078
  hubIngressDropped: Number(frame.hubIngressDropped || 0) || 0,