Files
rea/experiments/analysis-efficiency/electron-cancellation.mjs
T
180ba03bfd fix(electron): preserve cancellation across waits, state reads and cleanup
Fixes #4388. Refs #4027.

Observe cancellation during first-window waits and state reads; preserve cancellation and caller reasons before cleanup and retain partial observations when cleanup fails. Keep late cancellation and cleanup failure precedence.

Validation: 49 focused cases, static/docs checks, six controlled verifier-oracle cases and 39 current-head CI checks passed. Codex and Greptile found no remaining blocking issues. Real Joplin cancellation phases verified; complete Linux cleanup/first-signal CLI exit and packaged-runtime startup remain explicitly unverified/failed. Both verifiers now fail incomplete cleanup, including after main-process exit.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
2026-10-16 21:12:26 +08:00

274 lines
8.5 KiB
JavaScript

#!/usr/bin/env node
// Real applications run unchanged. Fault cases hold a REA-side Playwright reply;
// they do not establish that the application naturally exhibits that failure.
import assert from "node:assert/strict";
import { mkdtemp, readFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { _electron } from "playwright-core";
const { values } = parseArgs({
options: {
executable: { type: "string" },
application: { type: "string" },
"profile-root": { type: "string" },
"runtime-root": { type: "string" },
case: { type: "string", default: "normal-wait" },
},
});
const cases = ["normal-wait", "window-wait", "initial-state", "final-state"];
assert.equal(
process.platform,
"linux",
"This verifier checks process identity through Linux /proc",
);
assert.ok(cases.includes(values.case));
assert.ok(values.executable && values.application && values["profile-root"]);
const runtime = resolve(
values["runtime-root"] ?? fileURLToPath(new URL("../..", import.meta.url)),
);
const { PlaywrightElectronActiveProvider } = await import(
pathToFileURL(
join(runtime, "dist/browser/PlaywrightElectronActiveProvider.js"),
)
);
const { captureElectronScenario } = await import(
pathToFileURL(
join(
runtime,
"dist/application/javascript/ElectronActiveObservationService.js",
),
)
);
const { runWithExecutionLimits } = await import(
pathToFileURL(
join(runtime, "dist/browser/PlaywrightElectronActiveActions.js"),
)
);
const profile = await mkdtemp(join(resolve(values["profile-root"]), "joplin-"));
const controller = new AbortController();
const reason = new Error(`real-application cancellation: ${values.case}`);
const started = performance.now();
let application;
let child;
let initialBirth;
let evaluateCalls = 0;
let injected = false;
let cancelledAt;
let cancelTimer;
let watchdog;
let rejectDeadline;
const deadline = new Promise((_, reject) => {
rejectDeadline = reject;
});
const birth = async (pid) => {
try {
const text = await readFile(`/proc/${pid}/stat`, "utf8");
const fields = text.slice(text.lastIndexOf(")") + 2).split(" ");
return fields[0] === "Z" ? null : fields[19];
} catch (cause) {
if (cause.code === "ENOENT") return null;
throw cause;
}
};
const armCancellation = () => {
if (cancelTimer !== undefined) return;
cancelTimer = setTimeout(
() => {
cancelledAt = performance.now();
controller.abort(reason);
watchdog = setTimeout(
() =>
rejectDeadline(
new Error("Cancellation did not settle within 20 seconds"),
),
20_000,
);
},
values.case === "normal-wait" ? 1_000 : 100,
);
};
const launch = async (options) => {
application = await _electron.launch(options);
child = application.process();
initialBirth = await birth(child.pid);
assert.ok(initialBirth, "Launched process identity is unavailable");
return new Proxy(application, {
get(target, key) {
if (key === "evaluate")
return (...args) => {
evaluateCalls += 1;
if (
(values.case === "initial-state" && evaluateCalls === 1) ||
(values.case === "final-state" && evaluateCalls === 5)
) {
injected = true;
armCancellation();
return new Promise(() => {});
}
const result = target.evaluate(...args);
if (values.case === "normal-wait" && evaluateCalls === 4)
void result.then(armCancellation, () => undefined);
return result;
};
if (key === "firstWindow" && values.case === "window-wait")
return () => {
injected = true;
armCancellation();
return new Promise(() => {});
};
const value = Reflect.get(target, key, target);
return typeof value === "function" ? value.bind(target) : value;
},
});
};
let record;
try {
const input = {
executable_path: resolve(values.executable),
application_path: resolve(values.application),
args: [
"--no-sandbox",
"--disable-gpu",
`--user-data-dir=${profile}/chromium`,
"--profile",
profile,
],
actions: values.case.endsWith("state")
? []
: [{ step_id: "settle", kind: "wait", duration_ms: 60_000 }],
};
const result = await Promise.race([
captureElectronScenario(
new PlaywrightElectronActiveProvider(process.env, launch),
input,
{ signal: controller.signal },
),
deadline,
]);
assert.equal(result.ok, false, "Expected a typed cancellation");
const error = result.error;
const partial = error.partialObservation?.capture;
const causes = [];
const seen = new Set();
for (
let cause = error;
cause instanceof Error && !seen.has(cause);
cause = cause.cause
) {
seen.add(cause);
causes.push({
tag: cause._tag ?? null,
reason: cause.reason ?? null,
message: cause.message,
diagnostics: cause.diagnostics ?? null,
});
}
record = {
case: values.case,
fault_injected: injected,
input,
profile,
evaluate_calls: evaluateCalls,
elapsed_ms: performance.now() - started,
cancellation_to_result_ms:
cancelledAt === undefined ? null : performance.now() - cancelledAt,
tag: error._tag,
reason: error.reason,
user_category: error.userCategory,
diagnostics: error.diagnostics ?? null,
message: error.message,
user_message: error.userMessage ?? null,
cleanup: error.cleanup ?? null,
cause: error.cause instanceof Error ? error.cause.message : null,
causes,
cleanup_incomplete: error.cleanupIncomplete,
partial:
partial === undefined
? null
: {
application: partial.application,
actions: partial.actions,
coverage: partial.coverage,
window_count: partial.windows.length,
limitations: partial.limitations,
},
launched_process_exited:
child !== undefined && (await birth(child.pid)) !== initialBirth,
};
assert.equal(controller.signal.aborted, true);
assert.equal(error.userCategory, "cancelled");
if (values.case !== "normal-wait") assert.equal(injected, true);
if (values.case.endsWith("wait"))
assert.ok(partial?.actions.some((action) => action.status === "cancelled"));
if (values.case === "initial-state") assert.equal(partial, undefined);
if (values.case === "final-state") {
assert.ok(
partial?.limitations.some((text) =>
text.includes("initial pre-action snapshot"),
),
);
assert.ok(
causes.some(
(cause) =>
cause.diagnostics?.final_state_read === "cancellation_grace_expired",
),
);
}
// Cleanup is a separate oracle. A known Linux lineage limitation must remain
// visible, rather than turning cancellation coverage into a cleanup claim.
record.cancellation_phase = "verified";
record.cleanup_phase =
record.launched_process_exited && !error.cleanupIncomplete
? "verified"
: "unverified";
record.workflow_status =
record.cleanup_phase === "verified" ? "verified" : "partial";
console.log(JSON.stringify(record));
assert.equal(
record.cleanup_phase,
"verified",
"Provider cleanup remains unverified; cancellation alone is a partial result",
);
} catch (cause) {
console.log(
JSON.stringify({
case: values.case,
fault_injected: injected,
evaluate_calls: evaluateCalls,
elapsed_ms: performance.now() - started,
cancellation_to_failure_ms:
cancelledAt === undefined ? null : performance.now() - cancelledAt,
completed: false,
observed: record ?? null,
verifier_error: cause instanceof Error ? cause.message : String(cause),
}),
);
throw cause;
} finally {
clearTimeout(cancelTimer);
clearTimeout(watchdog);
if (
application !== undefined &&
child?.exitCode === null &&
child?.signalCode === null
)
await runWithExecutionLimits(
application.close(),
undefined,
performance.now() + 5_000,
).catch((cause) => {
console.error(
JSON.stringify({
verifier_cleanup: "unverified",
message: cause.message,
}),
);
process.exitCode = 1;
});
// The external resource scope owns any leftovers; do not leave a failed
// verifier awaiting an application whose ownership the provider cannot prove.
if (child?.exitCode === null && child?.signalCode === null) process.exit(1);
}