fix: 收紧 WebProbe 浏览器启动与清理

This commit is contained in:
Codex
2026-07-13 10:26:48 +02:00
parent 11b1f3d647
commit cd2863e030
5 changed files with 79 additions and 35 deletions
@@ -40,6 +40,12 @@ export function webProbeBrowserEvidenceEnvAssignments(spec: HwlabRuntimeLaneSpec
];
}
export function webProbeScreenshotFullPage(spec: HwlabRuntimeLaneSpec, explicitFullPage: boolean): boolean {
const policy = spec.webProbe?.resourcePolicy?.browserEvidence;
if (policy === undefined) throw new Error("web-probe browser evidence policy is missing from the owning YAML");
return explicitFullPage || policy.screenshotMode !== "viewport";
}
export function webProbeGuardedLaunchShell(
spec: HwlabRuntimeLaneSpec,
mode: WebProbeMemoryGuardMode,
@@ -48,6 +54,11 @@ export function webProbeGuardedLaunchShell(
): string {
const policy = memoryPolicy(spec);
if (policy === null) throw new Error("web-probe memory start policy is missing from the owning YAML");
const closeStepTimeoutMs = spec.webProbe?.resourcePolicy?.observerLifecycle.closeStepTimeoutMs;
if (closeStepTimeoutMs === undefined) throw new Error("web-probe observer lifecycle policy is missing from the owning YAML");
const cleanupPollMilliseconds = policy.launchReadyPollMilliseconds;
const cleanupPollSeconds = Math.max(0.05, cleanupPollMilliseconds / 1000);
const cleanupPollAttempts = Math.max(1, Math.ceil(closeStepTimeoutMs / cleanupPollMilliseconds));
const comparator = mode === "manual-start" ? "-le" : "-lt";
const common = [
`exec 9>${shellQuote(policy.launchLockPath)}`,
@@ -62,10 +73,10 @@ export function webProbeGuardedLaunchShell(
"launch_output=$(mktemp)",
"launch_error=$(mktemp)",
"trap 'rm -f \"$launch_output\" \"$launch_error\"' EXIT",
`if ! ${command} >"$launch_output" 2>"$launch_error"; then cat "$launch_output"; cat "$launch_error" >&2; exit 43; fi`,
`if ! (exec 9>&-; ${command}) >"$launch_output" 2>"$launch_error"; then cat "$launch_output"; cat "$launch_error" >&2; exit 43; fi`,
"cat \"$launch_output\"",
"cat \"$launch_error\" >&2",
`node - "$launch_output" ${shellQuote(String(policy.launchReadyTimeoutSeconds * 1000))} ${shellQuote(String(policy.launchReadyPollMilliseconds))} <<'UNIDESK_WEB_PROBE_STRUCTURED_READINESS'`,
`if ! node - "$launch_output" ${shellQuote(String(policy.launchReadyTimeoutSeconds * 1000))} ${shellQuote(String(policy.launchReadyPollMilliseconds))} <<'UNIDESK_WEB_PROBE_STRUCTURED_READINESS'`,
"const fs = require('fs');",
"const text = fs.readFileSync(process.argv[2], 'utf8').trim();",
"const timeoutMs = Number(process.argv[3]);",
@@ -73,9 +84,9 @@ export function webProbeGuardedLaunchShell(
"let value = null; try { value = JSON.parse(text); } catch {}",
"const data = value && typeof value === 'object' && value.data && typeof value.data === 'object' ? value.data : value;",
"if (!data || typeof data !== 'object' || !data.jobId) process.exit(43);",
"if (data.browserCreated === true && Number.isInteger(data.browserPid) && data.browserPid > 0) process.exit(0);",
"const rootPid = [data.browserPid, data.workerPid, data.jobPid, data.pid].find((item) => Number.isInteger(item) && item > 0);",
"const rootPid = [data.workerPid, data.jobPid, data.pid].find((item) => Number.isInteger(item) && item > 0);",
"if (!rootPid) process.exit(43);",
"const reportedBrowserPid = Number.isInteger(data.browserPid) && data.browserPid > 0 ? data.browserPid : null;",
"const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
"const browserDescendant = () => {",
" const rows = [];",
@@ -86,14 +97,14 @@ export function webProbeGuardedLaunchShell(
" const close = stat.lastIndexOf(')');",
" const fields = stat.slice(close + 2).split(' ');",
" const ppid = Number(fields[1]);",
" const command = fs.readFileSync('/proc/' + name + '/cmdline', 'utf8').replace(/\\0/g, ' ');",
" rows.push({ pid: Number(name), ppid, command });",
" const executable = fs.readlinkSync('/proc/' + name + '/exe');",
" rows.push({ pid: Number(name), ppid, executable });",
" } catch {}",
" }",
" const descendants = new Set([rootPid]);",
" let changed = true;",
" while (changed) { changed = false; for (const row of rows) if (descendants.has(row.ppid) && !descendants.has(row.pid)) { descendants.add(row.pid); changed = true; } }",
" return rows.some((row) => descendants.has(row.pid) && /(chromium|chrome|playwright)/i.test(row.command));",
" return rows.some((row) => descendants.has(row.pid) && (reportedBrowserPid === null || row.pid === reportedBrowserPid) && /(^|\\/)(chromium|chrome|google-chrome)(-[^/]*)?$/i.test(row.executable));",
"};",
"(async () => {",
" const deadline = Date.now() + timeoutMs;",
@@ -101,12 +112,24 @@ export function webProbeGuardedLaunchShell(
" process.exit(43);",
"})().catch(() => process.exit(43));",
"UNIDESK_WEB_PROBE_STRUCTURED_READINESS",
"then flock -u 9; exit 43; fi",
"flock -u 9",
].join("\n");
return [
...common,
`setsid sh -c ${shellQuote(command)} &`,
`setsid sh -c ${shellQuote(`exec 9>&-\n${command}`)} &`,
"browser_job_pid=$!",
"cleanup_browser_group() {",
" kill -TERM -- \"-$browser_job_pid\" 2>/dev/null || true",
" cleanup_attempt=0",
` while kill -0 -- "-$browser_job_pid" 2>/dev/null && [ "$cleanup_attempt" -lt ${cleanupPollAttempts} ]; do cleanup_attempt=$((cleanup_attempt + 1)); sleep ${cleanupPollSeconds}; done`,
" if kill -0 -- \"-$browser_job_pid\" 2>/dev/null; then kill -KILL -- \"-$browser_job_pid\" 2>/dev/null || true; fi",
" wait \"$browser_job_pid\" 2>/dev/null || true",
"}",
"trap 'browser_exit=$?; trap - EXIT TERM INT HUP; cleanup_browser_group; exit \"$browser_exit\"' EXIT",
"trap 'exit 143' TERM",
"trap 'exit 130' INT",
"trap 'exit 129' HUP",
"browser_ready=false",
`browser_ready_deadline=$(( $(date +%s) + ${policy.launchReadyTimeoutSeconds} ))`,
"while kill -0 \"$browser_job_pid\" 2>/dev/null; do",
@@ -120,13 +143,13 @@ export function webProbeGuardedLaunchShell(
" const stat = fs.readFileSync('/proc/' + name + '/stat', 'utf8');",
" const close = stat.lastIndexOf(')');",
" const fields = stat.slice(close + 2).split(' ');",
" rows.push({ pid: Number(name), ppid: Number(fields[1]), command: fs.readFileSync('/proc/' + name + '/cmdline', 'utf8').replace(/\\0/g, ' ') });",
" rows.push({ pid: Number(name), ppid: Number(fields[1]), executable: fs.readlinkSync('/proc/' + name + '/exe') });",
" } catch {}",
"}",
"const descendants = new Set([rootPid]);",
"let changed = true;",
"while (changed) { changed = false; for (const row of rows) if (descendants.has(row.ppid) && !descendants.has(row.pid)) { descendants.add(row.pid); changed = true; } }",
"process.exit(rows.some((row) => descendants.has(row.pid) && /(chromium|chrome|playwright)/i.test(row.command)) ? 0 : 1);",
"process.exit(rows.some((row) => descendants.has(row.pid) && /(^|\\/)(chromium|chrome|google-chrome)(-[^/]*)?$/i.test(row.executable)) ? 0 : 1);",
"UNIDESK_WEB_PROBE_CHILD_READINESS",
" then browser_ready=true; break; fi",
" if [ \"$(date +%s)\" -ge \"$browser_ready_deadline\" ]; then break; fi",
@@ -134,14 +157,14 @@ export function webProbeGuardedLaunchShell(
"done",
"flock -u 9",
"if [ \"$browser_ready\" != true ]; then",
" kill -TERM -- \"-$browser_job_pid\" 2>/dev/null || true",
" sleep 1",
" if kill -0 \"$browser_job_pid\" 2>/dev/null; then kill -KILL -- \"-$browser_job_pid\" 2>/dev/null || true; fi",
" wait \"$browser_job_pid\" 2>/dev/null || true",
" printf '%s\\n' 'web-probe browser launch readiness was not confirmed' >&2",
" exit 43",
"fi",
"set +e",
"wait \"$browser_job_pid\"",
"browser_exit=$?",
"set -e",
"exit \"$browser_exit\"",
].join("\n");
}