@livedesk/client 0.1.196 → 0.1.197

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.197",
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.404",
45
+ "@livedesk/fast-osx-arm64": "0.1.404",
46
+ "@livedesk/fast-osx-x64": "0.1.404",
47
+ "@livedesk/fast-win-x64": "0.1.404"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"
@@ -1,9 +1,61 @@
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 signalAgentTree(child, signal, platform, signalProcess) {
37
+ const processId = Number(child?.pid || 0);
38
+ if (platform !== 'win32'
39
+ && child?.livedeskOwnsProcessGroup === true
40
+ && Number.isInteger(processId)
41
+ && processId > 1) {
42
+ try {
43
+ signalProcess(-processId, signal);
44
+ return;
45
+ } catch (error) {
46
+ if (error?.code !== 'ESRCH') throw error;
47
+ }
48
+ }
49
+ child.kill(signal);
50
+ }
51
+
3
52
  export function installAgentTerminationHandlers({
4
53
  hostProcess = process,
5
54
  getAgentProcess,
6
- shutdownTimeoutMs = 3000,
55
+ shutdownTimeoutMs = 8000,
56
+ platform = process.platform,
57
+ signalProcess = (pid, signal) => process.kill(pid, signal),
58
+ terminateWindowsTree = terminateWindowsProcessTree,
7
59
  exitProcess = code => hostProcess.exit(code)
8
60
  } = {}) {
9
61
  if (typeof getAgentProcess !== 'function') {
@@ -18,32 +70,72 @@ export function installAgentTerminationHandlers({
18
70
  terminating = true;
19
71
  const exitCode = SIGNAL_EXIT_CODES[signal] || 1;
20
72
  const child = getAgentProcess();
21
- if (!child || child.exitCode !== null || child.killed === true) {
73
+ if (!child || child.exitCode !== null) {
22
74
  exitProcess(exitCode);
23
75
  return;
24
76
  }
25
77
 
26
78
  let finished = false;
79
+ let poll = null;
80
+ let timeout = null;
81
+ let hardStop = null;
27
82
  const finish = () => {
28
83
  if (finished) return;
29
84
  finished = true;
85
+ if (poll) clearInterval(poll);
86
+ if (timeout) clearTimeout(timeout);
87
+ if (hardStop) clearTimeout(hardStop);
30
88
  exitProcess(exitCode);
31
89
  };
32
- child.once('exit', finish);
90
+ const processId = Number(child?.pid || 0);
91
+ if (platform === 'win32') {
92
+ if (!Number.isInteger(processId) || processId <= 1) {
93
+ finish();
94
+ return;
95
+ }
96
+ // On Windows child.kill() only gives us the Agent leader's
97
+ // lifecycle. It does not provide an authoritative contract
98
+ // that FFmpeg descendants have stopped. Keep the launcher
99
+ // alive until taskkill has completed against this exact,
100
+ // launcher-owned process tree. Never search for or terminate
101
+ // unrelated ffmpeg.exe processes globally.
102
+ let treeTermination;
103
+ try {
104
+ treeTermination = terminateWindowsTree(processId);
105
+ } catch {
106
+ finish();
107
+ return;
108
+ }
109
+ Promise.resolve(treeTermination)
110
+ .catch(() => null)
111
+ .finally(finish);
112
+ return;
113
+ }
114
+ const finishWhenTreeStops = () => {
115
+ const ownsUnixGroup = child?.livedeskOwnsProcessGroup === true
116
+ && Number(child?.pid || 0) > 1;
117
+ const childAlive = child.exitCode === null && child.signalCode == null;
118
+ const groupAlive = ownsUnixGroup
119
+ && isOwnedUnixProcessGroupAlive(child, platform, signalProcess);
120
+ if (!childAlive && !groupAlive) {
121
+ finish();
122
+ }
123
+ };
124
+ child.once('exit', finishWhenTreeStops);
33
125
  try {
34
- child.kill(signal);
126
+ signalAgentTree(child, signal, platform, signalProcess);
35
127
  } catch {
36
128
  finish();
37
129
  return;
38
130
  }
39
- const timeout = setTimeout(() => {
131
+ poll = setInterval(finishWhenTreeStops, 50);
132
+ timeout = setTimeout(() => {
40
133
  try {
41
- if (child.exitCode === null) child.kill('SIGKILL');
134
+ signalAgentTree(child, 'SIGKILL', platform, signalProcess);
42
135
  } catch {
43
136
  }
44
- finish();
137
+ hardStop = setTimeout(finish, 250);
45
138
  }, Math.max(100, Number(shutdownTimeoutMs) || 3000));
46
- timeout.unref?.();
47
139
  };
48
140
  handlers.set(signal, handler);
49
141
  hostProcess.once(signal, handler);