@livedesk/client 0.1.196 → 0.1.198

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.
@@ -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);
package/package.json CHANGED
@@ -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);