fix: stabilize hwlab web-probe script navigation
This commit is contained in:
@@ -5131,17 +5131,25 @@ function runNodeWebProbeScript(
|
||||
): Record<string, unknown> {
|
||||
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.password ?? "");
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||
const parsedReport = parseJsonObject(result.stdout);
|
||||
const stdoutReport = parseJsonObject(result.stdout);
|
||||
const runPaths = webProbeScriptRunPathsFromStderr(result.stderr);
|
||||
const recoveredReport = stdoutReport === null ? readNodeWebProbeScriptReport(options, spec, runPaths.reportPath) : null;
|
||||
const recoveredArtifacts = stdoutReport === null || result.timedOut ? readNodeWebProbeScriptArtifacts(options, spec, runPaths.runDir) : null;
|
||||
const parsedReport = stdoutReport ?? recoveredReport?.report ?? null;
|
||||
const report = compactWebProbeScriptResult(parsedReport);
|
||||
const passed = result.exitCode === 0 && report?.ok === true;
|
||||
const summary = nullableRecord(report?.summary);
|
||||
const stdoutBytes = Buffer.byteLength(result.stdout, "utf8");
|
||||
const outputFailureKind = parsedReport === null
|
||||
? stdoutBytes > 64 * 1024
|
||||
? result.timedOut
|
||||
? "web-probe-command-timeout"
|
||||
: stdoutBytes > 64 * 1024
|
||||
? "web-probe-output-too-large"
|
||||
: "web-probe-report-parse-failed"
|
||||
: null;
|
||||
const degradedReason = typeof summary?.degradedReason === "string"
|
||||
const degradedReason = result.timedOut
|
||||
? "web-probe-command-timeout"
|
||||
: typeof summary?.degradedReason === "string"
|
||||
? summary.degradedReason
|
||||
: typeof report?.failureKind === "string"
|
||||
? report.failureKind
|
||||
@@ -5150,19 +5158,37 @@ function runNodeWebProbeScript(
|
||||
: result.timedOut
|
||||
? "web-probe-command-timeout"
|
||||
: null;
|
||||
const failureKind = typeof summary?.failureKind === "string"
|
||||
const failureKind = result.timedOut
|
||||
? "web-probe-command-timeout"
|
||||
: typeof summary?.failureKind === "string"
|
||||
? summary.failureKind
|
||||
: typeof report?.failureKind === "string"
|
||||
? report.failureKind
|
||||
: outputFailureKind;
|
||||
const effectiveSummary = summary ?? (outputFailureKind === null ? null : {
|
||||
const effectiveSummary = summary !== null ? {
|
||||
...summary,
|
||||
transportTimedOut: result.timedOut,
|
||||
recoveredFrom: stdoutReport !== null ? "stdout" : recoveredReport?.source ?? null,
|
||||
} : (outputFailureKind === null ? null : {
|
||||
ok: false,
|
||||
status: "blocked",
|
||||
degradedReason: outputFailureKind,
|
||||
failureKind: outputFailureKind,
|
||||
failedCondition: outputFailureKind === "web-probe-output-too-large"
|
||||
? "remote web-probe stdout exceeded the bounded summary parser input"
|
||||
: outputFailureKind === "web-probe-command-timeout"
|
||||
? "remote web-probe command timed out before stdout returned a parseable report"
|
||||
: "remote web-probe stdout did not contain a parseable JSON report",
|
||||
runDir: runPaths.runDir,
|
||||
reportPath: runPaths.reportPath,
|
||||
reportLoad: recoveredReport === null ? null : {
|
||||
source: recoveredReport.source,
|
||||
path: recoveredReport.path,
|
||||
degradedReason: recoveredReport.degradedReason,
|
||||
result: recoveredReport.result === null ? null : compactCommandResultRedacted(recoveredReport.result, [material.password ?? ""]),
|
||||
},
|
||||
screenshots: recoveredArtifacts?.screenshots ?? [],
|
||||
artifacts: recoveredArtifacts?.artifacts ?? null,
|
||||
stdoutBytes,
|
||||
stderrTail: result.stderr.trim().slice(-2000),
|
||||
valuesRedacted: true,
|
||||
@@ -5186,6 +5212,18 @@ function runNodeWebProbeScript(
|
||||
failureKind,
|
||||
summary: effectiveSummary,
|
||||
probe: report,
|
||||
reportLoad: stdoutReport !== null ? { source: "stdout", path: report?.reportPath ?? null, degradedReason: null } : recoveredReport === null ? null : {
|
||||
source: recoveredReport.source,
|
||||
path: recoveredReport.path,
|
||||
degradedReason: recoveredReport.degradedReason,
|
||||
result: recoveredReport.result === null ? null : compactCommandResultRedacted(recoveredReport.result, [material.password ?? ""]),
|
||||
},
|
||||
recoveredArtifacts: recoveredArtifacts === null ? null : {
|
||||
source: recoveredArtifacts.source,
|
||||
degradedReason: recoveredArtifacts.degradedReason,
|
||||
result: recoveredArtifacts.result === null ? null : compactCommandResultRedacted(recoveredArtifacts.result, [material.password ?? ""]),
|
||||
artifacts: recoveredArtifacts.artifacts,
|
||||
},
|
||||
result: compactResult,
|
||||
valuesRedacted: true,
|
||||
};
|
||||
@@ -5199,7 +5237,10 @@ function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secre
|
||||
"run_root=.state/web-probe-script",
|
||||
"mkdir -p \"$run_root\"",
|
||||
"run_dir=$(mktemp -d \"$run_root/run.XXXXXX\")",
|
||||
"report_file=\"$run_dir/web-probe-script-report.json\"",
|
||||
"chmod 700 \"$run_dir\"",
|
||||
"printf 'UNIDESK_WEB_PROBE_RUN_DIR=%s\\n' \"$run_dir\" >&2",
|
||||
"printf 'UNIDESK_WEB_PROBE_REPORT_PATH=%s\\n' \"$report_file\" >&2",
|
||||
"user_script=\"$run_dir/user-script.mjs\"",
|
||||
"runner=\"$run_dir/runner.mjs\"",
|
||||
`node -e "require('fs').writeFileSync(process.argv[1], Buffer.from(process.argv[2], 'base64'))" "$user_script" ${shellQuote(userScriptB64)}`,
|
||||
@@ -5217,6 +5258,104 @@ function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secre
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function webProbeScriptRunPathsFromStderr(stderr: string): { runDir: string | null; reportPath: string | null } {
|
||||
const runDir = lineValueFromText(stderr, "UNIDESK_WEB_PROBE_RUN_DIR");
|
||||
const markedReportPath = lineValueFromText(stderr, "UNIDESK_WEB_PROBE_REPORT_PATH");
|
||||
const reportPath = markedReportPath ?? (runDir === null ? null : `${runDir.replace(/\/$/u, "")}/web-probe-script-report.json`);
|
||||
return {
|
||||
runDir: isSafeWebProbeScriptRunDir(runDir) ? runDir : null,
|
||||
reportPath: isSafeWebProbeScriptReportPath(reportPath) ? reportPath : null,
|
||||
};
|
||||
}
|
||||
|
||||
function lineValueFromText(text: string, name: string): string | null {
|
||||
const pattern = new RegExp(`^${name}=([^\\r\\n]+)$`, "mu");
|
||||
const match = text.match(pattern);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
function readNodeWebProbeScriptReport(
|
||||
options: NodeWebProbeScriptOptions,
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
reportPath: string | null,
|
||||
): { source: string; report: Record<string, unknown> | null; result: CommandResult | null; degradedReason: string | null; path: string | null } | null {
|
||||
if (!reportPath) return null;
|
||||
if (!isSafeWebProbeScriptReportPath(reportPath)) return { source: "unsafe-path", report: null, result: null, degradedReason: "web-probe-report-path-invalid", path: reportPath };
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, `test -f ${shellQuote(reportPath)} && cat ${shellQuote(reportPath)}`, 55);
|
||||
if (result.exitCode !== 0 || result.timedOut) {
|
||||
return { source: "report-file", report: null, result, degradedReason: result.timedOut ? "web-probe-command-timeout" : "web-probe-report-read-failed", path: reportPath };
|
||||
}
|
||||
const report = parseJsonObject(result.stdout);
|
||||
return {
|
||||
source: "report-file",
|
||||
report,
|
||||
result,
|
||||
degradedReason: report === null ? "web-probe-report-parse-failed" : null,
|
||||
path: reportPath,
|
||||
};
|
||||
}
|
||||
|
||||
function readNodeWebProbeScriptArtifacts(
|
||||
options: NodeWebProbeScriptOptions,
|
||||
spec: HwlabRuntimeLaneSpec,
|
||||
runDir: string | null,
|
||||
): { source: string; artifacts: Record<string, unknown> | null; screenshots: Record<string, unknown>[]; result: CommandResult | null; degradedReason: string | null } | null {
|
||||
if (!runDir) return null;
|
||||
if (!isSafeWebProbeScriptRunDir(runDir)) return { source: "unsafe-path", artifacts: null, screenshots: [], result: null, degradedReason: "web-probe-run-dir-invalid" };
|
||||
const script = [
|
||||
"set -eu",
|
||||
`test -d ${shellQuote(runDir)}`,
|
||||
`find ${shellQuote(runDir)} -maxdepth 1 -type f \\( -name '*.png' -o -name '*.json' \\) -printf '%p\\t%s\\n' | tail -n 30`,
|
||||
].join("\n");
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, 55);
|
||||
if (result.exitCode !== 0 || result.timedOut) {
|
||||
return { source: "run-dir", artifacts: null, screenshots: [], result, degradedReason: result.timedOut ? "web-probe-command-timeout" : "web-probe-artifact-list-failed" };
|
||||
}
|
||||
const items = result.stdout.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [path, sizeText] = line.split("\t");
|
||||
const byteCount = Number(sizeText);
|
||||
return {
|
||||
kind: path.endsWith(".png") ? "screenshot" : "json",
|
||||
path,
|
||||
byteCount: Number.isFinite(byteCount) ? byteCount : null,
|
||||
};
|
||||
})
|
||||
.filter((item) => isSafeWebProbeScriptArtifactPath(String(item.path)));
|
||||
const screenshots = items.filter((item) => item.kind === "screenshot");
|
||||
return {
|
||||
source: "run-dir",
|
||||
artifacts: { runDir, count: items.length, items },
|
||||
screenshots,
|
||||
result,
|
||||
degradedReason: null,
|
||||
};
|
||||
}
|
||||
|
||||
function isSafeWebProbeScriptRunDir(value: string | null): value is string {
|
||||
return typeof value === "string"
|
||||
&& value.length > 0
|
||||
&& !value.includes("\0")
|
||||
&& !value.includes("..")
|
||||
&& /\.state\/web-probe-script\/run\.[A-Za-z0-9]+$/u.test(value);
|
||||
}
|
||||
|
||||
function isSafeWebProbeScriptReportPath(value: string | null): value is string {
|
||||
return typeof value === "string"
|
||||
&& isSafeWebProbeScriptArtifactPath(value)
|
||||
&& value.endsWith("/web-probe-script-report.json");
|
||||
}
|
||||
|
||||
function isSafeWebProbeScriptArtifactPath(value: string): boolean {
|
||||
return typeof value === "string"
|
||||
&& value.length > 0
|
||||
&& !value.includes("\0")
|
||||
&& !value.includes("..")
|
||||
&& /\.state\/web-probe-script\/run\.[A-Za-z0-9]+\/[^/]+[.](?:png|json)$/u.test(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function parseSecretOptions(args: string[]): NodeSecretOptions {
|
||||
|
||||
Reference in New Issue
Block a user