livedesk 0.1.445 → 0.1.447

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.
@@ -296,6 +296,7 @@ export function createUdpRendezvousServer({
296
296
  if (!hub || !client) return;
297
297
  send({
298
298
  type: 'rendezvous.peer',
299
+ protocol: UDP_P2P_PROTOCOL,
299
300
  roomId,
300
301
  role: 'client',
301
302
  host: client.address,
@@ -303,6 +304,7 @@ export function createUdpRendezvousServer({
303
304
  }, hub.address, hub.port);
304
305
  send({
305
306
  type: 'rendezvous.peer',
307
+ protocol: UDP_P2P_PROTOCOL,
306
308
  roomId,
307
309
  role: 'hub',
308
310
  host: hub.address,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.445",
4
- "livedeskClientVersion": "0.1.200",
3
+ "version": "0.1.447",
4
+ "livedeskClientVersion": "0.1.202",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -50,10 +50,10 @@
50
50
  "ws": "^8.18.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@livedesk/fast-linux-x64": "0.1.407",
54
- "@livedesk/fast-osx-arm64": "0.1.407",
55
- "@livedesk/fast-osx-x64": "0.1.407",
56
- "@livedesk/fast-win-x64": "0.1.407"
53
+ "@livedesk/fast-linux-x64": "0.1.410",
54
+ "@livedesk/fast-osx-arm64": "0.1.410",
55
+ "@livedesk/fast-osx-x64": "0.1.410",
56
+ "@livedesk/fast-win-x64": "0.1.410"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"
@@ -1,4 +1,4 @@
1
- import { execFile, spawn } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
2
  import {
3
3
  appendFileSync,
4
4
  closeSync,
@@ -13,6 +13,10 @@ import {
13
13
  import net from 'node:net';
14
14
  import { dirname, join, resolve } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import {
17
+ registerOwnedWindowsChild,
18
+ terminateExactRegisteredWindowsTreeUntilStopped
19
+ } from './windows-owned-process-tree.js';
16
20
 
17
21
  const ROLE_TRANSITION_SUPERVISOR_FLAG = '--run-livedesk-role-transition-supervisor';
18
22
  const SUPERVISOR_CLAIM_TIMEOUT_MS = 5_000;
@@ -148,20 +152,14 @@ async function verifyReplacement({ child, role, previousPid, runtimePort, remote
148
152
  throw new Error(`replacement-health-timeout:${role}`);
149
153
  }
150
154
 
151
- async function terminateOwnedProcessTree(pid) {
152
- const processId = positiveInteger(pid);
153
- if (processId <= 1 || !processIsAlive(processId)) return;
155
+ async function terminateOwnedProcessTree(child) {
156
+ const processId = positiveInteger(child?.pid);
157
+ if (processId <= 1) return;
154
158
  if (process.platform === 'win32') {
155
- await new Promise(resolveKill => {
156
- execFile(
157
- 'taskkill.exe',
158
- ['/PID', String(processId), '/T', '/F'],
159
- { windowsHide: true, timeout: 10_000 },
160
- () => resolveKill()
161
- );
162
- });
159
+ await terminateExactRegisteredWindowsTreeUntilStopped(child);
163
160
  return;
164
161
  }
162
+ if (!processIsAlive(processId)) return;
165
163
  try { process.kill(-processId, 'SIGTERM'); } catch {
166
164
  try { process.kill(processId, 'SIGTERM'); } catch { /* already stopped */ }
167
165
  }
@@ -234,6 +232,7 @@ async function runRoleTransitionSupervisor(env = process.env) {
234
232
  let child = null;
235
233
  try {
236
234
  logDescriptor = openSync(settings.logPath, 'a');
235
+ const replacementSpawnedAtMs = Date.now();
237
236
  child = spawn(
238
237
  process.execPath,
239
238
  [settings.launcherEntry, '--force-role', settings.role, '--no-open'],
@@ -245,15 +244,17 @@ async function runRoleTransitionSupervisor(env = process.env) {
245
244
  windowsHide: true
246
245
  }
247
246
  );
247
+ child.livedeskWindowsSpawnedAtMs = replacementSpawnedAtMs;
248
248
  await new Promise((resolveSpawn, rejectSpawn) => {
249
249
  child.once('spawn', resolveSpawn);
250
250
  child.once('error', rejectSpawn);
251
251
  });
252
+ await registerOwnedWindowsChild(child);
252
253
  } catch (error) {
253
254
  lastError = error;
254
255
  appendDiagnostic(settings.logPath, `attempt=${attempt} launch-failed error=${error?.message || error}`);
255
- if (child?.pid && processIsAlive(child.pid)) {
256
- await terminateOwnedProcessTree(child.pid);
256
+ if (child?.pid) {
257
+ await terminateOwnedProcessTree(child);
257
258
  }
258
259
  await wait(350);
259
260
  continue;
@@ -287,9 +288,7 @@ async function runRoleTransitionSupervisor(env = process.env) {
287
288
  } catch (error) {
288
289
  lastError = error;
289
290
  appendDiagnostic(settings.logPath, `attempt=${attempt} failed error=${error?.message || error}`);
290
- if (processIsAlive(child.pid)) {
291
- await terminateOwnedProcessTree(child.pid);
292
- }
291
+ await terminateOwnedProcessTree(child);
293
292
  await wait(350);
294
293
  }
295
294
  }
@@ -312,6 +311,7 @@ export async function startRoleTransitionSupervisor(options = {}) {
312
311
  const logPath = join(stateDir, 'role-transition.log');
313
312
  const transitionId = `role-${role}-${Date.now()}-${process.pid}`;
314
313
  rmSync(statePath, { force: true });
314
+ const supervisorSpawnedAtMs = Date.now();
315
315
  const child = spawn(
316
316
  process.execPath,
317
317
  [fileURLToPath(import.meta.url), ROLE_TRANSITION_SUPERVISOR_FLAG],
@@ -334,10 +334,12 @@ export async function startRoleTransitionSupervisor(options = {}) {
334
334
  windowsHide: true
335
335
  }
336
336
  );
337
+ child.livedeskWindowsSpawnedAtMs = supervisorSpawnedAtMs;
337
338
  await new Promise((resolveSpawn, rejectSpawn) => {
338
339
  child.once('spawn', resolveSpawn);
339
340
  child.once('error', rejectSpawn);
340
341
  });
342
+ await registerOwnedWindowsChild(child);
341
343
  child.unref();
342
344
 
343
345
  const deadline = Date.now() + SUPERVISOR_CLAIM_TIMEOUT_MS;
@@ -353,7 +355,7 @@ export async function startRoleTransitionSupervisor(options = {}) {
353
355
  }
354
356
  await wait(50);
355
357
  }
356
- await terminateOwnedProcessTree(child.pid);
358
+ await terminateOwnedProcessTree(child);
357
359
  throw new Error(`role-transition-supervisor-claim-timeout:${child.pid || 0}`);
358
360
  }
359
361
 
@@ -0,0 +1,432 @@
1
+ import { execFile } from 'node:child_process';
2
+
3
+ const WINDOWS_EPOCH_TICKS = 621_355_968_000_000_000n;
4
+ const SPAWN_CLOCK_TOLERANCE_MS = 250;
5
+
6
+ function positivePid(value) {
7
+ const pid = Number(value || 0);
8
+ return Number.isInteger(pid) && pid > 1 ? pid : 0;
9
+ }
10
+
11
+ function normalizeRecord(value, depth = 0) {
12
+ const pid = positivePid(value?.pid ?? value?.ProcessId);
13
+ const parentPid = Number(value?.parentPid ?? value?.ParentProcessId ?? 0);
14
+ const startOrder = String(value?.startOrder ?? value?.CreationUtcTicks ?? '').trim();
15
+ if (!pid || !/^\d+$/.test(startOrder)) return null;
16
+ return {
17
+ pid,
18
+ parentPid: Number.isInteger(parentPid) && parentPid >= 0 ? parentPid : 0,
19
+ startOrder,
20
+ depth: Math.max(0, Number(depth) || 0)
21
+ };
22
+ }
23
+
24
+ function sameRecord(left, right) {
25
+ return positivePid(left?.pid) === positivePid(right?.pid)
26
+ && String(left?.startOrder || '') === String(right?.startOrder || '');
27
+ }
28
+
29
+ function windowsTicksForMilliseconds(milliseconds) {
30
+ return BigInt(Math.trunc(Number(milliseconds) || 0)) * 10_000n + WINDOWS_EPOCH_TICKS;
31
+ }
32
+
33
+ function collectRootAbsentWindowsTree(rootPid, snapshot, {
34
+ spawnedAtMs,
35
+ exitedAtMs
36
+ } = {}) {
37
+ const processId = positivePid(rootPid);
38
+ if (!processId || !Number.isFinite(Number(exitedAtMs)) || Number(exitedAtMs) <= 0) return [];
39
+ const records = (Array.isArray(snapshot) ? snapshot : [])
40
+ .map(record => normalizeRecord(record))
41
+ .filter(Boolean);
42
+ const lowerBound = windowsTicksForMilliseconds(
43
+ Math.max(0, Number(spawnedAtMs || 0) - SPAWN_CLOCK_TOLERANCE_MS)
44
+ );
45
+ const upperBound = windowsTicksForMilliseconds(exitedAtMs);
46
+ const childrenByParent = new Map();
47
+ for (const record of records) {
48
+ if (!childrenByParent.has(record.parentPid)) childrenByParent.set(record.parentPid, []);
49
+ childrenByParent.get(record.parentPid).push(record);
50
+ }
51
+ const result = [];
52
+ const queue = [{ pid: processId, startOrder: String(lowerBound), depth: 0, virtualRoot: true }];
53
+ const seen = new Set([processId]);
54
+ while (queue.length > 0) {
55
+ const parent = queue.shift();
56
+ for (const child of childrenByParent.get(parent.pid) || []) {
57
+ if (seen.has(child.pid)) continue;
58
+ try {
59
+ const childStart = BigInt(child.startOrder);
60
+ if (childStart < BigInt(parent.startOrder)) continue;
61
+ if (parent.virtualRoot && (childStart < lowerBound || childStart > upperBound)) continue;
62
+ } catch {
63
+ continue;
64
+ }
65
+ seen.add(child.pid);
66
+ const descendant = { ...child, depth: parent.depth + 1 };
67
+ result.push(descendant);
68
+ queue.push({ ...descendant, virtualRoot: false });
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+
74
+ function runPowerShell(script, {
75
+ execFileImpl = execFile,
76
+ timeoutMs = 10_000
77
+ } = {}) {
78
+ return new Promise(resolveRun => {
79
+ execFileImpl(
80
+ 'powershell.exe',
81
+ ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
82
+ {
83
+ windowsHide: true,
84
+ timeout: timeoutMs,
85
+ killSignal: 'SIGKILL',
86
+ maxBuffer: 4 * 1024 * 1024
87
+ },
88
+ (error, stdout, stderr) => {
89
+ resolveRun({
90
+ ok: !error,
91
+ error: error || null,
92
+ stdout: String(stdout || '').trim(),
93
+ stderr: String(stderr || '').trim()
94
+ });
95
+ }
96
+ );
97
+ });
98
+ }
99
+
100
+ export async function queryExactWindowsProcessSnapshot(options = {}) {
101
+ const script = [
102
+ '$ErrorActionPreference = "Stop";',
103
+ '$records = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object {',
104
+ ' $ticks = if ($_.CreationDate) { [string]$_.CreationDate.ToUniversalTime().Ticks } else { "" };',
105
+ ' [pscustomobject]@{ ProcessId = [int]$_.ProcessId; ParentProcessId = [int]$_.ParentProcessId; CreationUtcTicks = $ticks }',
106
+ '});',
107
+ '[pscustomobject]@{ Records = $records } | ConvertTo-Json -Compress -Depth 4'
108
+ ].join(' ');
109
+ const result = await runPowerShell(script, options);
110
+ if (!result.ok || !result.stdout) {
111
+ throw new Error(
112
+ `windows-process-snapshot-failed:${result.stderr || result.error?.message || 'empty-cim-result'}`
113
+ );
114
+ }
115
+ let parsed;
116
+ try {
117
+ parsed = JSON.parse(result.stdout);
118
+ } catch (error) {
119
+ throw new Error(`windows-process-snapshot-invalid-json:${error?.message || error}`);
120
+ }
121
+ const values = parsed?.Records == null
122
+ ? []
123
+ : Array.isArray(parsed.Records) ? parsed.Records : [parsed.Records];
124
+ return values.map(value => normalizeRecord(value)).filter(Boolean);
125
+ }
126
+
127
+ export async function captureExactWindowsProcessIdentity(pid, options = {}) {
128
+ const processId = positivePid(pid);
129
+ if (!processId) return null;
130
+ const snapshot = await (options.querySnapshotImpl || queryExactWindowsProcessSnapshot)(options);
131
+ return snapshot.find(record => record.pid === processId) || null;
132
+ }
133
+
134
+ export function registerOwnedWindowsChild(child, {
135
+ platform = process.platform,
136
+ querySnapshotImpl = queryExactWindowsProcessSnapshot,
137
+ execFileImpl = execFile,
138
+ captureTimeoutMs = 2_500,
139
+ retryMs = 50
140
+ } = {}) {
141
+ if (platform !== 'win32' || !positivePid(child?.pid)) return Promise.resolve(null);
142
+ if (child.livedeskWindowsIdentityPromise) return child.livedeskWindowsIdentityPromise;
143
+ child.livedeskWindowsSpawnedAtMs ||= Date.now();
144
+ child.livedeskWindowsTrackedRecords ||= new Map();
145
+ if (!child.livedeskWindowsExitListenerRegistered) {
146
+ child.livedeskWindowsExitListenerRegistered = true;
147
+ child.once?.('exit', () => {
148
+ if (!child.livedeskWindowsExitedAtMs) child.livedeskWindowsExitedAtMs = Date.now();
149
+ });
150
+ }
151
+ child.livedeskWindowsIdentityPromise = (async () => {
152
+ const deadline = Date.now() + Math.max(50, Number(captureTimeoutMs) || 2_500);
153
+ let lastError = null;
154
+ do {
155
+ if ((child.exitCode !== null && child.exitCode !== undefined)
156
+ || child.signalCode
157
+ || child.livedeskWindowsExitedAtMs) {
158
+ child.livedeskWindowsExitedAtMs ||= Date.now();
159
+ }
160
+ try {
161
+ const snapshot = await querySnapshotImpl({ execFileImpl });
162
+ const identity = snapshot.find(record => record.pid === positivePid(child.pid)) || null;
163
+ if (identity && !child.livedeskWindowsExitedAtMs) return identity;
164
+ lastError = null;
165
+ if (child.livedeskWindowsExitedAtMs) {
166
+ for (const record of collectRootAbsentWindowsTree(child.pid, snapshot, {
167
+ spawnedAtMs: child.livedeskWindowsSpawnedAtMs,
168
+ exitedAtMs: child.livedeskWindowsExitedAtMs
169
+ })) {
170
+ child.livedeskWindowsTrackedRecords.set(`${record.pid}:${record.startOrder}`, record);
171
+ }
172
+ child.livedeskWindowsSafeExitSnapshot = true;
173
+ return null;
174
+ }
175
+ } catch (error) {
176
+ lastError = error;
177
+ }
178
+ const remainingMs = deadline - Date.now();
179
+ if (remainingMs <= 0) break;
180
+ await new Promise(resolveWait => setTimeout(
181
+ resolveWait,
182
+ Math.min(Math.max(1, Number(retryMs) || 50), remainingMs)
183
+ ));
184
+ } while (Date.now() < deadline);
185
+ throw lastError || new Error(`windows-owned-process-identity-timeout:${child.pid}`);
186
+ })().then(identity => {
187
+ child.livedeskWindowsIdentity = identity;
188
+ child.livedeskWindowsIdentityError = null;
189
+ return identity;
190
+ }).catch(error => {
191
+ child.livedeskWindowsIdentityError = error;
192
+ return null;
193
+ });
194
+ return child.livedeskWindowsIdentityPromise;
195
+ }
196
+
197
+ export function collectExactWindowsProcessTree(rootIdentity, snapshot, {
198
+ rootExitedAtMs = null
199
+ } = {}) {
200
+ const root = normalizeRecord(rootIdentity, 0);
201
+ if (!root) return [];
202
+ const records = (Array.isArray(snapshot) ? snapshot : [])
203
+ .map(record => normalizeRecord(record))
204
+ .filter(Boolean);
205
+ const currentByPid = new Map(records.map(record => [record.pid, record]));
206
+ const currentRoot = currentByPid.get(root.pid) || null;
207
+ const exactRootAlive = sameRecord(currentRoot, root);
208
+ let directChildUpperBound = null;
209
+ if (!exactRootAlive) {
210
+ if (currentRoot) {
211
+ directChildUpperBound = BigInt(currentRoot.startOrder) - 1n;
212
+ } else if (Number.isFinite(Number(rootExitedAtMs)) && Number(rootExitedAtMs) > 0) {
213
+ directChildUpperBound = windowsTicksForMilliseconds(rootExitedAtMs);
214
+ } else {
215
+ return [];
216
+ }
217
+ }
218
+
219
+ const childrenByParent = new Map();
220
+ for (const record of records) {
221
+ if (!childrenByParent.has(record.parentPid)) childrenByParent.set(record.parentPid, []);
222
+ childrenByParent.get(record.parentPid).push(record);
223
+ }
224
+ const result = exactRootAlive ? [{ ...currentRoot, depth: 0 }] : [];
225
+ const queue = [{ record: root, depth: 0, virtualRoot: true }];
226
+ const seen = new Set([root.pid]);
227
+ while (queue.length > 0) {
228
+ const parent = queue.shift();
229
+ for (const child of childrenByParent.get(parent.record.pid) || []) {
230
+ if (seen.has(child.pid)) continue;
231
+ let childStart;
232
+ try {
233
+ childStart = BigInt(child.startOrder);
234
+ if (childStart < BigInt(parent.record.startOrder)) continue;
235
+ if (parent.virtualRoot
236
+ && directChildUpperBound !== null
237
+ && childStart > directChildUpperBound) continue;
238
+ } catch {
239
+ continue;
240
+ }
241
+ seen.add(child.pid);
242
+ const descendant = { ...child, depth: parent.depth + 1 };
243
+ result.push(descendant);
244
+ queue.push({ record: descendant, depth: descendant.depth, virtualRoot: false });
245
+ }
246
+ }
247
+ return result;
248
+ }
249
+
250
+ async function stopExactWindowsRecords(records, options = {}) {
251
+ const targets = (Array.isArray(records) ? records : [])
252
+ .map(record => normalizeRecord(record, record?.depth))
253
+ .filter(Boolean);
254
+ if (targets.length === 0) return { ok: true };
255
+ const encoded = Buffer.from(JSON.stringify(targets.map(target => ({
256
+ pid: target.pid,
257
+ startOrder: target.startOrder
258
+ }))), 'utf8').toString('base64');
259
+ const script = [
260
+ '$ErrorActionPreference = "Stop";',
261
+ `$json = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${encoded}"));`,
262
+ '$targets = ConvertFrom-Json -InputObject $json;',
263
+ '$failures = [System.Collections.Generic.List[string]]::new();',
264
+ 'foreach ($item in $targets) {',
265
+ ' $targetPid = [int]$item.pid;',
266
+ ' $expectedTicks = [string]$item.startOrder;',
267
+ ' try {',
268
+ ' $target = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1;',
269
+ ' if (-not $target) { continue };',
270
+ ' $actualTicks = if ($target.CreationDate) { [string]$target.CreationDate.ToUniversalTime().Ticks } else { "" };',
271
+ ' if ($actualTicks -ne $expectedTicks) { continue };',
272
+ ' Stop-Process -Id $targetPid -Force -ErrorAction Stop;',
273
+ ' } catch { $failures.Add("pid=$targetPid $($_.Exception.Message)") }',
274
+ '}',
275
+ 'if ($failures.Count -gt 0) { [Console]::Error.WriteLine(($failures -join "; ")); exit 1 }'
276
+ ].join(' ');
277
+ return runPowerShell(script, { ...options, timeoutMs: 15_000 });
278
+ }
279
+
280
+ export async function terminateExactRegisteredWindowsTree(child, {
281
+ platform = process.platform,
282
+ querySnapshotImpl = queryExactWindowsProcessSnapshot,
283
+ stopRecordsImpl = stopExactWindowsRecords,
284
+ execFileImpl = execFile,
285
+ maxAttempts = 4,
286
+ retryMs = 100
287
+ } = {}) {
288
+ if (platform !== 'win32' || !positivePid(child?.pid)) return;
289
+ const identity = child.livedeskWindowsIdentity
290
+ || await registerOwnedWindowsChild(child, { platform, querySnapshotImpl, execFileImpl });
291
+ if (!identity) {
292
+ if (child.livedeskWindowsIdentityError) throw child.livedeskWindowsIdentityError;
293
+ if (!child.livedeskWindowsSafeExitSnapshot) {
294
+ throw new Error(`windows-owned-process-exit-snapshot-unavailable:${child.pid}`);
295
+ }
296
+ if (!child.livedeskWindowsTrackedRecords?.size) return;
297
+ }
298
+
299
+ const tracked = new Map(child.livedeskWindowsTrackedRecords || []);
300
+ const trackedExitBoundaries = new Map();
301
+ let rootExitedAtMs = Number(child.livedeskWindowsExitedAtMs || 0) || null;
302
+ let latestStopAtMs = rootExitedAtMs;
303
+ let lastError = null;
304
+ const mergeTracked = (record, depth = record?.depth) => {
305
+ const normalized = normalizeRecord(record, depth);
306
+ if (!normalized) return;
307
+ const key = `${normalized.pid}:${normalized.startOrder}`;
308
+ const previous = tracked.get(key);
309
+ if (!previous || normalized.depth > previous.depth) tracked.set(key, normalized);
310
+ };
311
+ const mergeSnapshot = snapshot => {
312
+ const currentByPid = new Map(snapshot.map(record => [record.pid, record]));
313
+ if (identity) {
314
+ for (const record of collectExactWindowsProcessTree(identity, snapshot, { rootExitedAtMs })) {
315
+ mergeTracked(record);
316
+ }
317
+ } else {
318
+ for (const record of collectRootAbsentWindowsTree(child.pid, snapshot, {
319
+ spawnedAtMs: child.livedeskWindowsSpawnedAtMs,
320
+ exitedAtMs: rootExitedAtMs
321
+ })) {
322
+ mergeTracked(record);
323
+ }
324
+ }
325
+ for (const parent of [...tracked.values()]) {
326
+ const parentKey = `${parent.pid}:${parent.startOrder}`;
327
+ const parentAlive = sameRecord(currentByPid.get(parent.pid), parent);
328
+ if (!parentAlive && !trackedExitBoundaries.has(parentKey)) {
329
+ trackedExitBoundaries.set(parentKey, Date.now());
330
+ }
331
+ for (const record of collectExactWindowsProcessTree(parent, snapshot, {
332
+ rootExitedAtMs: parentAlive
333
+ ? null
334
+ : trackedExitBoundaries.get(parentKey) || latestStopAtMs
335
+ })) {
336
+ mergeTracked(record, Number(parent.depth || 0) + Number(record.depth || 0));
337
+ }
338
+ }
339
+ };
340
+ const attempts = Math.max(1, Number(maxAttempts) || 4);
341
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
342
+ let snapshot;
343
+ try {
344
+ snapshot = await querySnapshotImpl({ execFileImpl });
345
+ } catch (error) {
346
+ lastError = error;
347
+ if (attempt < attempts) {
348
+ await new Promise(resolveWait => setTimeout(
349
+ resolveWait,
350
+ Math.max(25, Number(retryMs) || 100)
351
+ ));
352
+ continue;
353
+ }
354
+ break;
355
+ }
356
+ const currentByPid = new Map(snapshot.map(record => [record.pid, record]));
357
+ if (identity && !sameRecord(currentByPid.get(identity.pid), identity) && rootExitedAtMs === null) {
358
+ rootExitedAtMs = Date.now();
359
+ }
360
+ mergeSnapshot(snapshot);
361
+ const remaining = [...tracked.values()]
362
+ .filter(record => sameRecord(currentByPid.get(record.pid), record))
363
+ .sort((left, right) => Number(right.depth || 0) - Number(left.depth || 0));
364
+ if (remaining.length === 0) return;
365
+ const result = await stopRecordsImpl(remaining, { execFileImpl });
366
+ if (result?.ok === false) {
367
+ lastError = result.error || new Error(result.stderr || 'exact-windows-stop-failed');
368
+ }
369
+ latestStopAtMs = Date.now();
370
+ for (const record of remaining) {
371
+ const key = `${record.pid}:${record.startOrder}`;
372
+ if (!trackedExitBoundaries.has(key)) trackedExitBoundaries.set(key, latestStopAtMs);
373
+ }
374
+ if (identity && remaining.some(record => sameRecord(record, identity)) && rootExitedAtMs === null) {
375
+ rootExitedAtMs = Date.now();
376
+ }
377
+ if (attempt < attempts) {
378
+ await new Promise(resolveWait => setTimeout(resolveWait, Math.max(25, Number(retryMs) || 100)));
379
+ }
380
+ }
381
+
382
+ let finalSnapshot;
383
+ try {
384
+ finalSnapshot = await querySnapshotImpl({ execFileImpl });
385
+ } catch (error) {
386
+ throw lastError || error;
387
+ }
388
+ const finalByPid = new Map(finalSnapshot.map(record => [record.pid, record]));
389
+ mergeSnapshot(finalSnapshot);
390
+ const remaining = [...tracked.values()].filter(record => (
391
+ sameRecord(finalByPid.get(record.pid), record)
392
+ ));
393
+ if (remaining.length > 0) {
394
+ throw new Error(
395
+ `exact-windows-owned-tree-still-alive:${remaining.map(record => record.pid).join(',')}`
396
+ + `${lastError?.message ? `:${lastError.message}` : ''}`
397
+ );
398
+ }
399
+ }
400
+
401
+ export async function terminateExactRegisteredWindowsTreeUntilStopped(child, {
402
+ retryForeverMs = 500,
403
+ onRetry = () => undefined,
404
+ ...options
405
+ } = {}) {
406
+ if ((options.platform || process.platform) !== 'win32' || !positivePid(child?.pid)) return;
407
+ if (child.livedeskWindowsDrainUntilStoppedPromise) {
408
+ return child.livedeskWindowsDrainUntilStoppedPromise;
409
+ }
410
+ child.livedeskWindowsDrainUntilStoppedPromise = (async () => {
411
+ let attempt = 0;
412
+ while (true) {
413
+ attempt += 1;
414
+ try {
415
+ await terminateExactRegisteredWindowsTree(child, options);
416
+ return;
417
+ } catch (error) {
418
+ onRetry(error, attempt);
419
+ if (!child.livedeskWindowsIdentity) {
420
+ child.livedeskWindowsIdentityPromise = null;
421
+ child.livedeskWindowsIdentityError = null;
422
+ child.livedeskWindowsSafeExitSnapshot = false;
423
+ }
424
+ await new Promise(resolveWait => setTimeout(
425
+ resolveWait,
426
+ Math.max(50, Number(retryForeverMs) || 500)
427
+ ));
428
+ }
429
+ }
430
+ })();
431
+ return child.livedeskWindowsDrainUntilStoppedPromise;
432
+ }