From d84762ac0c0c8c63eb1efcf95afc95ff37564d2b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 1 Nov 2026 21:09:19 +0100 Subject: [PATCH 1/5] =?UTF-8?q?test:=20reproduce=20#1183=20=E2=80=94=20emu?= =?UTF-8?q?lator=20boots=20start=20without=20-crash-report-mode=20never?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/drivers/android/index.test.ts | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/drivers/android/index.test.ts b/src/drivers/android/index.test.ts index 9dd019c38..53a326d37 100644 --- a/src/drivers/android/index.test.ts +++ b/src/drivers/android/index.test.ts @@ -552,6 +552,44 @@ describe("AndroidDriver", () => { ); }); + it("starts the cold boot and the restart from the baseline with -crash-report-mode never, so a stale crash dump asks no one", async () => { + const filesystem = await androidFilesystem(); + const host = new EmulatorHost(filesystem); + const launches: (readonly string[])[] = []; + const hostRunner = host.runner(); + const runner: ProcessRunner = { + ...hostRunner, + spawn: (command, args) => { + launches.push(args); + return hostRunner.spawn(command, args); + }, + }; + 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)); + + 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 From 0cbba442ff48fc40df8f0d6bb1dfc29f395ebb30 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 1 Nov 2026 21:36:21 +0100 Subject: [PATCH 2/5] fix(android): start the emulator with -crash-report-mode never (#1183) --- src/daemon/main.test.ts | 2 + src/drivers/android/AGENTS.md | 2 +- src/drivers/android/index.test.ts | 40 +++++++++++++++++++- src/drivers/android/index.ts | 3 ++ src/drivers/android/physical-handler.test.ts | 2 +- 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/daemon/main.test.ts b/src/daemon/main.test.ts index 65aae48fc..fc524c26d 100644 --- a/src/daemon/main.test.ts +++ b/src/daemon/main.test.ts @@ -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", diff --git a/src/drivers/android/AGENTS.md b/src/drivers/android/AGENTS.md index 6da184cae..86d1e297d 100644 --- a/src/drivers/android/AGENTS.md +++ b/src/drivers/android/AGENTS.md @@ -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 -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 -grpc-use-token`, and `-crash-report-mode never` (a stale crash dump otherwise opens a modal consent dialog that blocks the boot; `never` also drops the dump) (without `-grpc`, `-port` leaves the emulator with no gRPC endpoint and no `avd/running` discovery file). 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` diff --git a/src/drivers/android/index.test.ts b/src/drivers/android/index.test.ts index 53a326d37..a6791425b 100644 --- a/src/drivers/android/index.test.ts +++ b/src/drivers/android/index.test.ts @@ -513,6 +513,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-no-snapshot-load", ], command: binaries.emulator, @@ -711,6 +713,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-snapshot", "simlock_clean_baseline", ], @@ -828,6 +832,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-wipe-data", "-no-snapshot-load", ], @@ -1139,6 +1145,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-snapshot", "simlock_clean_baseline", ], @@ -1191,6 +1199,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-snapshot", "simlock_clean_baseline", ], @@ -1252,6 +1262,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-no-snapshot-load", ], [ @@ -1263,6 +1275,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-snapshot", "simlock_clean_baseline", ], @@ -1282,7 +1296,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", @@ -1291,6 +1305,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...flags, ]); } @@ -1311,6 +1327,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-gpu", "swiftshader_indirect", "-no-snapshot-load", @@ -1374,6 +1392,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...flags, "-snapshot", "simlock_clean_baseline", @@ -1416,6 +1436,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...flags, "-wipe-data", "-no-snapshot-load", @@ -1445,6 +1467,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...flags, "-snapshot", "simlock_clean_baseline", @@ -1474,6 +1498,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...flags, "-wipe-data", "-no-snapshot-load", @@ -1502,6 +1528,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...flags, "-snapshot", "simlock_clean_baseline", @@ -1555,6 +1583,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...launchFlags, "-no-snapshot-load", ], @@ -1596,6 +1626,8 @@ describe("AndroidDriver", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...flags, "-no-snapshot-load", ], @@ -5452,6 +5484,8 @@ describe("AndroidDriver console port reservations", () => { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", "-no-snapshot-load", ], command: binaries.emulator, @@ -7252,6 +7286,8 @@ function baselineBuildExpectations(options: { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...emulatorFlags, ...options.launchArgs, ], @@ -7342,6 +7378,8 @@ function baselineBuildExpectations(options: { "-grpc", "8586", "-grpc-use-token", + "-crash-report-mode", + "never", ...emulatorFlags, "-snapshot", "simlock_clean_baseline", diff --git a/src/drivers/android/index.ts b/src/drivers/android/index.ts index 9ba50d3ae..9cac35fea 100644 --- a/src/drivers/android/index.ts +++ b/src/drivers/android/index.ts @@ -2235,6 +2235,9 @@ export class AndroidDriver implements Driver { "-grpc", String(data.port + GRPC_PORT_OFFSET), "-grpc-use-token", + // why: a dump an earlier crash left in the user's 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 drops the dump. + "-crash-report-mode", + "never", ...this.#emulatorFlags, ...launchArgs, ], diff --git a/src/drivers/android/physical-handler.test.ts b/src/drivers/android/physical-handler.test.ts index 15d7100e4..d1c71434a 100644 --- a/src/drivers/android/physical-handler.test.ts +++ b/src/drivers/android/physical-handler.test.ts @@ -1375,7 +1375,7 @@ describe("Android physical handler: independence from emulator calls", () => { { hangs: true, match: { - args: [/^-avd$/, "simlock_one", /.*/, /.*/, /.*/, /.*/, /.*/, /.*/, /.*/], + args: [/^-avd$/, "simlock_one", /.*/, /.*/, /.*/, /.*/, /.*/, /.*/, /.*/, /.*/, /.*/], command: `${sdk}/emulator/emulator`, }, }, From b189e5ad3ea45eaa50d02ebcc1eccd0b90322054 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 1 Nov 2026 21:44:30 +0100 Subject: [PATCH 3/5] fix(android): give the emulator a crash database under the Simlock root (#1183) --- src/drivers/android/AGENTS.md | 2 +- src/drivers/android/index.test.ts | 17 +++++++++++++---- src/drivers/android/index.ts | 16 ++++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/drivers/android/AGENTS.md b/src/drivers/android/AGENTS.md index 86d1e297d..92028473f 100644 --- a/src/drivers/android/AGENTS.md +++ b/src/drivers/android/AGENTS.md @@ -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 -grpc-use-token`, and `-crash-report-mode never` (a stale crash dump otherwise opens a modal consent dialog that blocks the boot; `never` also drops the dump) (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 -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 `/emu-crash.db`, never the user's shared `/tmp/android-/emu-crash-.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` diff --git a/src/drivers/android/index.test.ts b/src/drivers/android/index.test.ts index a6791425b..7ce7984d0 100644 --- a/src/drivers/android/index.test.ts +++ b/src/drivers/android/index.test.ts @@ -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`, @@ -554,16 +557,18 @@ describe("AndroidDriver", () => { ); }); - it("starts the cold boot and the restart from the baseline with -crash-report-mode never, so a stale crash dump asks no one", async () => { + 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) => { + spawn: (command, args, options) => { launches.push(args); - return hostRunner.spawn(command, args); + crashDatabases.push(options?.env?.ANDROID_EMU_CRASH_REPORTING_DATABASE); + return hostRunner.spawn(command, args, options); }, }; const driver = await createDriver(filesystem, runner, { @@ -580,6 +585,10 @@ describe("AndroidDriver", () => { 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]; diff --git a/src/drivers/android/index.ts b/src/drivers/android/index.ts index 9cac35fea..f0d6e071c 100644 --- a/src/drivers/android/index.ts +++ b/src/drivers/android/index.ts @@ -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-/emu-crash-.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 @@ -2235,13 +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 the user's 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 drops the dump. + // 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; From eb2de310f80b39971791e287857d22ea18aa526c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 1 Nov 2026 21:48:08 +0100 Subject: [PATCH 4/5] test(e2e): a lease leaves a dump in the user's shared emulator crash db alone (#1183) --- e2e/slow-android-smoke.test.ts | 46 +++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/e2e/slow-android-smoke.test.ts b/e2e/slow-android-smoke.test.ts index ed46c3556..355a7a16f 100644 --- a/e2e/slow-android-smoke.test.ts +++ b/e2e/slow-android-smoke.test.ts @@ -1,7 +1,8 @@ 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 { homedir, userInfo } from "node:os"; +import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; @@ -30,6 +31,30 @@ async function adbDevices(serverPort: number): Promise { .map((line) => line.split(/\s+/)[0] as string); } +/** + * why: the emulator keeps crash dumps in `/tmp/android-/emu-crash-.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 { + 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 { const avdmanager = join(ANDROID_HOME, "cmdline-tools", "latest", "bin", "avdmanager"); const binary = existsSync(avdmanager) @@ -86,7 +111,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({ @@ -98,7 +123,12 @@ describe.skipIf(!hasAndroidSdk)( if (adbServerPort === undefined) { throw new Error("the real-SDK lane must allocate its own adb server port"); } + const completedDir = await sharedCrashCompletedDir(); + const planted = join(completedDir, `simlock-1183-${process.pid}-${Date.now()}.dmp`); + const createdRoot = firstMissingAncestor(completedDir); try { + await mkdir(completedDir, { recursive: true }); + await writeFile(planted, "simlock slow-lane crash dump marker"); const catalog = await env.cli(["catalog", "--json", "--platform", "android"]); expect(catalog.code).toBe(0); checkCatalog(catalog.json as AndroidCatalogOutput); @@ -143,6 +173,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"); @@ -210,6 +246,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); From 3301085f17092c2e361a777c3d698fe6334932b5 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Sun, 1 Nov 2026 21:58:42 +0100 Subject: [PATCH 5/5] test(e2e): plant a crash dump crashpad recognises (xattrs) so the smoke test goes red without the crash database (#1183) --- e2e/slow-android-smoke.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/e2e/slow-android-smoke.test.ts b/e2e/slow-android-smoke.test.ts index 355a7a16f..10a42d08f 100644 --- a/e2e/slow-android-smoke.test.ts +++ b/e2e/slow-android-smoke.test.ts @@ -1,6 +1,7 @@ import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; 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"; @@ -124,11 +125,25 @@ describe.skipIf(!hasAndroidSdk)( throw new Error("the real-SDK lane must allocate its own adb server port"); } const completedDir = await sharedCrashCompletedDir(); - const planted = join(completedDir, `simlock-1183-${process.pid}-${Date.now()}.dmp`); + // 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);