@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.04a1e5b9e484
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 +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1563 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- 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 +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +16 -0
- package/dist/runtime/attempt-process-supervisor.js +94 -23
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +192 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +168 -55
- package/dist/runtime/host-runtime-lease.d.ts +92 -8
- package/dist/runtime/host-runtime-lease.js +1141 -114
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +29 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +19 -9
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +137 -107
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +489 -34
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +106 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +126 -17
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +773 -69
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- 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.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.js +3 -2
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +60 -11
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/shared/time/monotonic.d.ts +1 -0
- package/dist/shared/time/monotonic.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type GitRunner = (args: readonly string[]) => Promise<{
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout?: string;
|
|
4
|
+
}>;
|
|
5
|
+
export type UnreferencedExport = {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly testOnly: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function isTestPath(file: string): boolean;
|
|
11
|
+
export declare function addedExports(diff: string): Map<string, string>;
|
|
12
|
+
export declare function findUnreferencedNewExports(git: GitRunner, worktreePath: string, baselineRevision: string | undefined): Promise<UnreferencedExport[]>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Finds exports added by this change that no production code uses. An export reached only from
|
|
2
|
+
// tests is code the current Story does not actually deliver: reviewers flag it and the Story
|
|
3
|
+
// cannot fix it without changing scope, so publication stops before the PR is opened.
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const SourceFile = /\.(?:[cm]?[jt]sx?)$/u;
|
|
7
|
+
const TestFile = /(?:^|\/)(?:test|tests|__tests__|e2e)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
8
|
+
const ExportDeclaration = /^\+\s*export\s+(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\*?|const|let|var|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/u;
|
|
9
|
+
const Lines = /\r?\n/u;
|
|
10
|
+
const MaximumNames = 200;
|
|
11
|
+
export function isTestPath(file) {
|
|
12
|
+
return TestFile.test(file);
|
|
13
|
+
}
|
|
14
|
+
export function addedExports(diff) {
|
|
15
|
+
const added = new Map();
|
|
16
|
+
let file = null;
|
|
17
|
+
for (const line of diff.split(Lines)) {
|
|
18
|
+
if (line.startsWith("+++ ")) {
|
|
19
|
+
const target = line.slice(4).trim();
|
|
20
|
+
file = target === "/dev/null" ? null : target.replace(/^b\//u, "");
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!file || !SourceFile.test(file) || isTestPath(file) || file.endsWith(".d.ts"))
|
|
24
|
+
continue;
|
|
25
|
+
const match = ExportDeclaration.exec(line);
|
|
26
|
+
if (match && added.size < MaximumNames)
|
|
27
|
+
added.set(match[1], file);
|
|
28
|
+
}
|
|
29
|
+
return added;
|
|
30
|
+
}
|
|
31
|
+
export async function findUnreferencedNewExports(git, worktreePath, baselineRevision) {
|
|
32
|
+
const diff = await git(["diff", "--no-color", "--no-ext-diff", "-U0", baselineRevision?.trim() || "HEAD", "--"]);
|
|
33
|
+
if (diff.exitCode !== 0)
|
|
34
|
+
return [];
|
|
35
|
+
// New files are untracked until Bridge commits them, so git diff alone would miss their exports.
|
|
36
|
+
const untracked = await git(["ls-files", "--others", "--exclude-standard"]);
|
|
37
|
+
const sections = [diff.stdout ?? ""];
|
|
38
|
+
for (const file of (untracked.exitCode === 0 ? untracked.stdout ?? "" : "").split(Lines).filter(row => SourceFile.test(row) && !isTestPath(row))) {
|
|
39
|
+
const content = await fs.readFile(path.join(worktreePath, file), "utf8").catch(() => "");
|
|
40
|
+
sections.push(`+++ b/${file}`, ...content.split(Lines).map(line => `+${line}`));
|
|
41
|
+
}
|
|
42
|
+
const found = [];
|
|
43
|
+
for (const [name, file] of addedExports(sections.join("\n"))) {
|
|
44
|
+
const grep = await git(["grep", "--untracked", "-I", "-c", "-w", "-F", "-e", name, "--"]);
|
|
45
|
+
// git grep exits 1 for no match; any other failure skips the name rather than guessing.
|
|
46
|
+
if (grep.exitCode !== 0 && grep.exitCode !== 1)
|
|
47
|
+
continue;
|
|
48
|
+
const counts = (grep.stdout ?? "").split(Lines).filter(Boolean).map(row => {
|
|
49
|
+
const separator = row.lastIndexOf(":");
|
|
50
|
+
return { file: row.slice(0, separator).replaceAll("\\", "/"), count: Number(row.slice(separator + 1)) };
|
|
51
|
+
});
|
|
52
|
+
const ownUses = counts.find(row => row.file === file)?.count ?? 0;
|
|
53
|
+
const elsewhere = counts.filter(row => row.file !== file);
|
|
54
|
+
if (ownUses > 1 || elsewhere.some(row => !isTestPath(row.file)))
|
|
55
|
+
continue;
|
|
56
|
+
found.push({ name, path: file, testOnly: elsewhere.length > 0 });
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatBytes(bytes: number): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function formatBytes(bytes) {
|
|
2
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
3
|
+
return "0 B";
|
|
4
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
5
|
+
const power = Math.max(0, Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024))));
|
|
6
|
+
const value = bytes / 1024 ** power;
|
|
7
|
+
const precision = power === 0 || value >= 100 ? 0 : 1;
|
|
8
|
+
return `${value.toFixed(precision)} ${units[power]}`;
|
|
9
|
+
}
|
package/dist/shared/git/git.js
CHANGED
|
@@ -8,7 +8,8 @@ export class GitClient {
|
|
|
8
8
|
const result = await this.processRunner.run({
|
|
9
9
|
command: "git",
|
|
10
10
|
args,
|
|
11
|
-
cwd
|
|
11
|
+
cwd,
|
|
12
|
+
...(args.includes("-z") ? { rawOutput: true } : {}),
|
|
12
13
|
});
|
|
13
14
|
if (result.exitCode !== 0) {
|
|
14
15
|
throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function isSubPath(root: string, candidate: string): boolean;
|
|
2
2
|
export declare function resolveExistingDirectoryPath(inputPath: string): Promise<string>;
|
|
3
|
+
export declare function resolvePathIdentity(inputPath: string): Promise<string>;
|
|
3
4
|
export declare function ensureDirectory(inputPath: string): Promise<string>;
|
|
4
5
|
export declare function safeSegment(value: string, fallback: string): string;
|
package/dist/shared/path/path.js
CHANGED
|
@@ -15,6 +15,24 @@ export async function resolveExistingDirectoryPath(inputPath) {
|
|
|
15
15
|
}
|
|
16
16
|
return await fs.realpath(inputPath);
|
|
17
17
|
}
|
|
18
|
+
export async function resolvePathIdentity(inputPath) {
|
|
19
|
+
let existingAncestor = path.resolve(inputPath);
|
|
20
|
+
const missingSegments = [];
|
|
21
|
+
while (true) {
|
|
22
|
+
try {
|
|
23
|
+
return path.join(await fs.realpath(existingAncestor), ...missingSegments);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== "ENOENT")
|
|
27
|
+
throw error;
|
|
28
|
+
const parent = path.dirname(existingAncestor);
|
|
29
|
+
if (parent === existingAncestor)
|
|
30
|
+
throw error;
|
|
31
|
+
missingSegments.unshift(path.basename(existingAncestor));
|
|
32
|
+
existingAncestor = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
18
36
|
export async function ensureDirectory(inputPath) {
|
|
19
37
|
await fs.mkdir(inputPath, { recursive: true });
|
|
20
38
|
return await resolveExistingDirectoryPath(inputPath);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface DotNetToolchainSelection {
|
|
2
|
+
readonly executablePath: string;
|
|
3
|
+
readonly rootPath: string;
|
|
4
|
+
readonly sdkVersion: string;
|
|
5
|
+
readonly requiredMajor: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DotNetToolchainOptions {
|
|
8
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
9
|
+
readonly platform?: NodeJS.Platform;
|
|
10
|
+
readonly homeDirectory?: string;
|
|
11
|
+
readonly fileExists?: (candidate: string) => Promise<boolean>;
|
|
12
|
+
readonly listSdks?: (candidate: string) => Promise<string>;
|
|
13
|
+
readonly resolveSdk?: (candidate: string, repositoryRoot: string) => Promise<string>;
|
|
14
|
+
/** Supplied roots that hold many repositories, such as the bridge clone folder. */
|
|
15
|
+
readonly cloneFolders?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function configureDotNetToolchain(repoRoots: readonly string[], options?: DotNetToolchainOptions): Promise<DotNetToolchainSelection | null>;
|
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
const maximumDiscoveryDepth = 32;
|
|
9
|
+
const maximumDiscoveryEntries = 20_000;
|
|
10
|
+
const maximumProjectFiles = 256;
|
|
11
|
+
const maximumInvocationContexts = 256;
|
|
12
|
+
export async function configureDotNetToolchain(repoRoots, options = {}) {
|
|
13
|
+
const environment = options.environment ?? process.env;
|
|
14
|
+
const platform = options.platform ?? process.platform;
|
|
15
|
+
const requirements = await discoverSdkRequirements(repoRoots, options.cloneFolders ?? []);
|
|
16
|
+
if (requirements.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const requiredMajor = Math.max(...requirements.map((requirement) => requirement.minimumMajor));
|
|
19
|
+
const policyGroups = groupPolicyRequirements(requirements, platform);
|
|
20
|
+
const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
|
|
21
|
+
if (explicit && !path.isAbsolute(explicit)) {
|
|
22
|
+
throw new Error("dotnet_toolchain_invalid: FORGE_DOTNET_PATH must be an absolute executable path.");
|
|
23
|
+
}
|
|
24
|
+
const candidates = dotNetCandidates(environment, platform, options.homeDirectory ?? os.homedir(), requiredMajor);
|
|
25
|
+
const fileExists = options.fileExists ?? isFile;
|
|
26
|
+
const listSdks = options.listSdks ?? probeSdks;
|
|
27
|
+
const resolveSdk = options.resolveSdk ?? probeSelectedSdk;
|
|
28
|
+
const probed = [];
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (!await fileExists(candidate))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const output = await listSdks(candidate);
|
|
34
|
+
probed.push(`${candidate} (${sdkVersions(output).join(", ") || "no SDKs"})`);
|
|
35
|
+
// Project, solution, and repository-root commands can resolve different
|
|
36
|
+
// SDKs. Preserve all invocation contexts as well as nested project policies;
|
|
37
|
+
// only identical policy identities may share a probe.
|
|
38
|
+
const resolvedVersions = [];
|
|
39
|
+
for (const requirement of policyGroups) {
|
|
40
|
+
const resolved = (await resolveSdk(candidate, requirement.directory)).trim();
|
|
41
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(resolved)) {
|
|
42
|
+
throw new Error(`No valid SDK selected for ${requirement.directory}`);
|
|
43
|
+
}
|
|
44
|
+
if ((majorVersion(resolved) ?? 0) < requirement.minimumMajor) {
|
|
45
|
+
throw new Error(`SDK ${resolved} cannot satisfy .NET ${requirement.minimumMajor} in ${requirement.directory}`);
|
|
46
|
+
}
|
|
47
|
+
resolvedVersions.push(resolved);
|
|
48
|
+
}
|
|
49
|
+
// Detect a policy added, removed, or changed during discovery/probing.
|
|
50
|
+
// In particular, do not reuse a parent's probe after a nested override appears.
|
|
51
|
+
for (const requirement of requirements) {
|
|
52
|
+
const policy = await nearestSdkPolicy(requirement.directory);
|
|
53
|
+
if (policy?.path !== requirement.policy?.path || policy?.contents !== requirement.policy?.contents) {
|
|
54
|
+
throw new Error(`SDK policy changed during resolution for ${requirement.directory}; retry after repository changes finish`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const selectedVersion = resolvedVersions.sort((a, b) => a.localeCompare(b, "en", { numeric: true })).at(-1);
|
|
58
|
+
const rootPath = path.dirname(candidate);
|
|
59
|
+
prependPath(environment, rootPath, platform);
|
|
60
|
+
setEnvironmentValue(environment, "DOTNET_ROOT", rootPath);
|
|
61
|
+
if (platform === "win32")
|
|
62
|
+
setEnvironmentValue(environment, "DOTNET_ROOT_X64", rootPath);
|
|
63
|
+
setEnvironmentValue(environment, "FORGE_DOTNET_PATH", candidate);
|
|
64
|
+
return { executablePath: candidate, rootPath, sdkVersion: selectedVersion, requiredMajor: requiredMajor || majorVersion(selectedVersion) };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
probed.push(`${candidate} (${error instanceof Error ? error.message : String(error)})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const detail = probed.length > 0 ? ` Probed: ${probed.join("; ")}.` : "";
|
|
71
|
+
throw new Error(`dotnet_toolchain_unavailable: repositories require ${requiredMajor ? `.NET SDK ${requiredMajor}.x` : "a .NET SDK"}, but no compatible dotnet executable was found.`
|
|
72
|
+
+ ` Set FORGE_DOTNET_PATH to the absolute executable path.${detail}`);
|
|
73
|
+
}
|
|
74
|
+
async function discoverSdkRequirements(repoRoots, cloneFolders) {
|
|
75
|
+
const { projects, invocationDirectories } = await findDotNetInputs(repoRoots, cloneFolders);
|
|
76
|
+
const requirements = [];
|
|
77
|
+
for (const project of projects) {
|
|
78
|
+
const source = await fs.readFile(project, "utf8");
|
|
79
|
+
const directory = path.dirname(project);
|
|
80
|
+
const policy = await nearestSdkPolicy(directory);
|
|
81
|
+
const majors = [policy?.major ?? 0];
|
|
82
|
+
for (const match of source.matchAll(/<TargetFrameworks?>\s*([^<]+)\s*<\/TargetFrameworks?>/giu)) {
|
|
83
|
+
for (const framework of match[1]?.split(";") ?? []) {
|
|
84
|
+
const target = /^net(\d+)\./iu.exec(framework.trim());
|
|
85
|
+
if (target?.[1])
|
|
86
|
+
majors.push(Number.parseInt(target[1], 10));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
requirements.push({ directory, policy, minimumMajor: Math.max(...majors) });
|
|
90
|
+
}
|
|
91
|
+
for (const directory of invocationDirectories) {
|
|
92
|
+
const policy = await nearestSdkPolicy(directory);
|
|
93
|
+
requirements.push({ directory, policy, minimumMajor: policy?.major ?? 0 });
|
|
94
|
+
}
|
|
95
|
+
return requirements;
|
|
96
|
+
}
|
|
97
|
+
async function nearestSdkPolicy(projectDirectory) {
|
|
98
|
+
for (let directory = projectDirectory;; directory = path.dirname(directory)) {
|
|
99
|
+
const policyPath = path.join(directory, "global.json");
|
|
100
|
+
try {
|
|
101
|
+
const contents = await fs.readFile(policyPath, "utf8");
|
|
102
|
+
let major = null;
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(contents);
|
|
105
|
+
major = typeof parsed.sdk?.version === "string" ? majorVersion(parsed.sdk.version) : null;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// The actual SDK resolver diagnoses malformed policies; never bypass one.
|
|
109
|
+
}
|
|
110
|
+
return { path: policyPath, contents, major };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error.code !== "ENOENT")
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (path.dirname(directory) === directory)
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function groupPolicyRequirements(requirements, platform) {
|
|
121
|
+
const groups = new Map();
|
|
122
|
+
for (const requirement of requirements) {
|
|
123
|
+
const key = requirement.policy ? canonicalPath(requirement.policy.path, platform) : "no-global-json";
|
|
124
|
+
const previous = groups.get(key);
|
|
125
|
+
if (previous && previous.policy?.contents !== requirement.policy?.contents) {
|
|
126
|
+
throw new Error(`dotnet_toolchain_changed: SDK policy changed during discovery: ${requirement.policy?.path}`);
|
|
127
|
+
}
|
|
128
|
+
groups.set(key, previous
|
|
129
|
+
? { ...previous, minimumMajor: Math.max(previous.minimumMajor, requirement.minimumMajor) }
|
|
130
|
+
: requirement);
|
|
131
|
+
}
|
|
132
|
+
return [...groups.values()];
|
|
133
|
+
}
|
|
134
|
+
async function findDotNetInputs(repoRoots, cloneFolders) {
|
|
135
|
+
const projects = [];
|
|
136
|
+
const invocationDirectories = new Map();
|
|
137
|
+
const visitedDirectories = new Set();
|
|
138
|
+
const roots = [...new Set(repoRoots.map((candidate) => path.resolve(candidate)))];
|
|
139
|
+
const explicitRoots = new Set(roots.map((root) => canonicalPath(root, process.platform)));
|
|
140
|
+
const cloneFolderRoots = new Set(cloneFolders.map((folder) => canonicalPath(path.resolve(folder), process.platform)));
|
|
141
|
+
// The entry bound applies per repository. A clone folder is not one: each of
|
|
142
|
+
// its child directories is bounded as its own repository root.
|
|
143
|
+
let entryCount = 0;
|
|
144
|
+
async function visitRoot(root) {
|
|
145
|
+
entryCount = 0;
|
|
146
|
+
await visit(root, 0, root, new Set());
|
|
147
|
+
}
|
|
148
|
+
function addInvocationDirectory(directory, root) {
|
|
149
|
+
const identity = canonicalPath(directory, process.platform);
|
|
150
|
+
if (invocationDirectories.has(identity))
|
|
151
|
+
return;
|
|
152
|
+
if (invocationDirectories.size >= maximumInvocationContexts)
|
|
153
|
+
throw discoveryLimit("invocation contexts", maximumInvocationContexts, root);
|
|
154
|
+
invocationDirectories.set(identity, directory);
|
|
155
|
+
}
|
|
156
|
+
async function visit(directory, depth, root, ancestors) {
|
|
157
|
+
const directoryIdentity = canonicalPath(directory, process.platform);
|
|
158
|
+
if (visitedDirectories.has(directoryIdentity))
|
|
159
|
+
return;
|
|
160
|
+
if (depth > maximumDiscoveryDepth)
|
|
161
|
+
throw discoveryLimit("depth", maximumDiscoveryDepth, root);
|
|
162
|
+
const physicalDirectory = await fs.realpath(directory);
|
|
163
|
+
if (ancestors.has(physicalDirectory))
|
|
164
|
+
throw new Error(`dotnet_toolchain_discovery: directory cycle at ${directory}`);
|
|
165
|
+
const nextAncestors = new Set([...ancestors, physicalDirectory]);
|
|
166
|
+
const cloneFolder = depth === 0 && cloneFolderRoots.has(directoryIdentity);
|
|
167
|
+
visitedDirectories.add(directoryIdentity);
|
|
168
|
+
const entries = [];
|
|
169
|
+
for await (const entry of await fs.opendir(directory)) {
|
|
170
|
+
if (++entryCount > maximumDiscoveryEntries)
|
|
171
|
+
throw discoveryLimit("entries", maximumDiscoveryEntries, root);
|
|
172
|
+
entries.push(entry);
|
|
173
|
+
}
|
|
174
|
+
// Stable traversal also makes the representative policy probe deterministic.
|
|
175
|
+
entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
// Hidden folders (.git, .vs, .forge-worktrees, ...) are tool state, never
|
|
178
|
+
// repository sources; an explicitly supplied hidden root is still visited.
|
|
179
|
+
if (skippedDirectories.has(entry.name) || entry.name.startsWith(".") && !entry.isFile())
|
|
180
|
+
continue;
|
|
181
|
+
const entryPath = path.join(directory, entry.name);
|
|
182
|
+
const linked = entry.isSymbolicLink() ? await fs.stat(entryPath) : null;
|
|
183
|
+
if (linked?.isDirectory()) {
|
|
184
|
+
if (cloneFolder || explicitRoots.has(canonicalPath(entryPath, process.platform))) {
|
|
185
|
+
await visitRoot(entryPath);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`dotnet_toolchain_discovery: symbolic/reparse directory ${entryPath} must be supplied as an explicit repository root, not traversed from ${root}`);
|
|
189
|
+
}
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
if (cloneFolder)
|
|
192
|
+
await visitRoot(entryPath);
|
|
193
|
+
else
|
|
194
|
+
await visit(entryPath, depth + 1, root, nextAncestors);
|
|
195
|
+
}
|
|
196
|
+
else if (entry.isFile() || linked?.isFile()) {
|
|
197
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
198
|
+
if (extension === ".csproj") {
|
|
199
|
+
if (projects.length >= maximumProjectFiles)
|
|
200
|
+
throw discoveryLimit("projects", maximumProjectFiles, root);
|
|
201
|
+
projects.push(entryPath);
|
|
202
|
+
}
|
|
203
|
+
// A solution's SDK resolver starts at its directory. A global.json also
|
|
204
|
+
// defines a CLI invocation context even when every child project overrides
|
|
205
|
+
// it (or the repository contains only SDK-based tools).
|
|
206
|
+
if ([".sln", ".slnx", ".slnf"].includes(extension)
|
|
207
|
+
|| canonicalPath(entry.name, process.platform) === "global.json") {
|
|
208
|
+
addInvocationDirectory(directory, root);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const root of roots) {
|
|
214
|
+
await visitRoot(root);
|
|
215
|
+
}
|
|
216
|
+
// Supplied roots are also supported command working directories. A parent's
|
|
217
|
+
// policy cannot be dropped merely because every project/solution below it has
|
|
218
|
+
// its own override. Ignore unrelated roots without any discovered .NET inputs.
|
|
219
|
+
const inputDirectories = [...projects.map((project) => path.dirname(project)), ...invocationDirectories.values()];
|
|
220
|
+
for (const root of roots) {
|
|
221
|
+
if (inputDirectories.some((directory) => isWithinDirectory(root, directory))) {
|
|
222
|
+
addInvocationDirectory(root, root);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { projects, invocationDirectories: [...invocationDirectories.values()] };
|
|
226
|
+
}
|
|
227
|
+
function isWithinDirectory(root, directory) {
|
|
228
|
+
const relative = path.relative(root, directory);
|
|
229
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
230
|
+
}
|
|
231
|
+
function discoveryLimit(bound, maximum, root) {
|
|
232
|
+
return new Error(`dotnet_toolchain_discovery: ${bound} limit ${maximum} exceeded under ${root}; configure narrower explicit repository roots`);
|
|
233
|
+
}
|
|
234
|
+
function dotNetCandidates(environment, platform, homeDirectory, requiredMajor) {
|
|
235
|
+
const executable = platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
236
|
+
const explicit = environmentValue(environment, "FORGE_DOTNET_PATH");
|
|
237
|
+
if (explicit)
|
|
238
|
+
return [explicit];
|
|
239
|
+
const dotNetRoot = environmentValue(environment, "DOTNET_ROOT");
|
|
240
|
+
const pathValue = environmentValue(environment, "PATH") ?? "";
|
|
241
|
+
const programFiles = environmentValue(environment, "ProgramFiles");
|
|
242
|
+
const candidates = [
|
|
243
|
+
dotNetRoot ? path.join(dotNetRoot, executable) : null,
|
|
244
|
+
path.join(homeDirectory, ".dotnet", executable),
|
|
245
|
+
requiredMajor > 0 ? path.join(homeDirectory, `.dotnet${requiredMajor}`, executable) : null,
|
|
246
|
+
...(platform === "win32" && programFiles ? [path.join(programFiles, "dotnet", executable)] : []),
|
|
247
|
+
...pathValue.split(path.delimiter).filter(Boolean).map((directory) => path.join(directory, executable)),
|
|
248
|
+
].filter((candidate) => Boolean(candidate));
|
|
249
|
+
return [...new Map(candidates.map((candidate) => [canonicalPath(candidate, platform), path.resolve(candidate)])).values()];
|
|
250
|
+
}
|
|
251
|
+
async function probeSdks(candidate) {
|
|
252
|
+
const { stdout } = await execFileAsync(candidate, ["--list-sdks"], {
|
|
253
|
+
encoding: "utf8",
|
|
254
|
+
timeout: 10_000,
|
|
255
|
+
windowsHide: true,
|
|
256
|
+
});
|
|
257
|
+
return stdout;
|
|
258
|
+
}
|
|
259
|
+
async function probeSelectedSdk(candidate, repositoryRoot) {
|
|
260
|
+
const { stdout } = await execFileAsync(candidate, ["--version"], {
|
|
261
|
+
cwd: repositoryRoot,
|
|
262
|
+
encoding: "utf8",
|
|
263
|
+
timeout: 10_000,
|
|
264
|
+
windowsHide: true,
|
|
265
|
+
});
|
|
266
|
+
return stdout;
|
|
267
|
+
}
|
|
268
|
+
function sdkVersions(output) {
|
|
269
|
+
return output.split(/\r?\n/gu)
|
|
270
|
+
.map((line) => /^(\d+\.\d+\.\d+(?:[-+][^\s]+)?)/u.exec(line.trim())?.[1] ?? null)
|
|
271
|
+
.filter((version) => version !== null);
|
|
272
|
+
}
|
|
273
|
+
function majorVersion(version) {
|
|
274
|
+
const match = /^(\d+)\./u.exec(version.trim());
|
|
275
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : null;
|
|
276
|
+
}
|
|
277
|
+
async function isFile(candidate) {
|
|
278
|
+
try {
|
|
279
|
+
return (await fs.stat(candidate)).isFile();
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function prependPath(environment, directory, platform) {
|
|
286
|
+
const current = environmentValue(environment, "PATH") ?? "";
|
|
287
|
+
const normalizedDirectory = canonicalPath(directory, platform);
|
|
288
|
+
const remaining = current.split(path.delimiter)
|
|
289
|
+
.filter(Boolean)
|
|
290
|
+
.filter((entry) => canonicalPath(entry, platform) !== normalizedDirectory);
|
|
291
|
+
setEnvironmentValue(environment, "PATH", [directory, ...remaining].join(path.delimiter));
|
|
292
|
+
}
|
|
293
|
+
function environmentValue(environment, name) {
|
|
294
|
+
const key = Object.keys(environment).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
295
|
+
return key ? environment[key] : undefined;
|
|
296
|
+
}
|
|
297
|
+
function setEnvironmentValue(environment, name, value) {
|
|
298
|
+
const keys = Object.keys(environment).filter((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
299
|
+
const target = keys[0] ?? name;
|
|
300
|
+
for (const duplicate of keys.slice(1))
|
|
301
|
+
delete environment[duplicate];
|
|
302
|
+
environment[target] = value;
|
|
303
|
+
}
|
|
304
|
+
function canonicalPath(candidate, platform) {
|
|
305
|
+
const normalized = path.normalize(candidate);
|
|
306
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
307
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { captureHostProcessIdentity } from "./host-process-supervisor.js";
|
|
2
|
+
import { monotonicNowMs } from "../time/monotonic.js";
|
|
2
3
|
/** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
|
|
3
4
|
export class ExactHostProcessRecovery {
|
|
4
5
|
platform;
|
|
@@ -71,8 +72,8 @@ function signalExactProcessGroup(pid, signal) {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
async function waitForExit(pid, timeoutMs) {
|
|
74
|
-
const deadline =
|
|
75
|
-
while (
|
|
75
|
+
const deadline = monotonicNowMs() + timeoutMs;
|
|
76
|
+
while (monotonicNowMs() < deadline && processExists(pid)) {
|
|
76
77
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -13,7 +13,9 @@ export interface HostProcessSupervisedLaunch {
|
|
|
13
13
|
readonly supervisorId: string;
|
|
14
14
|
readonly strategy: HostProcessSupervisorStrategy;
|
|
15
15
|
readonly metadataPath: string;
|
|
16
|
+
readonly memoryTelemetryPath?: string;
|
|
16
17
|
readonly payloadPath?: string;
|
|
18
|
+
readonly payloadSha256?: string;
|
|
17
19
|
readonly readyPath?: string;
|
|
18
20
|
readonly startGrantPath?: string;
|
|
19
21
|
readonly commandDigest: string;
|
|
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { monotonicNowMs } from "../time/monotonic.js";
|
|
6
7
|
const windowsJobSupervisorPath = fileURLToPath(new URL("../../../runtime/windows-job-supervisor.ps1", import.meta.url));
|
|
7
8
|
export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, resolvedArgs, platform = process.platform) {
|
|
8
9
|
const env = request.env;
|
|
@@ -22,7 +23,8 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
|
|
|
22
23
|
const payloadPath = `${metadataPath}.launch.json`;
|
|
23
24
|
const readyPath = `${metadataPath}.provider-ready.json`;
|
|
24
25
|
const startGrantPath = `${metadataPath}.provider-start-granted`;
|
|
25
|
-
|
|
26
|
+
const memoryTelemetryPath = `${metadataPath}.memory.json`;
|
|
27
|
+
const serializedPayload = `${JSON.stringify({
|
|
26
28
|
command: resolvedCommand,
|
|
27
29
|
args: resolvedArgs,
|
|
28
30
|
cwd: request.cwd,
|
|
@@ -30,7 +32,10 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
|
|
|
30
32
|
supervisorId,
|
|
31
33
|
readyPath,
|
|
32
34
|
startGrantPath,
|
|
33
|
-
|
|
35
|
+
memoryTelemetryPath,
|
|
36
|
+
})}\n`;
|
|
37
|
+
const payloadSha256 = createHash("sha256").update(serializedPayload).digest("hex");
|
|
38
|
+
fs.writeFileSync(payloadPath, serializedPayload, {
|
|
34
39
|
encoding: "utf8",
|
|
35
40
|
flag: "wx",
|
|
36
41
|
mode: 0o600,
|
|
@@ -49,12 +54,16 @@ export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, res
|
|
|
49
54
|
"Outer",
|
|
50
55
|
"-PayloadPath",
|
|
51
56
|
payloadPath,
|
|
57
|
+
"-PayloadSha256",
|
|
58
|
+
payloadSha256,
|
|
52
59
|
],
|
|
53
60
|
detached: false,
|
|
54
61
|
supervisorId,
|
|
55
62
|
strategy: "windows_job_object",
|
|
56
63
|
metadataPath,
|
|
64
|
+
memoryTelemetryPath,
|
|
57
65
|
payloadPath,
|
|
66
|
+
payloadSha256,
|
|
58
67
|
readyPath,
|
|
59
68
|
startGrantPath,
|
|
60
69
|
commandDigest,
|
|
@@ -147,12 +156,7 @@ export function captureHostProcessIdentity(pid, platform = process.platform) {
|
|
|
147
156
|
}
|
|
148
157
|
export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
|
|
149
158
|
if (!launch) {
|
|
150
|
-
|
|
151
|
-
child.kill("SIGTERM");
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// The process already settled.
|
|
155
|
-
}
|
|
159
|
+
await terminateUnsupervisedProcessTree(child, graceMs);
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
158
162
|
if (launch.strategy === "windows_job_object") {
|
|
@@ -189,9 +193,48 @@ export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
|
|
|
189
193
|
const processGroupId = child.pid;
|
|
190
194
|
if (!processGroupId || !signalUnixProcessGroup(processGroupId, "SIGTERM"))
|
|
191
195
|
return;
|
|
192
|
-
await
|
|
196
|
+
await waitForUnixProcessGroupExit(processGroupId, graceMs);
|
|
193
197
|
if (unixProcessGroupExists(processGroupId)) {
|
|
194
198
|
signalUnixProcessGroup(processGroupId, "SIGKILL");
|
|
199
|
+
await waitForUnixProcessGroupExit(processGroupId, graceMs);
|
|
200
|
+
}
|
|
201
|
+
if (unixProcessGroupExists(processGroupId)) {
|
|
202
|
+
throw new Error("host_process_supervisor_termination_failed: supervised Unix process group did not stop.");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function terminateUnsupervisedProcessTree(child, graceMs) {
|
|
206
|
+
const rootPid = child.pid ?? 0;
|
|
207
|
+
if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
|
|
208
|
+
try {
|
|
209
|
+
child.kill("SIGTERM");
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// The process already settled before a PID was assigned.
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (process.platform === "win32") {
|
|
217
|
+
// Without a Job Object there is no trustworthy ancestry record after the
|
|
218
|
+
// root has already exited. Never target a potentially reused Windows PID.
|
|
219
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
220
|
+
return;
|
|
221
|
+
const rootIdentity = captureHostProcessIdentity(rootPid, "win32");
|
|
222
|
+
forceWindowsProcessTree(rootPid);
|
|
223
|
+
await waitForExactProcessExit(rootPid, rootIdentity, graceMs, "win32");
|
|
224
|
+
if (exactProcessExists(rootPid, rootIdentity, "win32")) {
|
|
225
|
+
throw new Error("host_process_supervisor_termination_failed: unsupervised Windows provider tree did not stop.");
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!signalUnixProcessGroup(rootPid, "SIGTERM"))
|
|
230
|
+
return;
|
|
231
|
+
await waitForUnixProcessGroupExit(rootPid, graceMs);
|
|
232
|
+
if (unixProcessGroupExists(rootPid)) {
|
|
233
|
+
signalUnixProcessGroup(rootPid, "SIGKILL");
|
|
234
|
+
await waitForUnixProcessGroupExit(rootPid, graceMs);
|
|
235
|
+
}
|
|
236
|
+
if (unixProcessGroupExists(rootPid)) {
|
|
237
|
+
throw new Error("host_process_supervisor_termination_failed: unsupervised Unix provider tree did not stop.");
|
|
195
238
|
}
|
|
196
239
|
}
|
|
197
240
|
function requiredEnvironment(env, name) {
|
|
@@ -234,6 +277,12 @@ function unixProcessGroupExists(processGroupId) {
|
|
|
234
277
|
throw error;
|
|
235
278
|
}
|
|
236
279
|
}
|
|
280
|
+
async function waitForUnixProcessGroupExit(processGroupId, timeoutMs) {
|
|
281
|
+
const deadline = monotonicNowMs() + timeoutMs;
|
|
282
|
+
while (monotonicNowMs() < deadline && unixProcessGroupExists(processGroupId)) {
|
|
283
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
237
286
|
function readSupervisorMetadata(metadataPath) {
|
|
238
287
|
try {
|
|
239
288
|
return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
@@ -259,8 +308,8 @@ function processExists(pid) {
|
|
|
259
308
|
}
|
|
260
309
|
}
|
|
261
310
|
async function waitForExactProcessExit(pid, identity, timeoutMs, platform) {
|
|
262
|
-
const deadline =
|
|
263
|
-
while (
|
|
311
|
+
const deadline = monotonicNowMs() + timeoutMs;
|
|
312
|
+
while (monotonicNowMs() < deadline && exactProcessExists(pid, identity, platform)) {
|
|
264
313
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
265
314
|
}
|
|
266
315
|
}
|
|
@@ -3,6 +3,8 @@ export interface ProcessRunRequest {
|
|
|
3
3
|
readonly args: readonly string[];
|
|
4
4
|
readonly cwd: string;
|
|
5
5
|
readonly env?: NodeJS.ProcessEnv;
|
|
6
|
+
readonly rawOutput?: boolean;
|
|
7
|
+
readonly windowsVerbatimArguments?: boolean;
|
|
6
8
|
}
|
|
7
9
|
export interface ProcessRunResult {
|
|
8
10
|
readonly exitCode: number;
|
|
@@ -6,22 +6,27 @@ export class NodeProcessRunner {
|
|
|
6
6
|
const child = spawn(request.command, request.args, {
|
|
7
7
|
cwd: request.cwd,
|
|
8
8
|
env: request.env,
|
|
9
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
windowsVerbatimArguments: request.windowsVerbatimArguments,
|
|
10
11
|
});
|
|
11
12
|
let stdout = "";
|
|
12
13
|
let stderr = "";
|
|
13
14
|
child.on("error", reject);
|
|
14
|
-
const stdoutReader = readline.createInterface({ input: child.stdout });
|
|
15
|
-
const stderrReader = readline.createInterface({ input: child.stderr });
|
|
16
|
-
|
|
15
|
+
const stdoutReader = request.rawOutput ? undefined : readline.createInterface({ input: child.stdout });
|
|
16
|
+
const stderrReader = request.rawOutput ? undefined : readline.createInterface({ input: child.stderr });
|
|
17
|
+
if (request.rawOutput) {
|
|
18
|
+
child.stdout.setEncoding("utf8").on("data", chunk => { stdout += chunk; });
|
|
19
|
+
child.stderr.setEncoding("utf8").on("data", chunk => { stderr += chunk; });
|
|
20
|
+
}
|
|
21
|
+
stdoutReader?.on("line", (line) => {
|
|
17
22
|
stdout += `${line}\n`;
|
|
18
23
|
});
|
|
19
|
-
stderrReader
|
|
24
|
+
stderrReader?.on("line", (line) => {
|
|
20
25
|
stderr += `${line}\n`;
|
|
21
26
|
});
|
|
22
27
|
child.on("close", (code) => {
|
|
23
|
-
stdoutReader
|
|
24
|
-
stderrReader
|
|
28
|
+
stdoutReader?.close();
|
|
29
|
+
stderrReader?.close();
|
|
25
30
|
resolve({
|
|
26
31
|
exitCode: code ?? 1,
|
|
27
32
|
stdout,
|