fix(adb): parse successful daemon notices from both output streams

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>
This commit is contained in:
morluto committed 2026-10-14 16:10:46 +08:00
1 parent 187f113f79
commit 57bb7ec5ca
4 files changed
+115 -4

No files matched your search

+63
View File
@@ -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",
+21 -4
View File
@@ -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 };
};
+30
View File
@@ -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(
+1
View File
@@ -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(