rea/scripts/verify/android/windows-guardian-isolation.mjs

106 lines
3.3 KiB
JavaScript

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createInterface } from "node:readline";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { createJadxWorkspaceRoot } from "../../../dist/android/JadxWorkspaceRoot.js";
const source = fileURLToPath(import.meta.url);
const marker = "REA synthetic guardian isolation test\n";
const exists = (path) =>
stat(path).then(
() => true,
() => false,
);
if (process.argv[1] === source && process.argv[2] === "owner") {
const root = await createJadxWorkspaceRoot();
await writeFile(join(root.path, "synthetic.txt"), marker);
process.stdout.write(JSON.stringify({ path: root.path }) + "\n");
await new Promise(() => {});
}
const bounded = async (promise, milliseconds, label) => {
let timer;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(label)), milliseconds);
}),
]);
} finally {
clearTimeout(timer);
}
};
const launch = async () => {
const child = spawn(process.execPath, [source, "owner"], {
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-8192);
});
const exit = new Promise((resolve, reject) => {
child.once("close", resolve);
child.once("error", reject);
});
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
try {
const ready = await bounded(
lines[Symbol.asyncIterator]().next(),
20000,
`Android guardian owner readiness timed out: ${stderr}`,
);
assert.equal(ready.done, false, stderr);
const { path } = JSON.parse(ready.value);
assert.equal(typeof path, "string");
assert.equal(await exists(join(path, "synthetic.txt")), true);
return { child, exit, path };
} catch (cause) {
child.kill();
await bounded(exit, 15000, "Android guardian owner did not exit");
throw cause;
}
};
const stop = async ({ child, exit, path }) => {
assert.equal(child.kill(), true);
await bounded(exit, 15000, "Android guardian owner did not exit");
for (let attempt = 0; attempt < 100; attempt++) {
if (!(await exists(path))) return;
await delay(100);
}
throw new Error(`Android guardian retained ${path} after owner death`);
};
/** Two independent owners must never delete one another's private workspace. */
export async function verifyWindowsGuardianIsolation() {
assert.equal(process.platform, "win32");
const first = await launch();
let second;
try {
second = await launch();
assert.notEqual(first.path, second.path);
await stop(first);
assert.equal(
await exists(join(second.path, "synthetic.txt")),
true,
"First owner deleted the second owner's workspace",
);
await stop(second);
console.log("PASS Windows Android guardian two-owner cleanup isolation");
} finally {
for (const owner of [first, second]) {
if (owner === undefined) continue;
if (owner.child.exitCode === null && owner.child.signalCode === null)
owner.child.kill();
await bounded(owner.exit, 15000, "Android guardian owner did not exit");
}
}
}