@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.bfa11ebe98c6
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 +1562 -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-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 +11 -0
- package/dist/runtime/attempt-process-supervisor.js +67 -20
- 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 +191 -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 +107 -53
- package/dist/runtime/host-runtime-lease.d.ts +90 -8
- package/dist/runtime/host-runtime-lease.js +1097 -108
- 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 +28 -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 +17 -8
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +132 -103
- 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 +481 -31
- 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 +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 +118 -16
- 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 +657 -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 +8 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +771 -68
- 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-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +57 -9
- 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/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,341 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ForgeFailureError } from "../session/session.types.js";
|
|
6
|
+
const inFlightPrebuilds = new Map();
|
|
7
|
+
export function createAspNetRuntimePrebuildPlan(options) {
|
|
8
|
+
if (!isDotnetRun(options.command, options.args))
|
|
9
|
+
return null;
|
|
10
|
+
const projectArgument = readOptionValue(options.args, "--project");
|
|
11
|
+
const projectPath = path.resolve(options.cwd, projectArgument ?? ".");
|
|
12
|
+
const normalizedWorktree = path.resolve(options.worktreePath);
|
|
13
|
+
const relativeProject = path.relative(normalizedWorktree, projectPath);
|
|
14
|
+
if (relativeProject.startsWith("..") || path.isAbsolute(relativeProject)) {
|
|
15
|
+
throw new Error("host_runtime_build_invalid: the ASP.NET project must remain inside the managed worktree.");
|
|
16
|
+
}
|
|
17
|
+
const projectDirectory = projectArgument && path.extname(projectPath).toLowerCase() === ".csproj"
|
|
18
|
+
? path.dirname(projectPath)
|
|
19
|
+
: projectPath;
|
|
20
|
+
const buildArgs = [
|
|
21
|
+
"build",
|
|
22
|
+
...(projectArgument ? [projectArgument] : []),
|
|
23
|
+
"--nologo",
|
|
24
|
+
"--no-restore",
|
|
25
|
+
"--disable-build-servers",
|
|
26
|
+
"-p:UseSharedCompilation=false",
|
|
27
|
+
"-p:DefaultItemExcludesInProjectFolder=TestResults/**",
|
|
28
|
+
...forwardBuildOptions(options.args),
|
|
29
|
+
];
|
|
30
|
+
const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
|
|
31
|
+
inputCommitSha: options.inputCommitSha,
|
|
32
|
+
runtimeContractFingerprint: options.runtimeContractFingerprint,
|
|
33
|
+
project: relativeProject.replaceAll("\\", "/"),
|
|
34
|
+
args: buildArgs,
|
|
35
|
+
environment: buildEnvironmentContract(options.environment ?? {}),
|
|
36
|
+
})).digest("hex")}`;
|
|
37
|
+
return {
|
|
38
|
+
fingerprint,
|
|
39
|
+
receiptPath: path.join(projectDirectory, "obj", ".forge-runtime-build.json"),
|
|
40
|
+
command: options.command,
|
|
41
|
+
args: buildArgs,
|
|
42
|
+
cwd: options.cwd,
|
|
43
|
+
environment: options.environment ?? {},
|
|
44
|
+
projectPath,
|
|
45
|
+
worktreePath: normalizedWorktree,
|
|
46
|
+
inputCommitSha: options.inputCommitSha,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function ensureAspNetRuntimePrebuilt(plan, execute, signal) {
|
|
50
|
+
const pending = inFlightPrebuilds.get(plan.receiptPath);
|
|
51
|
+
if (pending)
|
|
52
|
+
return await pending;
|
|
53
|
+
const started = ensureAspNetRuntimePrebuiltCore(plan, execute, signal);
|
|
54
|
+
inFlightPrebuilds.set(plan.receiptPath, started);
|
|
55
|
+
try {
|
|
56
|
+
return await started;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (inFlightPrebuilds.get(plan.receiptPath) === started)
|
|
60
|
+
inFlightPrebuilds.delete(plan.receiptPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function ensureAspNetRuntimePrebuiltCore(plan, execute, signal) {
|
|
64
|
+
const inputs = await aspNetBuildInputs(plan);
|
|
65
|
+
const fingerprint = `sha256:${createHash("sha256").update(JSON.stringify({
|
|
66
|
+
planFingerprint: plan.fingerprint,
|
|
67
|
+
...inputs,
|
|
68
|
+
})).digest("hex")}`;
|
|
69
|
+
const receipt = await readReceipt(plan.receiptPath);
|
|
70
|
+
if (receipt?.fingerprint === fingerprint
|
|
71
|
+
&& receipt.planFingerprint === plan.fingerprint
|
|
72
|
+
&& receipt.inputCommitSha === plan.inputCommitSha
|
|
73
|
+
&& await aspNetOutputsMatch(plan, receipt.outputs)) {
|
|
74
|
+
return { cacheHit: true, receiptPath: plan.receiptPath, fingerprint };
|
|
75
|
+
}
|
|
76
|
+
const result = await execute(plan, signal);
|
|
77
|
+
if (result.exitCode !== 0) {
|
|
78
|
+
const evidence = [
|
|
79
|
+
diagnostic("stderr", result.stderrTail),
|
|
80
|
+
diagnostic("stdout", result.stdoutTail),
|
|
81
|
+
`stdout log: ${result.stdoutLogPath}`,
|
|
82
|
+
`stderr log: ${result.stderrLogPath}`,
|
|
83
|
+
].filter(Boolean).join("; ");
|
|
84
|
+
throw new ForgeFailureError("host_runtime_project_start_failed", `host_runtime_project_start_failed: dotnet build exited with code ${result.exitCode}.${evidence ? ` ${evidence}` : ""}`, "bridge");
|
|
85
|
+
}
|
|
86
|
+
await fs.mkdir(path.dirname(plan.receiptPath), { recursive: true });
|
|
87
|
+
const outputs = await collectAspNetBuildOutputs(plan);
|
|
88
|
+
if (outputs.length === 0) {
|
|
89
|
+
throw new Error("host_runtime_build_output_missing: dotnet build succeeded without producing a verifiable project output.");
|
|
90
|
+
}
|
|
91
|
+
const temporaryReceipt = `${plan.receiptPath}.${randomUUID()}.tmp`;
|
|
92
|
+
const nextReceipt = {
|
|
93
|
+
schemaVersion: 2,
|
|
94
|
+
fingerprint,
|
|
95
|
+
planFingerprint: plan.fingerprint,
|
|
96
|
+
inputCommitSha: plan.inputCommitSha,
|
|
97
|
+
projectPath: plan.projectPath,
|
|
98
|
+
...inputs,
|
|
99
|
+
outputs,
|
|
100
|
+
completedAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
await fs.writeFile(temporaryReceipt, `${JSON.stringify(nextReceipt)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
103
|
+
try {
|
|
104
|
+
await fs.rename(temporaryReceipt, plan.receiptPath);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await fs.rm(temporaryReceipt, { force: true }).catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
return { cacheHit: false, receiptPath: plan.receiptPath, fingerprint };
|
|
110
|
+
}
|
|
111
|
+
export function withAspNetPrebuiltRunArguments(command, args) {
|
|
112
|
+
if (!isDotnetRun(command, args)) {
|
|
113
|
+
throw new Error("node_runtime_adapter_invalid: aspnetcore adapter requires a dotnet run command.");
|
|
114
|
+
}
|
|
115
|
+
const applicationArguments = args.indexOf("--");
|
|
116
|
+
const insertionIndex = applicationArguments < 0 ? args.length : applicationArguments;
|
|
117
|
+
const beforeApplication = args.slice(0, insertionIndex);
|
|
118
|
+
return [
|
|
119
|
+
...beforeApplication,
|
|
120
|
+
...(!beforeApplication.includes("--no-build") ? ["--no-build"] : []),
|
|
121
|
+
...(!beforeApplication.includes("--no-restore") ? ["--no-restore"] : []),
|
|
122
|
+
...args.slice(insertionIndex),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
export async function resolveWorktreeCommitSha(worktreePath, supplied) {
|
|
126
|
+
if (supplied && /^[0-9a-f]{40,64}$/iu.test(supplied))
|
|
127
|
+
return supplied.toLowerCase();
|
|
128
|
+
return await new Promise((resolve, reject) => {
|
|
129
|
+
const child = spawn("git", ["rev-parse", "--verify", "HEAD^{commit}"], {
|
|
130
|
+
cwd: worktreePath,
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
133
|
+
});
|
|
134
|
+
let stdout = "";
|
|
135
|
+
let stderr = "";
|
|
136
|
+
child.stdout.setEncoding("utf8");
|
|
137
|
+
child.stderr.setEncoding("utf8");
|
|
138
|
+
child.stdout.on("data", chunk => { stdout = `${stdout}${String(chunk)}`.slice(-256); });
|
|
139
|
+
child.stderr.on("data", chunk => { stderr = `${stderr}${String(chunk)}`.slice(-1_000); });
|
|
140
|
+
child.once("error", reject);
|
|
141
|
+
child.once("close", code => {
|
|
142
|
+
const commit = stdout.trim();
|
|
143
|
+
if (code === 0 && /^[0-9a-f]{40,64}$/iu.test(commit))
|
|
144
|
+
resolve(commit.toLowerCase());
|
|
145
|
+
else
|
|
146
|
+
reject(new Error(`host_runtime_build_commit_unavailable: git rev-parse failed (${code ?? "unknown"}): ${stderr.trim()}`));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function isDotnetRun(command, args) {
|
|
151
|
+
const executable = path.basename(command).toLowerCase();
|
|
152
|
+
return (executable === "dotnet" || executable === "dotnet.exe") && args[0]?.toLowerCase() === "run";
|
|
153
|
+
}
|
|
154
|
+
function readOptionValue(args, option) {
|
|
155
|
+
const index = args.findIndex(argument => argument === option || argument.startsWith(`${option}=`));
|
|
156
|
+
if (index < 0)
|
|
157
|
+
return null;
|
|
158
|
+
const argument = args[index];
|
|
159
|
+
return argument.startsWith(`${option}=`) ? argument.slice(option.length + 1) : args[index + 1] ?? null;
|
|
160
|
+
}
|
|
161
|
+
function forwardBuildOptions(args) {
|
|
162
|
+
const forwarded = [];
|
|
163
|
+
const optionsWithValues = new Set(["-c", "--configuration", "-f", "--framework", "-r", "--runtime", "--arch", "--os"]);
|
|
164
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
165
|
+
const argument = args[index];
|
|
166
|
+
if (argument === "--")
|
|
167
|
+
break;
|
|
168
|
+
if (argument === "--project") {
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (argument.startsWith("--project="))
|
|
173
|
+
continue;
|
|
174
|
+
if (optionsWithValues.has(argument)) {
|
|
175
|
+
const value = args[index + 1];
|
|
176
|
+
if (value !== undefined) {
|
|
177
|
+
forwarded.push(argument, value);
|
|
178
|
+
index += 1;
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if ([...optionsWithValues].some(option => argument.startsWith(`${option}=`))
|
|
183
|
+
|| argument.startsWith("-p:")
|
|
184
|
+
|| argument.startsWith("/p:")) {
|
|
185
|
+
forwarded.push(argument);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return forwarded;
|
|
189
|
+
}
|
|
190
|
+
async function readReceipt(receiptPath) {
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(await fs.readFile(receiptPath, "utf8"));
|
|
193
|
+
return parsed.schemaVersion === 2
|
|
194
|
+
&& typeof parsed.fingerprint === "string"
|
|
195
|
+
&& typeof parsed.planFingerprint === "string"
|
|
196
|
+
&& typeof parsed.inputCommitSha === "string"
|
|
197
|
+
&& typeof parsed.sourceDigest === "string"
|
|
198
|
+
&& typeof parsed.dependencyDigest === "string"
|
|
199
|
+
&& typeof parsed.sdkVersion === "string"
|
|
200
|
+
&& Array.isArray(parsed.outputs)
|
|
201
|
+
? parsed
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError)
|
|
206
|
+
return null;
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function aspNetBuildInputs(plan) {
|
|
211
|
+
const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
|
|
212
|
+
? path.dirname(plan.projectPath)
|
|
213
|
+
: plan.projectPath;
|
|
214
|
+
const sourceDigest = await directoryContentDigest(projectDirectory);
|
|
215
|
+
const dependencyReceipt = path.join(projectDirectory, "obj", ".forge-dependency-preparation.json");
|
|
216
|
+
const dependencyContent = await fs.readFile(dependencyReceipt).catch(() => null);
|
|
217
|
+
const globalJson = await findAncestorFile(projectDirectory, plan.worktreePath, "global.json");
|
|
218
|
+
const globalJsonContent = globalJson ? await fs.readFile(globalJson) : null;
|
|
219
|
+
return {
|
|
220
|
+
sourceDigest,
|
|
221
|
+
dependencyDigest: `sha256:${createHash("sha256")
|
|
222
|
+
.update(dependencyContent ?? Buffer.alloc(0))
|
|
223
|
+
.update(globalJsonContent ?? Buffer.alloc(0))
|
|
224
|
+
.digest("hex")}`,
|
|
225
|
+
sdkVersion: await dotnetSdkVersion(plan.cwd),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function directoryContentDigest(directory) {
|
|
229
|
+
const files = [];
|
|
230
|
+
await collectInputFiles(directory, directory, files);
|
|
231
|
+
const hash = createHash("sha256");
|
|
232
|
+
for (const file of files.sort((left, right) => left.localeCompare(right))) {
|
|
233
|
+
hash.update(path.relative(directory, file).replaceAll("\\", "/"));
|
|
234
|
+
hash.update("\0");
|
|
235
|
+
hash.update(await fs.readFile(file));
|
|
236
|
+
hash.update("\0");
|
|
237
|
+
}
|
|
238
|
+
return `sha256:${hash.digest("hex")}`;
|
|
239
|
+
}
|
|
240
|
+
async function collectInputFiles(root, directory, files) {
|
|
241
|
+
const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
const absolute = path.join(directory, entry.name);
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
if (!["bin", "obj", "node_modules", ".git", "TestResults"].includes(entry.name)) {
|
|
246
|
+
await collectInputFiles(root, absolute, files);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (entry.isFile()) {
|
|
250
|
+
files.push(absolute);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function collectAspNetBuildOutputs(plan) {
|
|
255
|
+
const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
|
|
256
|
+
? path.dirname(plan.projectPath)
|
|
257
|
+
: plan.projectPath;
|
|
258
|
+
const outputRoot = path.join(projectDirectory, "bin");
|
|
259
|
+
const outputs = [];
|
|
260
|
+
await collectOutputFiles(outputRoot, projectDirectory, outputs);
|
|
261
|
+
return outputs.sort((left, right) => left.path.localeCompare(right.path));
|
|
262
|
+
}
|
|
263
|
+
async function collectOutputFiles(directory, projectDirectory, outputs) {
|
|
264
|
+
const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
const absolute = path.join(directory, entry.name);
|
|
267
|
+
if (entry.isDirectory())
|
|
268
|
+
await collectOutputFiles(absolute, projectDirectory, outputs);
|
|
269
|
+
else if (entry.isFile()) {
|
|
270
|
+
const stat = await fs.stat(absolute);
|
|
271
|
+
outputs.push({
|
|
272
|
+
path: path.relative(projectDirectory, absolute).replaceAll("\\", "/"),
|
|
273
|
+
size: stat.size,
|
|
274
|
+
digest: createHash("sha256").update(await fs.readFile(absolute)).digest("hex"),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function aspNetOutputsMatch(plan, outputs) {
|
|
280
|
+
if (outputs.length === 0)
|
|
281
|
+
return false;
|
|
282
|
+
const projectDirectory = path.extname(plan.projectPath).toLowerCase() === ".csproj"
|
|
283
|
+
? path.dirname(plan.projectPath)
|
|
284
|
+
: plan.projectPath;
|
|
285
|
+
for (const output of outputs) {
|
|
286
|
+
const absolute = path.resolve(projectDirectory, output.path);
|
|
287
|
+
const relative = path.relative(projectDirectory, absolute);
|
|
288
|
+
if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
289
|
+
return false;
|
|
290
|
+
const stat = await fs.stat(absolute).catch(() => null);
|
|
291
|
+
if (!stat?.isFile() || stat.size !== output.size)
|
|
292
|
+
return false;
|
|
293
|
+
const digest = createHash("sha256").update(await fs.readFile(absolute)).digest("hex");
|
|
294
|
+
if (digest !== output.digest)
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
async function findAncestorFile(directory, root, name) {
|
|
300
|
+
let current = path.resolve(directory);
|
|
301
|
+
const resolvedRoot = path.resolve(root);
|
|
302
|
+
while (true) {
|
|
303
|
+
const candidate = path.join(current, name);
|
|
304
|
+
if (await fs.stat(candidate).then(stat => stat.isFile()).catch(() => false))
|
|
305
|
+
return candidate;
|
|
306
|
+
if (current === resolvedRoot)
|
|
307
|
+
return null;
|
|
308
|
+
const parent = path.dirname(current);
|
|
309
|
+
if (parent === current || path.relative(resolvedRoot, parent).startsWith(".."))
|
|
310
|
+
return null;
|
|
311
|
+
current = parent;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function dotnetSdkVersion(cwd) {
|
|
315
|
+
return await new Promise(resolve => {
|
|
316
|
+
const child = spawn("dotnet", ["--version"], { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
317
|
+
let output = "";
|
|
318
|
+
child.stdout.setEncoding("utf8");
|
|
319
|
+
child.stderr.setEncoding("utf8");
|
|
320
|
+
child.stdout.on("data", chunk => { output += String(chunk); });
|
|
321
|
+
child.stderr.on("data", chunk => { output += String(chunk); });
|
|
322
|
+
child.once("error", () => resolve("unavailable"));
|
|
323
|
+
child.once("close", code => resolve(code === 0 ? output.trim().slice(0, 256) : `unavailable:${code ?? "unknown"}`));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function buildEnvironmentContract(environment) {
|
|
327
|
+
return {
|
|
328
|
+
DOTNET_ROOT: environment.DOTNET_ROOT ?? process.env.DOTNET_ROOT ?? null,
|
|
329
|
+
DOTNET_ROLL_FORWARD: environment.DOTNET_ROLL_FORWARD ?? process.env.DOTNET_ROLL_FORWARD ?? null,
|
|
330
|
+
DOTNET_MULTILEVEL_LOOKUP: environment.DOTNET_MULTILEVEL_LOOKUP ?? process.env.DOTNET_MULTILEVEL_LOOKUP ?? null,
|
|
331
|
+
MSBuildSDKsPath: environment.MSBuildSDKsPath ?? process.env.MSBuildSDKsPath ?? null,
|
|
332
|
+
NUGET_PACKAGES: environment.NUGET_PACKAGES ?? process.env.NUGET_PACKAGES ?? null,
|
|
333
|
+
FORGE_BUILD_CACHE_EPOCH: createHash("sha256")
|
|
334
|
+
.update(environment.FORGE_BUILD_CACHE_EPOCH ?? process.env.FORGE_BUILD_CACHE_EPOCH ?? "default")
|
|
335
|
+
.digest("hex"),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function diagnostic(label, value) {
|
|
339
|
+
const normalized = value.replaceAll(/[\r\n\t]+/gu, " ").trim();
|
|
340
|
+
return normalized ? `${label} tail: ${normalized.slice(-2_000)}` : "";
|
|
341
|
+
}
|
|
@@ -8,8 +8,9 @@ export declare class AttemptNodeRuntimeLauncher implements NodeRuntimeCommandLau
|
|
|
8
8
|
private readonly owner;
|
|
9
9
|
private readonly logDirectory;
|
|
10
10
|
private readonly listenerInspector;
|
|
11
|
+
private readonly exactSecrets;
|
|
11
12
|
private readonly diagnosticsByChild;
|
|
12
|
-
constructor(supervisor: AttemptProcessSupervisor, owner: HostPortLeaseOwner, logDirectory: string, listenerInspector?: HostListenerInspector);
|
|
13
|
+
constructor(supervisor: AttemptProcessSupervisor, owner: HostPortLeaseOwner, logDirectory: string, listenerInspector?: HostListenerInspector, exactSecrets?: readonly string[]);
|
|
13
14
|
launch(request: NodeRuntimeLaunchRequest, signal?: AbortSignal): Promise<AttemptProcessChildHandle>;
|
|
14
15
|
inspectListener(child: AttemptProcessChildHandle, request: NodeRuntimeLaunchRequest): Promise<HostListenerOwnership>;
|
|
15
16
|
stop(child: AttemptProcessChildHandle): Promise<void>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { redactText } from "../shared/redaction/redaction.js";
|
|
5
|
+
import { ForgeFailureError, sanitizeFailureError } from "../session/session.types.js";
|
|
4
6
|
import { HostListenerInspector } from "./host-listener-inspector.js";
|
|
5
7
|
const diagnosticTailLength = 4_000;
|
|
6
8
|
/** Launches one explicitly configured HTTP project beneath the attempt coordinator. */
|
|
@@ -9,18 +11,20 @@ export class AttemptNodeRuntimeLauncher {
|
|
|
9
11
|
owner;
|
|
10
12
|
logDirectory;
|
|
11
13
|
listenerInspector;
|
|
14
|
+
exactSecrets;
|
|
12
15
|
diagnosticsByChild = new WeakMap();
|
|
13
|
-
constructor(supervisor, owner, logDirectory, listenerInspector = new HostListenerInspector()) {
|
|
16
|
+
constructor(supervisor, owner, logDirectory, listenerInspector = new HostListenerInspector(), exactSecrets = []) {
|
|
14
17
|
this.supervisor = supervisor;
|
|
15
18
|
this.owner = owner;
|
|
16
19
|
this.logDirectory = logDirectory;
|
|
17
20
|
this.listenerInspector = listenerInspector;
|
|
21
|
+
this.exactSecrets = exactSecrets;
|
|
18
22
|
}
|
|
19
23
|
async launch(request, signal) {
|
|
20
24
|
const childName = projectChildName(request.port.name);
|
|
21
25
|
const stdoutPath = path.join(this.logDirectory, `${childName}.stdout.log`);
|
|
22
26
|
const stderrPath = path.join(this.logDirectory, `${childName}.stderr.log`);
|
|
23
|
-
const diagnostics = { exited: false, stdoutTail: "", stderrTail: "" };
|
|
27
|
+
const diagnostics = { exited: false, stdoutTail: "", stderrTail: "", outputRevision: 0 };
|
|
24
28
|
let child;
|
|
25
29
|
try {
|
|
26
30
|
child = await this.supervisor.startChildReady(childName, {
|
|
@@ -29,31 +33,39 @@ export class AttemptNodeRuntimeLauncher {
|
|
|
29
33
|
cwd: request.cwd,
|
|
30
34
|
env: request.environment,
|
|
31
35
|
captureOutput: false,
|
|
36
|
+
failureRedactionValues: this.exactSecrets,
|
|
32
37
|
}, {
|
|
33
38
|
onStdoutLine: async (line) => {
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
const redacted = redactText(line, this.exactSecrets);
|
|
40
|
+
diagnostics.stdoutTail = appendTail(diagnostics.stdoutTail, redacted);
|
|
41
|
+
diagnostics.outputRevision += 1;
|
|
42
|
+
await appendLine(stdoutPath, redacted);
|
|
36
43
|
},
|
|
37
44
|
onStderrLine: async (line) => {
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
const redacted = redactText(line, this.exactSecrets);
|
|
46
|
+
diagnostics.stderrTail = appendTail(diagnostics.stderrTail, redacted);
|
|
47
|
+
diagnostics.outputRevision += 1;
|
|
48
|
+
await appendLine(stderrPath, redacted);
|
|
40
49
|
},
|
|
41
50
|
}, signal, { maxStartAttempts: 3, retryDelayMs: 250 });
|
|
42
51
|
}
|
|
43
52
|
catch (error) {
|
|
44
|
-
throw enrichLaunchFailure(error, diagnostics);
|
|
53
|
+
throw enrichLaunchFailure(error, diagnostics, this.exactSecrets);
|
|
45
54
|
}
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
const exposedChild = sanitizedChildHandle(child, this.exactSecrets);
|
|
56
|
+
this.diagnosticsByChild.set(exposedChild, diagnostics);
|
|
57
|
+
void exposedChild.result.then(result => {
|
|
48
58
|
diagnostics.exited = true;
|
|
49
59
|
diagnostics.exitCode = result.exitCode;
|
|
50
60
|
diagnostics.stdoutTail = appendTail(diagnostics.stdoutTail, result.stdout);
|
|
51
61
|
diagnostics.stderrTail = appendTail(diagnostics.stderrTail, result.stderr);
|
|
62
|
+
diagnostics.outputRevision += 1;
|
|
52
63
|
}, error => {
|
|
53
64
|
diagnostics.exited = true;
|
|
54
|
-
diagnostics.stderrTail = appendTail(diagnostics.stderrTail, error instanceof Error ? error.message : String(error));
|
|
65
|
+
diagnostics.stderrTail = appendTail(diagnostics.stderrTail, redactText(error instanceof Error ? error.message : String(error), this.exactSecrets));
|
|
66
|
+
diagnostics.outputRevision += 1;
|
|
55
67
|
});
|
|
56
|
-
return
|
|
68
|
+
return exposedChild;
|
|
57
69
|
}
|
|
58
70
|
async inspectListener(child, request) {
|
|
59
71
|
return await this.listenerInspector.inspect(child, request.port, this.owner);
|
|
@@ -84,17 +96,44 @@ function appendTail(current, value) {
|
|
|
84
96
|
const combined = current ? `${current}\n${value}` : value;
|
|
85
97
|
return combined.slice(-diagnosticTailLength);
|
|
86
98
|
}
|
|
87
|
-
function enrichLaunchFailure(error, diagnostics) {
|
|
88
|
-
const
|
|
99
|
+
function enrichLaunchFailure(error, diagnostics, exactSecrets) {
|
|
100
|
+
const sanitized = sanitizeFailureError(error, exactSecrets);
|
|
89
101
|
const evidence = [
|
|
90
102
|
diagnostics.stderrTail ? `stderr tail: ${boundedDiagnostic(diagnostics.stderrTail)}` : "",
|
|
91
103
|
diagnostics.stdoutTail ? `stdout tail: ${boundedDiagnostic(diagnostics.stdoutTail)}` : "",
|
|
92
104
|
].filter(Boolean);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
if (evidence.length === 0)
|
|
106
|
+
return sanitized;
|
|
107
|
+
return replaceSanitizedErrorMessage(sanitized, `${sanitized.message} ${evidence.join("; ")}`);
|
|
108
|
+
}
|
|
109
|
+
function sanitizedChildHandle(child, exactSecrets) {
|
|
110
|
+
const sanitizeResult = (result) => ({
|
|
111
|
+
...result,
|
|
112
|
+
stdout: redactText(result.stdout, exactSecrets),
|
|
113
|
+
stderr: redactText(result.stderr, exactSecrets),
|
|
114
|
+
});
|
|
115
|
+
const ready = child.ready.catch(error => { throw sanitizeFailureError(error, exactSecrets); });
|
|
116
|
+
const result = child.result.then(sanitizeResult, error => { throw sanitizeFailureError(error, exactSecrets); });
|
|
117
|
+
const stop = child.stop.bind(child);
|
|
118
|
+
Object.defineProperties(child, {
|
|
119
|
+
ready: { configurable: true, enumerable: true, value: ready },
|
|
120
|
+
result: { configurable: true, enumerable: true, value: result },
|
|
121
|
+
stop: { configurable: true, enumerable: true, value: async (fencingToken) => {
|
|
122
|
+
try {
|
|
123
|
+
return sanitizeResult(await stop(fencingToken));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw sanitizeFailureError(error, exactSecrets);
|
|
127
|
+
}
|
|
128
|
+
} },
|
|
129
|
+
});
|
|
130
|
+
return child;
|
|
131
|
+
}
|
|
132
|
+
function replaceSanitizedErrorMessage(error, message) {
|
|
133
|
+
const options = error.cause === undefined ? undefined : { cause: error.cause };
|
|
134
|
+
return error instanceof ForgeFailureError
|
|
135
|
+
? new ForgeFailureError(error.failureCode, message, error.failureSource, options)
|
|
136
|
+
: new Error(message, options);
|
|
98
137
|
}
|
|
99
138
|
function boundedDiagnostic(value) {
|
|
100
139
|
return value.replaceAll(/[\r\n\t]+/gu, " ").trim().slice(-1_000);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
/** A separate process pins cwd without changing the multithreaded Bridge's cwd.
|
|
3
|
+
* All writes use single relative components, never a re-resolved ancestor path.
|
|
4
|
+
*/
|
|
5
|
+
export declare function runAttemptPathOperation(parent: string, identity: BigIntStats, operation: {
|
|
6
|
+
kind: "mkdir";
|
|
7
|
+
segments: string[];
|
|
8
|
+
} | {
|
|
9
|
+
kind: "private-directory-mode";
|
|
10
|
+
segments: string[];
|
|
11
|
+
} | {
|
|
12
|
+
kind: "overlay";
|
|
13
|
+
name: string;
|
|
14
|
+
content: string;
|
|
15
|
+
}, timeoutMs?: number, terminationConfirmationMs?: number): Promise<void>;
|
|
16
|
+
/** Archive copies may be large or numerous: never block Bridge heartbeats while a pinned worker writes. */
|
|
17
|
+
export declare function runAttemptArchiveOperation(parent: string, identity: BigIntStats, operation: {
|
|
18
|
+
name: string;
|
|
19
|
+
bytes: Buffer;
|
|
20
|
+
sha256: string;
|
|
21
|
+
}, timeoutMs?: number, terminationConfirmationMs?: number): Promise<void>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { runPinnedDirectoryProcess } from "./pinned-directory-process.js";
|
|
2
|
+
/** A separate process pins cwd without changing the multithreaded Bridge's cwd.
|
|
3
|
+
* All writes use single relative components, never a re-resolved ancestor path.
|
|
4
|
+
*/
|
|
5
|
+
export async function runAttemptPathOperation(parent, identity, operation, timeoutMs = 30_000, terminationConfirmationMs = 2_000) {
|
|
6
|
+
try {
|
|
7
|
+
await runPinnedDirectoryProcess(parent, identity, worker, operation, undefined, 16 * 1024, timeoutMs, terminationConfirmationMs);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
throw new Error(`playwright_runtime_adapter_invalid: pinned directory operation failed: ${String(error)}`, { cause: error });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Archive copies may be large or numerous: never block Bridge heartbeats while a pinned worker writes. */
|
|
14
|
+
export async function runAttemptArchiveOperation(parent, identity, operation, timeoutMs = 30_000, terminationConfirmationMs = 2_000) {
|
|
15
|
+
try {
|
|
16
|
+
await runPinnedDirectoryProcess(parent, identity, worker, { kind: "archive", name: operation.name, sha256: operation.sha256 }, operation.bytes, 16 * 1024, timeoutMs, terminationConfirmationMs);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`host_diagnostics_copy_failed: pinned directory operation failed: ${String(error)}`, { cause: error });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const worker = String.raw `
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const input = fs.readFileSync(0);
|
|
26
|
+
const boundary = input.indexOf(10);
|
|
27
|
+
const job = JSON.parse(input.subarray(0, boundary).toString('utf8'));
|
|
28
|
+
const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
29
|
+
const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
30
|
+
const component = value => {
|
|
31
|
+
if (typeof value !== 'string' || !value || value === '.' || value === '..'
|
|
32
|
+
|| ['/', ':', String.fromCharCode(92), String.fromCharCode(0)].some(char => value.includes(char)))
|
|
33
|
+
throw new Error('invalid relative component');
|
|
34
|
+
};
|
|
35
|
+
const statCwd = () => fs.statSync('.', { bigint: true });
|
|
36
|
+
try {
|
|
37
|
+
if (!same(statCwd(), { dev: BigInt(job.dev), ino: BigInt(job.ino) })
|
|
38
|
+
|| !samePath(fs.realpathSync('.'), job.parent)) throw new Error('parent identity changed');
|
|
39
|
+
if (job.kind === 'mkdir' || job.kind === 'private-directory-mode') {
|
|
40
|
+
let expected = job.parent;
|
|
41
|
+
for (const name of job.segments) {
|
|
42
|
+
component(name);
|
|
43
|
+
let before;
|
|
44
|
+
try { before = fs.lstatSync(name, { bigint: true }); }
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code !== 'ENOENT' || job.kind !== 'mkdir') throw error;
|
|
47
|
+
try { fs.mkdirSync(name, { mode: 0o700 }); }
|
|
48
|
+
catch (mkdirError) { if (mkdirError.code !== 'EEXIST') throw mkdirError; }
|
|
49
|
+
before = fs.lstatSync(name, { bigint: true });
|
|
50
|
+
}
|
|
51
|
+
if (!before.isDirectory() || before.isSymbolicLink()) throw new Error('redirected directory');
|
|
52
|
+
process.chdir(name);
|
|
53
|
+
expected = path.join(expected, name);
|
|
54
|
+
if (!same(before, statCwd()) || !samePath(fs.realpathSync('.'), expected)) throw new Error('child identity changed');
|
|
55
|
+
}
|
|
56
|
+
if (job.kind === 'private-directory-mode' && process.platform !== 'win32') fs.chmodSync('.', 0o700);
|
|
57
|
+
} else if (job.kind === 'overlay' || job.kind === 'archive') {
|
|
58
|
+
component(job.name);
|
|
59
|
+
let before;
|
|
60
|
+
try { before = fs.lstatSync(job.name, { bigint: true }); }
|
|
61
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
62
|
+
if (before && (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n))
|
|
63
|
+
throw new Error('overlay must be a regular file');
|
|
64
|
+
const fd = fs.openSync(job.name, (job.kind === 'archive' ? fs.constants.O_RDWR : fs.constants.O_WRONLY) | (fs.constants.O_NOFOLLOW ?? 0)
|
|
65
|
+
| (fs.constants.O_NONBLOCK ?? 0) | (before ? 0 : fs.constants.O_CREAT | fs.constants.O_EXCL), 0o600);
|
|
66
|
+
try {
|
|
67
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
68
|
+
if (!opened.isFile() || opened.nlink !== 1n || (before && !same(before, opened)))
|
|
69
|
+
throw new Error('overlay identity changed');
|
|
70
|
+
fs.ftruncateSync(fd, 0);
|
|
71
|
+
if (job.kind === 'overlay') fs.writeFileSync(fd, job.content, 'utf8');
|
|
72
|
+
else {
|
|
73
|
+
const bytes = input.subarray(boundary + 1);
|
|
74
|
+
fs.writeFileSync(fd, bytes);
|
|
75
|
+
fs.fsyncSync(fd);
|
|
76
|
+
const hash = require('node:crypto').createHash('sha256');
|
|
77
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
78
|
+
let offset = 0;
|
|
79
|
+
while (offset < bytes.length) {
|
|
80
|
+
const count = fs.readSync(fd, chunk, 0, Math.min(chunk.length, bytes.length - offset), offset);
|
|
81
|
+
if (!count) throw new Error('incomplete archive read');
|
|
82
|
+
hash.update(chunk.subarray(0, count));
|
|
83
|
+
offset += count;
|
|
84
|
+
}
|
|
85
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
86
|
+
const current = fs.lstatSync(job.name, { bigint: true });
|
|
87
|
+
if (after.size !== BigInt(bytes.length) || after.nlink !== 1n || current.isSymbolicLink()
|
|
88
|
+
|| !same(opened, after) || !same(after, current) || hash.digest('hex') !== job.sha256)
|
|
89
|
+
throw new Error('archive verification failed');
|
|
90
|
+
}
|
|
91
|
+
} finally { fs.closeSync(fd); }
|
|
92
|
+
} else throw new Error('invalid operation');
|
|
93
|
+
} catch (error) { process.stderr.write(error.message); process.exitCode = 1; }
|
|
94
|
+
`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
/** Validate every existing ancestor before creating anything through a pathname. */
|
|
3
|
+
export declare function assertCanonicalDirectory(target: string, allowMissing?: boolean): Promise<BigIntStats | undefined>;
|
|
4
|
+
export declare function createCanonicalDirectory(target: string): Promise<void>;
|
|
5
|
+
export declare function createPrivateCanonicalDirectory(target: string): Promise<void>;
|
|
6
|
+
export declare function restorePrivateDirectoryMode(target: string): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runAttemptPathOperation } from "./attempt-path-operation.js";
|
|
4
|
+
/** Validate every existing ancestor before creating anything through a pathname. */
|
|
5
|
+
export async function assertCanonicalDirectory(target, allowMissing = false) {
|
|
6
|
+
const resolved = path.resolve(target);
|
|
7
|
+
const parent = path.dirname(resolved);
|
|
8
|
+
if (parent !== resolved)
|
|
9
|
+
await assertCanonicalDirectory(parent, allowMissing);
|
|
10
|
+
try {
|
|
11
|
+
const stat = await fs.lstat(resolved, { bigint: true });
|
|
12
|
+
const real = await fs.realpath(resolved);
|
|
13
|
+
const realStat = await fs.lstat(real, { bigint: true });
|
|
14
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || !samePath(real, resolved)
|
|
15
|
+
|| stat.dev !== realStat.dev || stat.ino !== realStat.ino) {
|
|
16
|
+
throw new Error("playwright_runtime_adapter_invalid: writable paths must be canonical, link-free directories.");
|
|
17
|
+
}
|
|
18
|
+
return realStat;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (allowMissing && error.code === "ENOENT")
|
|
22
|
+
return;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function createCanonicalDirectory(target) {
|
|
27
|
+
if (await assertCanonicalDirectory(target, true))
|
|
28
|
+
return;
|
|
29
|
+
const resolved = path.resolve(target);
|
|
30
|
+
let parent = path.dirname(resolved);
|
|
31
|
+
let identity = await assertCanonicalDirectory(parent, true);
|
|
32
|
+
while (!identity) {
|
|
33
|
+
parent = path.dirname(parent);
|
|
34
|
+
identity = await assertCanonicalDirectory(parent, true);
|
|
35
|
+
}
|
|
36
|
+
await runAttemptPathOperation(parent, identity, { kind: "mkdir", segments: path.relative(parent, resolved).split(path.sep) });
|
|
37
|
+
await assertCanonicalDirectory(resolved);
|
|
38
|
+
}
|
|
39
|
+
export async function createPrivateCanonicalDirectory(target) {
|
|
40
|
+
await createCanonicalDirectory(target);
|
|
41
|
+
await restorePrivateDirectoryMode(target);
|
|
42
|
+
}
|
|
43
|
+
export async function restorePrivateDirectoryMode(target) {
|
|
44
|
+
if (process.platform === "win32")
|
|
45
|
+
return;
|
|
46
|
+
const resolved = path.resolve(target);
|
|
47
|
+
const parent = path.dirname(resolved);
|
|
48
|
+
const identity = await assertCanonicalDirectory(parent);
|
|
49
|
+
await runAttemptPathOperation(parent, identity, { kind: "private-directory-mode", segments: [path.basename(resolved)] });
|
|
50
|
+
await assertCanonicalDirectory(resolved);
|
|
51
|
+
}
|
|
52
|
+
function samePath(left, right) {
|
|
53
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
54
|
+
}
|