From 57bb7ec5ca44388b9e4ced1588eda0a72bd879ac Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:00:18 +0800 Subject: [PATCH] fix(adb): parse successful daemon notices from both output streams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recognize exact current and legacy ADB startup notices without treating stderr as device inventory or accepting error lookalikes. Fixes #3051. Reviewed-PR: #3073 Co-authored-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com> --- src/adb/AdbDeviceOutput.test.ts | 63 +++++++++++++++++++++++++++++++++ src/adb/AdbDeviceOutput.ts | 25 ++++++++++--- src/adb/AdbProvider.test.ts | 30 ++++++++++++++++ src/adb/AdbProvider.ts | 1 + 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/src/adb/AdbDeviceOutput.test.ts b/src/adb/AdbDeviceOutput.test.ts index 12b82ab84a..17dec05c38 100644 --- a/src/adb/AdbDeviceOutput.test.ts +++ b/src/adb/AdbDeviceOutput.test.ts @@ -56,7 +56,9 @@ describe("parseAdbDevicesOutput", () => { }, ]); }); +}); +describe("ADB daemon startup notices", () => { it("recognizes daemon startup notices instead of failing the listing", () => { const output = [ "* daemon not running; starting now at tcp:5037", @@ -70,6 +72,67 @@ describe("parseAdbDevicesOutput", () => { expect(devices).toHaveLength(1); }); + it("recognizes daemon startup notices emitted on stderr", () => { + const stdout = [ + "List of devices attached", + "emulator-5554 device transport_id:1", + "", + ].join("\n"); + const stderr = [ + "* daemon not running; starting now at tcp:5037", + "* daemon started successfully", + "", + ].join("\n"); + const { devices, daemonStarted, unparsedLines } = parseAdbDevicesOutput( + stdout, + stderr, + ); + expect(daemonStarted).toBe(true); + expect(unparsedLines).toEqual([]); + expect(devices).toHaveLength(1); + }); + + it("recognizes the legacy daemon notice with its trailing marker and CRLF", () => { + const { daemonStarted, unparsedLines } = parseAdbDevicesOutput( + "List of devices attached\r\n", + "* daemon started successfully *\r\n", + ); + expect(daemonStarted).toBe(true); + expect(unparsedLines).toEqual([]); + }); + + it("does not treat a daemon startup error containing the success phrase as success", () => { + const { daemonStarted, unparsedLines } = parseAdbDevicesOutput( + "List of devices attached\r\n", + "* daemon not started successfully\r\n", + ); + expect(daemonStarted).toBe(false); + expect(unparsedLines).toEqual([]); + }); + + it("ignores non-startup lines and device strings on stderr", () => { + const stdout = [ + "List of devices attached", + "emulator-5554 device transport_id:1", + "", + ].join("\n"); + const stderr = [ + "adb: warning: some transport warning", + "emulator-5556 device transport_id:2", + "", + ].join("\n"); + const { devices, daemonStarted, unparsedLines } = parseAdbDevicesOutput( + stdout, + stderr, + ); + expect(daemonStarted).toBe(false); + expect(unparsedLines).toEqual([]); + expect(devices).toHaveLength(1); + expect(devices[0]?.serial).toBe("emulator-5554"); + }); +}); + +describe("ADB device states", () => { it("keeps non-device states such as unauthorized and offline", () => { const output = [ "List of devices attached", diff --git a/src/adb/AdbDeviceOutput.ts b/src/adb/AdbDeviceOutput.ts index 498d882782..fd6a28e761 100644 --- a/src/adb/AdbDeviceOutput.ts +++ b/src/adb/AdbDeviceOutput.ts @@ -30,6 +30,12 @@ export interface AdbDeviceEntry { const DEVICE_LINE = /^(\S+)\s+(.*)$/u; +const normalizeAdbOutputLine = (rawLine: string): string => rawLine.trimEnd(); + +const isSuccessfulDaemonStartNotice = (line: string): boolean => + line === "* daemon started successfully" || + line === "* daemon started successfully *"; + const metadataValue = (remainder: string, key: string): string | null => { const match = new RegExp(`(?:^|\\s)${key}:(\\S+)`, "u").exec(remainder); return match === null ? null : match[1]!; @@ -42,6 +48,7 @@ const metadataValue = (remainder: string, key: string): string | null => { */ export const parseAdbDevicesOutput = ( stdout: string, + stderr = "", ): { devices: readonly AdbDeviceEntry[]; unparsedLines: readonly string[]; @@ -51,12 +58,13 @@ export const parseAdbDevicesOutput = ( const unparsedLines: string[] = []; let daemonStarted = false; for (const rawLine of stdout.split(/\r?\n/u)) { - const line = rawLine.trimEnd(); - if (line === "" || line.startsWith("List of devices attached")) continue; - if (line.startsWith("* daemon")) { - if (line.includes("started successfully")) daemonStarted = true; + const line = normalizeAdbOutputLine(rawLine); + if (isSuccessfulDaemonStartNotice(line)) { + daemonStarted = true; continue; } + if (line === "" || line.startsWith("List of devices attached")) continue; + if (line.startsWith("* daemon")) continue; const match = DEVICE_LINE.exec(line); const state = ADB_DEVICE_STATES.find((candidate) => { const remainder = match?.[2]; @@ -105,6 +113,15 @@ export const parseAdbDevicesOutput = ( : "none", }); } + if (!daemonStarted && stderr !== "") { + for (const rawLine of stderr.split(/\r?\n/u)) { + const line = normalizeAdbOutputLine(rawLine); + if (isSuccessfulDaemonStartNotice(line)) { + daemonStarted = true; + break; + } + } + } return { devices, unparsedLines, daemonStarted }; }; diff --git a/src/adb/AdbProvider.test.ts b/src/adb/AdbProvider.test.ts index 9c126785c5..c9be8b1760 100644 --- a/src/adb/AdbProvider.test.ts +++ b/src/adb/AdbProvider.test.ts @@ -121,6 +121,36 @@ fi`, ]); }); +it("observes daemon startup when notices are printed to stderr", async () => { + const { binary } = await writeAdbStub( + () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi +if [ "$1" = "devices" ]; then + printf '* daemon not running; starting now at tcp:5037\\n* daemon started successfully\\n' >&2 + printf 'List of devices attached\\n' + printf 'emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1\\n' + exit 0 +fi`, + ); + const result = await evidenceResult(providerOver(binary), { + operation: "list_adb_devices", + input: {}, + }); + expect(result.may_have_started_adb_server).toBe(true); + expect(result.devices).toEqual([ + { + serial: "emulator-5554", + state: "device", + transport: "tcp", + product: "sdk_gphone64_x86_64", + model: "sdk_gphone64_x86_64", + device: "emu64xa", + transport_id: 1, + kind: "emulator", + kind_basis: "emulator_serial_prefix", + }, + ]); +}); + it("refuses device listings with an invalid state token", async () => { const malformedLine = "serial deviceXYZ "; const { binary } = await writeAdbStub( diff --git a/src/adb/AdbProvider.ts b/src/adb/AdbProvider.ts index b7bc545ea4..881842642d 100644 --- a/src/adb/AdbProvider.ts +++ b/src/adb/AdbProvider.ts @@ -755,6 +755,7 @@ export class AdbProvider { if (!ran.ok) return ran; const { devices, unparsedLines, daemonStarted } = parseAdbDevicesOutput( ran.value.stdout, + ran.value.stderr, ); if (unparsedLines.length > 0) return err(