@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 0.1.3-dev.a58d7cd1092f
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/README.md +127 -0
- package/dist/bridge/bridge.base.d.ts +8 -1
- package/dist/bridge/bridge.service.d.ts +93 -3
- package/dist/bridge/bridge.service.js +1358 -47
- package/dist/bridge/bridge.types.d.ts +202 -1
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.js +47 -1
- package/dist/cli/cli.service.d.ts +45 -2
- package/dist/cli/cli.service.js +378 -26
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +386 -0
- package/dist/runtime/docker-engine-probe.d.ts +83 -0
- package/dist/runtime/docker-engine-probe.js +300 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +46 -0
- package/dist/runtime/host-listener-inspector.js +349 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +197 -0
- package/dist/runtime/host-runtime-lease.js +2203 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/memory-capacity.d.ts +68 -0
- package/dist/runtime/memory-capacity.js +121 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +284 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +121 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +72 -0
- package/dist/session/managed-attempt-tools.d.ts +21 -0
- package/dist/session/managed-attempt-tools.js +912 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +335 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +256 -26
- package/dist/session/phase-completion.d.ts +12 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +51 -10
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +24 -8
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +189 -8
- package/dist/session/phase-completion.schemas.js +117 -4
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +153 -22
- package/dist/session/pre-publication-validation.d.ts +46 -0
- package/dist/session/pre-publication-validation.js +208 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
- package/dist/session/provider-cli/provider-cli.process.js +474 -23
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
- package/dist/session/provider-cli/provider-cli.service.js +646 -90
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +91 -16
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +38 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +530 -42
- package/dist/session/session.types.d.ts +124 -1
- package/dist/session/session.types.js +231 -4
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
- package/dist/shared/process/dotnet-toolchain.js +179 -0
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/process/windows-launch.d.ts +1 -1
- package/dist/shared/process/windows-launch.js +6 -3
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +16 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +36 -2
- package/dist/worktree/dependency-warmer.js +347 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +13 -2
- package/dist/worktree/worktree.service.js +238 -47
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +8 -4
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const skippedDirectories = new Set([".git", "bin", "node_modules", "obj"]);
|
|
8
|
+
export async function configureDotNetToolchain(repoRoots, options = {}) {
|
|
9
|
+
const environment = options.environment ?? process.env;
|
|
10
|
+
const platform = options.platform ?? process.platform;
|
|
11
|
+
const requiredMajor = await requiredDotNetMajor(repoRoots);
|
|
12
|
+
if (requiredMajor === null)
|
|
13
|
+
return null;
|
|
14
|
+
const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
|
|
15
|
+
if (explicit && !path.isAbsolute(explicit)) {
|
|
16
|
+
throw new Error("dotnet_toolchain_invalid: FORGE_DOTNET_PATH must be an absolute executable path.");
|
|
17
|
+
}
|
|
18
|
+
const candidates = dotNetCandidates(environment, platform, options.homeDirectory ?? os.homedir(), requiredMajor);
|
|
19
|
+
const fileExists = options.fileExists ?? isFile;
|
|
20
|
+
const listSdks = options.listSdks ?? probeSdks;
|
|
21
|
+
const probed = [];
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
if (!await fileExists(candidate))
|
|
24
|
+
continue;
|
|
25
|
+
try {
|
|
26
|
+
const output = await listSdks(candidate);
|
|
27
|
+
const sdkVersion = sdkVersions(output).find((version) => majorVersion(version) === requiredMajor);
|
|
28
|
+
probed.push(`${candidate} (${sdkVersions(output).join(", ") || "no SDKs"})`);
|
|
29
|
+
if (!sdkVersion)
|
|
30
|
+
continue;
|
|
31
|
+
const rootPath = path.dirname(candidate);
|
|
32
|
+
prependPath(environment, rootPath, platform);
|
|
33
|
+
setEnvironmentValue(environment, "DOTNET_ROOT", rootPath);
|
|
34
|
+
if (platform === "win32")
|
|
35
|
+
setEnvironmentValue(environment, "DOTNET_ROOT_X64", rootPath);
|
|
36
|
+
setEnvironmentValue(environment, "FORGE_DOTNET_PATH", candidate);
|
|
37
|
+
return { executablePath: candidate, rootPath, sdkVersion, requiredMajor };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
probed.push(`${candidate} (${error instanceof Error ? error.message : String(error)})`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const detail = probed.length > 0 ? ` Probed: ${probed.join("; ")}.` : "";
|
|
44
|
+
throw new Error(`dotnet_toolchain_unavailable: repositories require .NET SDK ${requiredMajor}.x, but no compatible dotnet executable was found.`
|
|
45
|
+
+ ` Set FORGE_DOTNET_PATH to the absolute executable path.${detail}`);
|
|
46
|
+
}
|
|
47
|
+
async function requiredDotNetMajor(repoRoots) {
|
|
48
|
+
const majors = [];
|
|
49
|
+
for (const root of [...new Set(repoRoots.map((candidate) => path.resolve(candidate)))]) {
|
|
50
|
+
const globalMajor = await readGlobalJsonMajor(root);
|
|
51
|
+
if (globalMajor !== null) {
|
|
52
|
+
majors.push(globalMajor);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
majors.push(...await readProjectTargetMajors(root));
|
|
56
|
+
}
|
|
57
|
+
return majors.length > 0 ? Math.max(...majors) : null;
|
|
58
|
+
}
|
|
59
|
+
async function readGlobalJsonMajor(root) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(await fs.readFile(path.join(root, "global.json"), "utf8"));
|
|
62
|
+
return typeof parsed.sdk?.version === "string" ? majorVersion(parsed.sdk.version) : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function readProjectTargetMajors(root) {
|
|
69
|
+
const projects = await findProjectFiles(root, 4, 256);
|
|
70
|
+
const majors = [];
|
|
71
|
+
for (const project of projects) {
|
|
72
|
+
let source;
|
|
73
|
+
try {
|
|
74
|
+
source = await fs.readFile(project, "utf8");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
for (const match of source.matchAll(/<TargetFrameworks?>\s*([^<]+)\s*<\/TargetFrameworks?>/giu)) {
|
|
80
|
+
for (const framework of match[1]?.split(";") ?? []) {
|
|
81
|
+
const target = /^net(\d+)\./iu.exec(framework.trim());
|
|
82
|
+
if (target?.[1])
|
|
83
|
+
majors.push(Number.parseInt(target[1], 10));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return majors;
|
|
88
|
+
}
|
|
89
|
+
async function findProjectFiles(root, maximumDepth, maximumFiles) {
|
|
90
|
+
const projects = [];
|
|
91
|
+
async function visit(directory, depth) {
|
|
92
|
+
if (depth > maximumDepth || projects.length >= maximumFiles)
|
|
93
|
+
return;
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (projects.length >= maximumFiles)
|
|
103
|
+
return;
|
|
104
|
+
if (entry.isDirectory() && !skippedDirectories.has(entry.name)) {
|
|
105
|
+
await visit(path.join(directory, entry.name), depth + 1);
|
|
106
|
+
}
|
|
107
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".csproj")) {
|
|
108
|
+
projects.push(path.join(directory, entry.name));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await visit(root, 0);
|
|
113
|
+
return projects;
|
|
114
|
+
}
|
|
115
|
+
function dotNetCandidates(environment, platform, homeDirectory, requiredMajor) {
|
|
116
|
+
const executable = platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
117
|
+
const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
|
|
118
|
+
if (explicit)
|
|
119
|
+
return [explicit];
|
|
120
|
+
const dotNetRoot = environmentValue(environment, "DOTNET_ROOT");
|
|
121
|
+
const pathValue = environmentValue(environment, "PATH") ?? "";
|
|
122
|
+
const programFiles = environmentValue(environment, "ProgramFiles");
|
|
123
|
+
const candidates = [
|
|
124
|
+
dotNetRoot ? path.join(dotNetRoot, executable) : null,
|
|
125
|
+
path.join(homeDirectory, ".dotnet", executable),
|
|
126
|
+
path.join(homeDirectory, `.dotnet${requiredMajor}`, executable),
|
|
127
|
+
...(platform === "win32" && programFiles ? [path.join(programFiles, "dotnet", executable)] : []),
|
|
128
|
+
...pathValue.split(path.delimiter).filter(Boolean).map((directory) => path.join(directory, executable)),
|
|
129
|
+
].filter((candidate) => Boolean(candidate));
|
|
130
|
+
return [...new Map(candidates.map((candidate) => [canonicalPath(candidate, platform), path.resolve(candidate)])).values()];
|
|
131
|
+
}
|
|
132
|
+
async function probeSdks(candidate) {
|
|
133
|
+
const { stdout } = await execFileAsync(candidate, ["--list-sdks"], {
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
timeout: 10_000,
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
});
|
|
138
|
+
return stdout;
|
|
139
|
+
}
|
|
140
|
+
function sdkVersions(output) {
|
|
141
|
+
return output.split(/\r?\n/gu)
|
|
142
|
+
.map((line) => /^(\d+\.\d+\.\d+(?:[-+][^\s]+)?)/u.exec(line.trim())?.[1] ?? null)
|
|
143
|
+
.filter((version) => version !== null);
|
|
144
|
+
}
|
|
145
|
+
function majorVersion(version) {
|
|
146
|
+
const match = /^(\d+)\./u.exec(version.trim());
|
|
147
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : null;
|
|
148
|
+
}
|
|
149
|
+
async function isFile(candidate) {
|
|
150
|
+
try {
|
|
151
|
+
return (await fs.stat(candidate)).isFile();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function prependPath(environment, directory, platform) {
|
|
158
|
+
const current = environmentValue(environment, "PATH") ?? "";
|
|
159
|
+
const normalizedDirectory = canonicalPath(directory, platform);
|
|
160
|
+
const remaining = current.split(path.delimiter)
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.filter((entry) => canonicalPath(entry, platform) !== normalizedDirectory);
|
|
163
|
+
setEnvironmentValue(environment, "PATH", [directory, ...remaining].join(path.delimiter));
|
|
164
|
+
}
|
|
165
|
+
function environmentValue(environment, name) {
|
|
166
|
+
const key = Object.keys(environment).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
167
|
+
return key ? environment[key] : undefined;
|
|
168
|
+
}
|
|
169
|
+
function setEnvironmentValue(environment, name, value) {
|
|
170
|
+
const keys = Object.keys(environment).filter((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
171
|
+
const target = keys[0] ?? name;
|
|
172
|
+
for (const duplicate of keys.slice(1))
|
|
173
|
+
delete environment[duplicate];
|
|
174
|
+
environment[target] = value;
|
|
175
|
+
}
|
|
176
|
+
function canonicalPath(candidate, platform) {
|
|
177
|
+
const normalized = path.normalize(candidate);
|
|
178
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
179
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HostProcessSupervisorMetadata } from "./host-process-supervisor.js";
|
|
2
|
+
export type HostProcessRecoveryOutcome = "absent" | "terminated" | "pid_reused" | "unverifiable";
|
|
3
|
+
export interface HostProcessRecovery {
|
|
4
|
+
recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
|
|
5
|
+
}
|
|
6
|
+
/** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
|
|
7
|
+
export declare class ExactHostProcessRecovery implements HostProcessRecovery {
|
|
8
|
+
private readonly platform;
|
|
9
|
+
private readonly graceMs;
|
|
10
|
+
constructor(platform?: NodeJS.Platform, graceMs?: number);
|
|
11
|
+
recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { captureHostProcessIdentity } from "./host-process-supervisor.js";
|
|
2
|
+
/** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
|
|
3
|
+
export class ExactHostProcessRecovery {
|
|
4
|
+
platform;
|
|
5
|
+
graceMs;
|
|
6
|
+
constructor(platform = process.platform, graceMs = 2_000) {
|
|
7
|
+
this.platform = platform;
|
|
8
|
+
this.graceMs = graceMs;
|
|
9
|
+
}
|
|
10
|
+
async recover(metadata) {
|
|
11
|
+
if (!processExists(metadata.rootPid))
|
|
12
|
+
return "absent";
|
|
13
|
+
if (!metadata.rootIdentity)
|
|
14
|
+
return "unverifiable";
|
|
15
|
+
const currentIdentity = captureHostProcessIdentity(metadata.rootPid, this.platform);
|
|
16
|
+
if (!currentIdentity)
|
|
17
|
+
return processExists(metadata.rootPid) ? "unverifiable" : "absent";
|
|
18
|
+
if (currentIdentity !== metadata.rootIdentity)
|
|
19
|
+
return "pid_reused";
|
|
20
|
+
if (this.platform === "win32") {
|
|
21
|
+
signalExactProcess(metadata.rootPid, "SIGTERM");
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
signalExactProcessGroup(metadata.rootPid, "SIGTERM");
|
|
25
|
+
}
|
|
26
|
+
await waitForExit(metadata.rootPid, this.graceMs);
|
|
27
|
+
if (processExists(metadata.rootPid)) {
|
|
28
|
+
if (captureHostProcessIdentity(metadata.rootPid, this.platform) !== metadata.rootIdentity) {
|
|
29
|
+
return "pid_reused";
|
|
30
|
+
}
|
|
31
|
+
if (this.platform === "win32") {
|
|
32
|
+
signalExactProcess(metadata.rootPid, "SIGKILL");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
signalExactProcessGroup(metadata.rootPid, "SIGKILL");
|
|
36
|
+
}
|
|
37
|
+
await waitForExit(metadata.rootPid, this.graceMs);
|
|
38
|
+
}
|
|
39
|
+
if (processExists(metadata.rootPid)
|
|
40
|
+
&& captureHostProcessIdentity(metadata.rootPid, this.platform) === metadata.rootIdentity) {
|
|
41
|
+
throw new Error("host_process_recovery_degraded: exact supervised process tree did not stop.");
|
|
42
|
+
}
|
|
43
|
+
return "terminated";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function processExists(pid) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return error.code === "EPERM";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function signalExactProcess(pid, signal) {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, signal);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code !== "ESRCH")
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function signalExactProcessGroup(pid, signal) {
|
|
65
|
+
try {
|
|
66
|
+
process.kill(-pid, signal);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "ESRCH")
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function waitForExit(pid, timeoutMs) {
|
|
74
|
+
const deadline = Date.now() + timeoutMs;
|
|
75
|
+
while (Date.now() < deadline && processExists(pid)) {
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
export type HostProcessSupervisorStrategy = "windows_job_object" | "unix_process_group";
|
|
3
|
+
export interface HostProcessSupervisionRequest {
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly args: readonly string[];
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
8
|
+
}
|
|
9
|
+
export interface HostProcessSupervisedLaunch {
|
|
10
|
+
readonly command: string;
|
|
11
|
+
readonly args: readonly string[];
|
|
12
|
+
readonly detached: boolean;
|
|
13
|
+
readonly supervisorId: string;
|
|
14
|
+
readonly strategy: HostProcessSupervisorStrategy;
|
|
15
|
+
readonly metadataPath: string;
|
|
16
|
+
readonly memoryTelemetryPath?: string;
|
|
17
|
+
readonly payloadPath?: string;
|
|
18
|
+
readonly payloadSha256?: string;
|
|
19
|
+
readonly readyPath?: string;
|
|
20
|
+
readonly startGrantPath?: string;
|
|
21
|
+
readonly commandDigest: string;
|
|
22
|
+
}
|
|
23
|
+
export interface HostProcessProviderReady {
|
|
24
|
+
readonly schemaVersion: 1;
|
|
25
|
+
readonly supervisorId: string;
|
|
26
|
+
readonly rootPid?: number;
|
|
27
|
+
readonly rootIdentity?: string | null;
|
|
28
|
+
readonly providerPid: number;
|
|
29
|
+
readonly providerCreatedAt: string;
|
|
30
|
+
readonly providerIdentity?: string | null;
|
|
31
|
+
readonly startAttempt: number;
|
|
32
|
+
}
|
|
33
|
+
export interface HostProcessSupervisorMetadata {
|
|
34
|
+
readonly schemaVersion: 1;
|
|
35
|
+
readonly supervisorId: string;
|
|
36
|
+
readonly strategy: HostProcessSupervisorStrategy;
|
|
37
|
+
readonly rootPid: number;
|
|
38
|
+
readonly rootCreatedAt: string;
|
|
39
|
+
readonly rootIdentity?: string | null;
|
|
40
|
+
readonly commandDigest: string;
|
|
41
|
+
readonly worktreePath: string;
|
|
42
|
+
readonly runtimeLeaseId: string;
|
|
43
|
+
readonly dispatchAttemptId: string;
|
|
44
|
+
readonly fencingToken: string;
|
|
45
|
+
readonly bridgePid: number;
|
|
46
|
+
readonly providerPid?: number;
|
|
47
|
+
readonly providerCreatedAt?: string;
|
|
48
|
+
readonly providerIdentity?: string | null;
|
|
49
|
+
readonly providerStartAttempt?: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function resolveHostProcessSupervisedLaunch(request: HostProcessSupervisionRequest, resolvedCommand: string, resolvedArgs: readonly string[], platform?: NodeJS.Platform): HostProcessSupervisedLaunch | null;
|
|
52
|
+
export declare function persistHostProcessSupervisorMetadata(launch: HostProcessSupervisedLaunch, request: HostProcessSupervisionRequest, rootPid: number, rootCreatedAt: string, providerReady?: HostProcessProviderReady): HostProcessSupervisorMetadata;
|
|
53
|
+
/**
|
|
54
|
+
* Captures an OS-issued process birth marker. Recovery compares this marker
|
|
55
|
+
* before signalling a persisted PID so PID reuse can never redirect cleanup
|
|
56
|
+
* at an unrelated process.
|
|
57
|
+
*/
|
|
58
|
+
export declare function captureHostProcessIdentity(pid: number, platform?: NodeJS.Platform): string | null;
|
|
59
|
+
export declare function terminateHostProcessTree(child: ChildProcess, launch: HostProcessSupervisedLaunch | null, graceMs?: number): Promise<void>;
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const windowsJobSupervisorPath = fileURLToPath(new URL("../../../runtime/windows-job-supervisor.ps1", import.meta.url));
|
|
7
|
+
export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, resolvedArgs, platform = process.platform) {
|
|
8
|
+
const env = request.env;
|
|
9
|
+
if (env?.FORGE_RUNTIME_MODE !== "host")
|
|
10
|
+
return null;
|
|
11
|
+
const supervisorId = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_ID");
|
|
12
|
+
const metadataPath = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_METADATA_PATH");
|
|
13
|
+
const logDirectory = requiredEnvironment(env, "FORGE_LOG_DIR");
|
|
14
|
+
assertMetadataPath(logDirectory, metadataPath);
|
|
15
|
+
const commandDigest = createHash("sha256")
|
|
16
|
+
.update(JSON.stringify([resolvedCommand, ...resolvedArgs]))
|
|
17
|
+
.digest("hex");
|
|
18
|
+
if (platform === "win32") {
|
|
19
|
+
if (!fs.existsSync(windowsJobSupervisorPath)) {
|
|
20
|
+
throw new Error(`host_process_supervisor_missing: ${windowsJobSupervisorPath}`);
|
|
21
|
+
}
|
|
22
|
+
const payloadPath = `${metadataPath}.launch.json`;
|
|
23
|
+
const readyPath = `${metadataPath}.provider-ready.json`;
|
|
24
|
+
const startGrantPath = `${metadataPath}.provider-start-granted`;
|
|
25
|
+
const memoryTelemetryPath = `${metadataPath}.memory.json`;
|
|
26
|
+
const serializedPayload = `${JSON.stringify({
|
|
27
|
+
command: resolvedCommand,
|
|
28
|
+
args: resolvedArgs,
|
|
29
|
+
cwd: request.cwd,
|
|
30
|
+
parentPid: process.pid,
|
|
31
|
+
supervisorId,
|
|
32
|
+
readyPath,
|
|
33
|
+
startGrantPath,
|
|
34
|
+
memoryTelemetryPath,
|
|
35
|
+
})}\n`;
|
|
36
|
+
const payloadSha256 = createHash("sha256").update(serializedPayload).digest("hex");
|
|
37
|
+
fs.writeFileSync(payloadPath, serializedPayload, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
flag: "wx",
|
|
40
|
+
mode: 0o600,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
command: windowsPowerShellPath(),
|
|
44
|
+
args: [
|
|
45
|
+
"-NoLogo",
|
|
46
|
+
"-NoProfile",
|
|
47
|
+
"-NonInteractive",
|
|
48
|
+
"-ExecutionPolicy",
|
|
49
|
+
"Bypass",
|
|
50
|
+
"-File",
|
|
51
|
+
windowsJobSupervisorPath,
|
|
52
|
+
"-Mode",
|
|
53
|
+
"Outer",
|
|
54
|
+
"-PayloadPath",
|
|
55
|
+
payloadPath,
|
|
56
|
+
"-PayloadSha256",
|
|
57
|
+
payloadSha256,
|
|
58
|
+
],
|
|
59
|
+
detached: false,
|
|
60
|
+
supervisorId,
|
|
61
|
+
strategy: "windows_job_object",
|
|
62
|
+
metadataPath,
|
|
63
|
+
memoryTelemetryPath,
|
|
64
|
+
payloadPath,
|
|
65
|
+
payloadSha256,
|
|
66
|
+
readyPath,
|
|
67
|
+
startGrantPath,
|
|
68
|
+
commandDigest,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
command: resolvedCommand,
|
|
73
|
+
args: resolvedArgs,
|
|
74
|
+
detached: true,
|
|
75
|
+
supervisorId,
|
|
76
|
+
strategy: "unix_process_group",
|
|
77
|
+
metadataPath,
|
|
78
|
+
commandDigest,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function persistHostProcessSupervisorMetadata(launch, request, rootPid, rootCreatedAt, providerReady) {
|
|
82
|
+
if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
|
|
83
|
+
throw new Error("host_process_supervisor_invalid: supervised root PID was not assigned.");
|
|
84
|
+
}
|
|
85
|
+
const env = request.env ?? {};
|
|
86
|
+
const metadata = {
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
supervisorId: launch.supervisorId,
|
|
89
|
+
strategy: launch.strategy,
|
|
90
|
+
rootPid,
|
|
91
|
+
rootCreatedAt,
|
|
92
|
+
rootIdentity: providerReady?.rootPid === rootPid
|
|
93
|
+
? providerReady.rootIdentity ?? null
|
|
94
|
+
: captureHostProcessIdentity(rootPid),
|
|
95
|
+
commandDigest: launch.commandDigest,
|
|
96
|
+
worktreePath: request.cwd,
|
|
97
|
+
runtimeLeaseId: requiredEnvironment(env, "FORGE_RESOURCE_LEASE_ID"),
|
|
98
|
+
dispatchAttemptId: requiredEnvironment(env, "FORGE_DISPATCH_ATTEMPT_ID"),
|
|
99
|
+
fencingToken: requiredEnvironment(env, "FORGE_FENCING_TOKEN"),
|
|
100
|
+
bridgePid: process.pid,
|
|
101
|
+
providerPid: providerReady?.providerPid,
|
|
102
|
+
providerCreatedAt: providerReady?.providerCreatedAt,
|
|
103
|
+
providerIdentity: providerReady?.providerIdentity
|
|
104
|
+
?? (providerReady ? captureHostProcessIdentity(providerReady.providerPid) : undefined),
|
|
105
|
+
providerStartAttempt: providerReady?.startAttempt,
|
|
106
|
+
};
|
|
107
|
+
fs.writeFileSync(launch.metadataPath, `${JSON.stringify(metadata)}\n`, {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
flag: "wx",
|
|
110
|
+
mode: 0o600,
|
|
111
|
+
});
|
|
112
|
+
return metadata;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Captures an OS-issued process birth marker. Recovery compares this marker
|
|
116
|
+
* before signalling a persisted PID so PID reuse can never redirect cleanup
|
|
117
|
+
* at an unrelated process.
|
|
118
|
+
*/
|
|
119
|
+
export function captureHostProcessIdentity(pid, platform = process.platform) {
|
|
120
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
if (platform === "linux") {
|
|
124
|
+
const bootId = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
125
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
126
|
+
const close = stat.lastIndexOf(")");
|
|
127
|
+
const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/u) : [];
|
|
128
|
+
const startTicks = fields[19];
|
|
129
|
+
return bootId && startTicks && /^\d+$/u.test(startTicks)
|
|
130
|
+
? `linux:${bootId}:${startTicks}`
|
|
131
|
+
: null;
|
|
132
|
+
}
|
|
133
|
+
if (platform === "win32") {
|
|
134
|
+
const output = execFileSync(windowsPowerShellPath(), [
|
|
135
|
+
"-NoLogo",
|
|
136
|
+
"-NoProfile",
|
|
137
|
+
"-NonInteractive",
|
|
138
|
+
"-Command",
|
|
139
|
+
`(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
|
|
140
|
+
], {
|
|
141
|
+
encoding: "utf8",
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
timeout: 10_000,
|
|
144
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
145
|
+
}).trim();
|
|
146
|
+
return /^\d+$/u.test(output) ? `win32:${output}` : null;
|
|
147
|
+
}
|
|
148
|
+
// Other Unix variants do not expose a sufficiently strong birth marker
|
|
149
|
+
// through Node. Fail closed rather than trust a second-resolution ps time.
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
|
|
157
|
+
if (!launch) {
|
|
158
|
+
await terminateUnsupervisedProcessTree(child, graceMs);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (launch.strategy === "windows_job_object") {
|
|
162
|
+
const rootPid = child.pid ?? 0;
|
|
163
|
+
const metadata = readSupervisorMetadata(launch.metadataPath);
|
|
164
|
+
const rootIdentity = metadata?.rootPid === rootPid
|
|
165
|
+
? metadata.rootIdentity ?? null
|
|
166
|
+
: captureHostProcessIdentity(rootPid, "win32");
|
|
167
|
+
try {
|
|
168
|
+
child.kill("SIGTERM");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Closing the launcher is best effort when the Job Object already settled.
|
|
172
|
+
}
|
|
173
|
+
await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
|
|
174
|
+
if (exactProcessExists(rootPid, rootIdentity, "win32")) {
|
|
175
|
+
forceWindowsProcessTree(rootPid);
|
|
176
|
+
await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
|
|
177
|
+
}
|
|
178
|
+
if (exactProcessExists(rootPid, rootIdentity, "win32")) {
|
|
179
|
+
throw new Error("host_process_supervisor_termination_failed: supervised Windows root did not stop.");
|
|
180
|
+
}
|
|
181
|
+
const providerPid = metadata?.providerPid ?? 0;
|
|
182
|
+
const providerIdentity = metadata?.providerIdentity ?? null;
|
|
183
|
+
if (exactProcessExists(providerPid, providerIdentity, "win32")) {
|
|
184
|
+
forceWindowsProcessTree(providerPid);
|
|
185
|
+
await waitForExactProcessExit(providerPid, providerIdentity, graceMs, "win32");
|
|
186
|
+
}
|
|
187
|
+
if (exactProcessExists(providerPid, providerIdentity, "win32")) {
|
|
188
|
+
throw new Error("host_process_supervisor_termination_failed: supervised Windows provider tree did not stop.");
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const processGroupId = child.pid;
|
|
193
|
+
if (!processGroupId || !signalUnixProcessGroup(processGroupId, "SIGTERM"))
|
|
194
|
+
return;
|
|
195
|
+
await waitForUnixProcessGroupExit(processGroupId, graceMs);
|
|
196
|
+
if (unixProcessGroupExists(processGroupId)) {
|
|
197
|
+
signalUnixProcessGroup(processGroupId, "SIGKILL");
|
|
198
|
+
await waitForUnixProcessGroupExit(processGroupId, graceMs);
|
|
199
|
+
}
|
|
200
|
+
if (unixProcessGroupExists(processGroupId)) {
|
|
201
|
+
throw new Error("host_process_supervisor_termination_failed: supervised Unix process group did not stop.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function terminateUnsupervisedProcessTree(child, graceMs) {
|
|
205
|
+
const rootPid = child.pid ?? 0;
|
|
206
|
+
if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
|
|
207
|
+
try {
|
|
208
|
+
child.kill("SIGTERM");
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// The process already settled before a PID was assigned.
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (process.platform === "win32") {
|
|
216
|
+
// Without a Job Object there is no trustworthy ancestry record after the
|
|
217
|
+
// root has already exited. Never target a potentially reused Windows PID.
|
|
218
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
219
|
+
return;
|
|
220
|
+
const rootIdentity = captureHostProcessIdentity(rootPid, "win32");
|
|
221
|
+
forceWindowsProcessTree(rootPid);
|
|
222
|
+
await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
|
|
223
|
+
if (exactProcessExists(rootPid, rootIdentity, "win32")) {
|
|
224
|
+
throw new Error("host_process_supervisor_termination_failed: unsupervised Windows provider tree did not stop.");
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!signalUnixProcessGroup(rootPid, "SIGTERM"))
|
|
229
|
+
return;
|
|
230
|
+
await waitForUnixProcessGroupExit(rootPid, graceMs);
|
|
231
|
+
if (unixProcessGroupExists(rootPid)) {
|
|
232
|
+
signalUnixProcessGroup(rootPid, "SIGKILL");
|
|
233
|
+
await waitForUnixProcessGroupExit(rootPid, graceMs);
|
|
234
|
+
}
|
|
235
|
+
if (unixProcessGroupExists(rootPid)) {
|
|
236
|
+
throw new Error("host_process_supervisor_termination_failed: unsupervised Unix provider tree did not stop.");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function requiredEnvironment(env, name) {
|
|
240
|
+
const value = env[name]?.trim();
|
|
241
|
+
if (!value)
|
|
242
|
+
throw new Error(`host_process_supervisor_invalid: ${name} is required.`);
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
function assertMetadataPath(logDirectory, metadataPath) {
|
|
246
|
+
const root = path.resolve(logDirectory);
|
|
247
|
+
const target = path.resolve(metadataPath);
|
|
248
|
+
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
|
249
|
+
throw new Error("host_process_supervisor_invalid: metadata path must be inside FORGE_LOG_DIR.");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function windowsPowerShellPath() {
|
|
253
|
+
const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
|
|
254
|
+
const candidate = path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
255
|
+
return fs.existsSync(candidate) ? candidate : "powershell.exe";
|
|
256
|
+
}
|
|
257
|
+
function signalUnixProcessGroup(processGroupId, signal) {
|
|
258
|
+
try {
|
|
259
|
+
process.kill(-processGroupId, signal);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (error.code === "ESRCH")
|
|
264
|
+
return false;
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function unixProcessGroupExists(processGroupId) {
|
|
269
|
+
try {
|
|
270
|
+
process.kill(-processGroupId, 0);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (error.code === "ESRCH")
|
|
275
|
+
return false;
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function waitForUnixProcessGroupExit(processGroupId, timeoutMs) {
|
|
280
|
+
const deadline = Date.now() + timeoutMs;
|
|
281
|
+
while (Date.now() < deadline && unixProcessGroupExists(processGroupId)) {
|
|
282
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function readSupervisorMetadata(metadataPath) {
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function exactProcessExists(pid, identity, platform) {
|
|
294
|
+
if (!identity || !processExists(pid))
|
|
295
|
+
return false;
|
|
296
|
+
return captureHostProcessIdentity(pid, platform) === identity;
|
|
297
|
+
}
|
|
298
|
+
function processExists(pid) {
|
|
299
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
300
|
+
return false;
|
|
301
|
+
try {
|
|
302
|
+
process.kill(pid, 0);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
return error.code === "EPERM";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function waitForExactProcessExit(pid, identity, timeoutMs, platform) {
|
|
310
|
+
const deadline = Date.now() + timeoutMs;
|
|
311
|
+
while (Date.now() < deadline && exactProcessExists(pid, identity, platform)) {
|
|
312
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function forceWindowsProcessTree(pid) {
|
|
316
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
317
|
+
return;
|
|
318
|
+
const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
|
|
319
|
+
const taskkill = path.win32.join(systemRoot, "System32", "taskkill.exe");
|
|
320
|
+
try {
|
|
321
|
+
execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], {
|
|
322
|
+
windowsHide: true,
|
|
323
|
+
timeout: 5_000,
|
|
324
|
+
stdio: "ignore",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// A racing process exit is verified by the caller before it returns.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -2,7 +2,7 @@ export interface LaunchCommand {
|
|
|
2
2
|
readonly command: string;
|
|
3
3
|
readonly args: readonly string[];
|
|
4
4
|
}
|
|
5
|
-
export declare function buildWindowsCmdLaunch(command: string, args: readonly string[]): LaunchCommand;
|
|
5
|
+
export declare function buildWindowsCmdLaunch(command: string, args: readonly string[], comSpec?: string | undefined): LaunchCommand;
|
|
6
6
|
export declare function buildWindowsLaunchCommand(commandPath: string, args: readonly string[]): LaunchCommand;
|
|
7
7
|
export declare function windowsCommandLine(command: string, args: readonly string[]): string;
|
|
8
8
|
export declare function quoteWindowsCommandLineArg(value: string): string;
|