Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions e2e/slow-android-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { homedir, userInfo } from "node:os";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";

Expand Down Expand Up @@ -30,6 +32,30 @@ async function adbDevices(serverPort: number): Promise<string[]> {
.map((line) => line.split(/\s+/)[0] as string);
}

/**
* why: the emulator keeps crash dumps in `/tmp/android-<user>/emu-crash-<version>.db`, shared with
* Android Studio, and `-crash-report-mode never` deletes the ones in the database it reads.
* Returns the shared `completed/` folder of the installed emulator.
*/
async function sharedCrashCompletedDir(): Promise<string> {
const emulator = join(ANDROID_HOME, "emulator", "emulator");
const { stdout } = await execFileAsync(emulator, ["-version"]);
const version = /emulator version (\d+\.\d+\.\d+)/.exec(stdout)?.[1];
if (version === undefined) {
throw new Error(`could not read the emulator version from: ${stdout}`);
}
return join("/tmp", `android-${userInfo().username}`, `emu-crash-${version}.db`, "completed");
}

/** why: the test removes only the folders it created, so it needs the topmost one that was missing. */
function firstMissingAncestor(path: string): string | undefined {
let missing: string | undefined;
for (let dir = path; !existsSync(dir); dir = dirname(dir)) {
missing = dir;
}
return missing;
}

async function avdManagerList(): Promise<string[]> {
const avdmanager = join(ANDROID_HOME, "cmdline-tools", "latest", "bin", "avdmanager");
const binary = existsSync(avdmanager)
Expand Down Expand Up @@ -86,7 +112,7 @@ describe.skipIf(!hasAndroidSdk)(
{ tags: ["slow", "android"] },
() => {
it(
"catalog lists Simlock's Pixel 9 first, a cold lease of Pixel 9 boots a real emulator with the library screen size, and a shut-down emulator reports erasableReadable:false with no crash or false provenance finding",
"catalog lists Simlock's Pixel 9 first, a cold lease of Pixel 9 boots a real emulator with the library screen size and leaves a dump in the user's shared crash database alone, and a shut-down emulator reports erasableReadable:false with no crash or false provenance finding",
{ timeout: 420_000 },
async () => {
const env = await withDaemon({
Expand All @@ -98,7 +124,26 @@ describe.skipIf(!hasAndroidSdk)(
if (adbServerPort === undefined) {
throw new Error("the real-SDK lane must allocate its own adb server port");
}
const completedDir = await sharedCrashCompletedDir();
// why: crashpad skips a report file without its database xattrs, so a plain file would never be read, let alone deleted.
const dumpId = randomUUID();
const planted = join(completedDir, `${dumpId}.dmp`);
const createdRoot = firstMissingAncestor(completedDir);
try {
await mkdir(completedDir, { recursive: true });
await writeFile(planted, "simlock slow-lane crash dump marker");
await execFileAsync("xattr", [
"-w",
"org.chromium.crashpad.database.uuid",
dumpId,
planted,
]);
await execFileAsync("xattr", [
"-w",
"org.chromium.crashpad.database.creation_time",
String(Math.floor(Date.now() / 1000)),
planted,
]);
const catalog = await env.cli(["catalog", "--json", "--platform", "android"]);
expect(catalog.code).toBe(0);
checkCatalog(catalog.json as AndroidCatalogOutput);
Expand Down Expand Up @@ -143,6 +188,12 @@ describe.skipIf(!hasAndroidSdk)(
const density = await env.cli(["adb", "shell", "wm", "density"]);
expect(density.stdout).toContain("Physical density: 420");

// why: the lease is up, so no dialog blocked the boot, and the launch must have left the user's shared crash database alone.
expect(
existsSync(planted),
"the lease deleted a dump in the user's shared crash db",
).toBe(true);

const refused = await env.cli(["adb", "kill-server"]);
expect(refused.code, "simlock adb kill-server must be refused, not run").toBe(2);
expect(refused.error?.code).toBe("USAGE");
Expand Down Expand Up @@ -210,6 +261,10 @@ describe.skipIf(!hasAndroidSdk)(
const list = await env.cli(["list", "--devices"]);
expect(list.code).toBe(0);
} finally {
await rm(planted, { force: true });
if (createdRoot !== undefined) {
await rm(createdRoot, { recursive: true, force: true });
}
await env
.cli(["nuke", "--delete-devices", "--yes"], { timeout: 120_000 })
.catch(() => undefined);
Expand Down
2 changes: 2 additions & 0 deletions src/daemon/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2419,6 +2419,8 @@ describe("discoverDrivers on a host with an Android SDK", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-no-window",
"-gpu",
"host",
Expand Down
2 changes: 1 addition & 1 deletion src/drivers/android/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Provision — `provision`:
Boot — `makeReady` (per-device lock):

1. Baseline: stored hash matches → `-snapshot simlock_clean_baseline`; differs → delete
the snapshots, `-wipe-data`; none → cold boot. Always `-no-snapshot-save`, and `-grpc <console port + 3000> -grpc-use-token` (without `-grpc`, `-port` leaves the emulator with no gRPC endpoint and no `avd/running` discovery file).
the snapshots, `-wipe-data`; none → cold boot. Always `-no-snapshot-save`, and `-grpc <console port + 3000> -grpc-use-token` (without `-grpc`, `-port` leaves the emulator with no gRPC endpoint and no `avd/running` discovery file), and `-crash-report-mode never` (a stale crash dump otherwise opens a modal consent dialog that blocks the boot; `never` deletes the dumps in the database it reads, so the spawn env sets `ANDROID_EMU_CRASH_REPORTING_DATABASE` to `<root>/emu-crash.db`, never the user's shared `/tmp/android-<user>/emu-crash-<version>.db`).
2. Console port reserved at the first `prepare` boot in this process: even 5586–5682, not in
`adb devices`, not reserved, port, port+1 and port+3000 (gRPC) not listening.
3. Ready = `sys.boot_completed` 1, `init.svc.bootanim` stopped or unset, `adb emu avd path`
Expand Down
89 changes: 87 additions & 2 deletions src/drivers/android/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ const scopedEnv = {
} as const;
const scopedOptions = { env: scopedEnv } as const;
// why: The emulator outlives the daemon and is never read from, so it is spawned detached.
const emulatorOptions = { env: scopedEnv, stdio: "ignore" } as const;
const emulatorOptions = {
env: { ...scopedEnv, ANDROID_EMU_CRASH_REPORTING_DATABASE: `${avdDirectory}/emu-crash.db` },
stdio: "ignore",
} as const;
const binaries = {
adb: `${sdk}/platform-tools/adb`,
avdmanager: `${sdk}/cmdline-tools/latest/bin/avdmanager`,
Expand Down Expand Up @@ -513,6 +516,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-no-snapshot-load",
],
command: binaries.emulator,
Expand Down Expand Up @@ -552,6 +557,50 @@ describe("AndroidDriver", () => {
);
});

it("starts the cold boot and the restart from the baseline with -crash-report-mode never and a crash database under the Simlock root, so a stale crash dump asks no one and the user's shared database is never read", async () => {
const filesystem = await androidFilesystem();
const host = new EmulatorHost(filesystem);
const launches: (readonly string[])[] = [];
const crashDatabases: (string | undefined)[] = [];
const hostRunner = host.runner();
const runner: ProcessRunner = {
...hostRunner,
spawn: (command, args, options) => {
launches.push(args);
crashDatabases.push(options?.env?.ANDROID_EMU_CRASH_REPORTING_DATABASE);
return hostRunner.spawn(command, args, options);
},
};
const driver = await createDriver(filesystem, runner, {
ids: ["one"],
tcpProbe: new FakeTcpProbe([adbServerPort]),
});
const spec = {
physical: false,
model: "Pixel 5",
osVersion: "34",
platform: "android",
} as const;
await driver.resolveSpec(spec);

await driver.makeReady(await driver.provision(spec));

expect(crashDatabases).toEqual([
`${avdDirectory}/emu-crash.db`,
`${avdDirectory}/emu-crash.db`,
]);
const crashReportMode = (args: readonly string[]) => {
const flag = args.indexOf("-crash-report-mode");
return flag === -1 ? undefined : args[flag + 1];
};
expect(
launches.map((args) => ({ boot: args.at(-1), crashReportMode: crashReportMode(args) })),
).toEqual([
{ boot: "-no-snapshot-load", crashReportMode: "never" },
{ boot: "simlock_clean_baseline", crashReportMode: "never" },
]);
});

/**
* why: The half of the daemon-stop fix that argv cannot show. `stdio: "ignore"` alone does not
* release the event loop -- the ChildProcess handle does -- so without the `unref` a
Expand Down Expand Up @@ -673,6 +722,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-snapshot",
"simlock_clean_baseline",
],
Expand Down Expand Up @@ -790,6 +841,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-wipe-data",
"-no-snapshot-load",
],
Expand Down Expand Up @@ -1101,6 +1154,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-snapshot",
"simlock_clean_baseline",
],
Expand Down Expand Up @@ -1153,6 +1208,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-snapshot",
"simlock_clean_baseline",
],
Expand Down Expand Up @@ -1214,6 +1271,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-no-snapshot-load",
],
[
Expand All @@ -1225,6 +1284,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-snapshot",
"simlock_clean_baseline",
],
Expand All @@ -1244,7 +1305,7 @@ describe("AndroidDriver", () => {
const launches = emulatorLaunches(harness.runner);
expect(launches).toHaveLength(2);
for (const args of launches) {
expect(args.slice(0, 8 + flags.length)).toEqual([
expect(args.slice(0, 10 + flags.length)).toEqual([
"-avd",
"simlock_one",
"-port",
Expand All @@ -1253,6 +1314,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...flags,
]);
}
Expand All @@ -1273,6 +1336,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-gpu",
"swiftshader_indirect",
"-no-snapshot-load",
Expand Down Expand Up @@ -1336,6 +1401,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...flags,
"-snapshot",
"simlock_clean_baseline",
Expand Down Expand Up @@ -1378,6 +1445,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...flags,
"-wipe-data",
"-no-snapshot-load",
Expand Down Expand Up @@ -1407,6 +1476,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...flags,
"-snapshot",
"simlock_clean_baseline",
Expand Down Expand Up @@ -1436,6 +1507,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...flags,
"-wipe-data",
"-no-snapshot-load",
Expand Down Expand Up @@ -1464,6 +1537,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...flags,
"-snapshot",
"simlock_clean_baseline",
Expand Down Expand Up @@ -1517,6 +1592,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...launchFlags,
"-no-snapshot-load",
],
Expand Down Expand Up @@ -1558,6 +1635,8 @@ describe("AndroidDriver", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...flags,
"-no-snapshot-load",
],
Expand Down Expand Up @@ -5414,6 +5493,8 @@ describe("AndroidDriver console port reservations", () => {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
"-no-snapshot-load",
],
command: binaries.emulator,
Expand Down Expand Up @@ -7214,6 +7295,8 @@ function baselineBuildExpectations(options: {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...emulatorFlags,
...options.launchArgs,
],
Expand Down Expand Up @@ -7304,6 +7387,8 @@ function baselineBuildExpectations(options: {
"-grpc",
"8586",
"-grpc-use-token",
"-crash-report-mode",
"never",
...emulatorFlags,
"-snapshot",
"simlock_clean_baseline",
Expand Down
17 changes: 16 additions & 1 deletion src/drivers/android/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,18 @@ export class AndroidDriver implements Driver {
};
}

/**
* why: `-crash-report-mode never` deletes every dump in the crash database the emulator reads, and
* by default that is `/tmp/android-<user>/emu-crash-<version>.db`, shared with Android Studio and
* the user's own emulators. A database inside Simlock's own root keeps those dumps untouched.
*/
#emulatorEnv(): NodeJS.ProcessEnv {
return {
...this.#env(),
ANDROID_EMU_CRASH_REPORTING_DATABASE: join(this.#deviceRoot, "emu-crash.db"),
};
}

/**
* why: Announces every console port Simlock may have an emulator on to the server that was
* just started or adopted -- deliberately doing, for Simlock's own range only, what adb's
Expand Down Expand Up @@ -2235,10 +2247,13 @@ export class AndroidDriver implements Driver {
"-grpc",
String(data.port + GRPC_PORT_OFFSET),
"-grpc-use-token",
// why: a dump an earlier crash left in a crash database makes the emulator open a modal consent dialog at launch, which blocks its main thread before any port opens; `never` skips the dialog and deletes the dumps in the database it reads, so `#emulatorEnv` points that at Simlock's own root and never at the user's shared one.
"-crash-report-mode",
"never",
...this.#emulatorFlags,
...launchArgs,
],
{ env: this.#env(), stdio: "ignore" },
{ env: this.#emulatorEnv(), stdio: "ignore" },
);
handle.unref();
state.handle = handle;
Expand Down
Loading
Loading