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.
- package/bin/livedesk.js +974 -304
- package/bootstrap/client-process-identity.js +20 -6
- package/bootstrap/legacy-client-update.js +22 -20
- package/bootstrap/runtime-lock.js +362 -45
- package/bootstrap/runtime-shutdown.js +414 -0
- package/client/bin/livedesk-client-node.js +17 -4
- package/client/bin/livedesk-client.js +365 -104
- package/client/package.json +5 -5
- package/client/src/runtime/agent-process-lifecycle.js +194 -58
- package/client/src/runtime/windows-owned-process-manifest.js +235 -0
- package/electron/main.mjs +55 -38
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +284 -88
- package/hub/src/server.js +173 -28
- package/hub/src/transport/relay-hub-control.js +1045 -0
- package/hub/src/transport/udp-rendezvous.js +2 -0
- package/package.json +6 -6
- package/runtime-core/src/role-transition-supervisor.js +20 -18
- package/runtime-core/src/windows-owned-process-tree.js +432 -0
- package/web/dist/assets/index-5opXQY8X.js +25 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-D3VhnrP6.js +0 -25
|
@@ -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.
|
|
4
|
-
"livedeskClientVersion": "0.1.
|
|
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.
|
|
54
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
55
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
56
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
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 {
|
|
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(
|
|
152
|
-
const processId = positiveInteger(pid);
|
|
153
|
-
if (processId <= 1
|
|
155
|
+
async function terminateOwnedProcessTree(child) {
|
|
156
|
+
const processId = positiveInteger(child?.pid);
|
|
157
|
+
if (processId <= 1) return;
|
|
154
158
|
if (process.platform === 'win32') {
|
|
155
|
-
await
|
|
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
|
|
256
|
-
await terminateOwnedProcessTree(child
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|