diff --git a/src/adb/AdbDeviceOutput.test.ts b/src/adb/AdbDeviceOutput.test.ts index d2a32a8550..bd93598a99 100644 --- a/src/adb/AdbDeviceOutput.test.ts +++ b/src/adb/AdbDeviceOutput.test.ts @@ -168,6 +168,47 @@ describe("ADB device states", () => { }); }); + it.each([ + [ + "without a reason", + "no permissions; see [http://developer.android.com/tools/device.html]", + ], + [ + "with a reason", + "no permissions (user alice is not in the plugdev group); see [http://developer.android.com/tools/device.html]", + ], + ])("parses adb's no-permissions help text %s", (_name, stateText) => { + const { devices, unparsedLines } = parseAdbDevicesOutput( + [ + "List of devices attached", + `07ef9c2a ${stateText} usb:1-1 transport_id:4`, + "emulator-5554 device product:sdk model:sdk device:emu transport_id:1", + "", + ].join("\n"), + ); + expect(unparsedLines).toEqual({ sample: [], total: 0 }); + expect(devices.map(({ serial, state }) => [serial, state])).toEqual([ + ["07ef9c2a", "no permissions"], + ["emulator-5554", "device"], + ]); + expect(devices[0]).toMatchObject({ + transport: "usb", + product: null, + model: null, + device: null, + transport_id: 4, + kind: "physical", + }); + }); + + it("rejects a semicolon after any state other than no permissions", () => { + const { devices, unparsedLines } = parseAdbDevicesOutput( + "serial-bad-device device; see [url] transport_id:1\n", + ); + expect(devices).toEqual([]); + expect(unparsedLines.total).toBe(1); + }); + it("returns unrecognized lines instead of dropping them", () => { const output = [ "List of devices attached", diff --git a/src/adb/AdbDeviceOutput.ts b/src/adb/AdbDeviceOutput.ts index 4892e2860f..2dfda6e0b3 100644 --- a/src/adb/AdbDeviceOutput.ts +++ b/src/adb/AdbDeviceOutput.ts @@ -97,7 +97,14 @@ export const parseAdbDevicesOutput = ( if (remainder === undefined || !remainder.startsWith(candidate)) return false; const next = remainder[candidate.length]; - return next === undefined || /\s/u.test(next); + // adb prints no-permissions as `no permissions[ (reason)]; see [URL]` and + // omits the reason on a host where it finds no udev problem to name, + // such as one without a plugdev group, so `;` can follow it directly. + return ( + next === undefined || + /\s/u.test(next) || + (candidate === "no permissions" && next === ";") + ); }); if (match === null || state === undefined) { if (rawLine.trim() !== "") unparsed.add(rawLine); diff --git a/src/adb/AdbProvider.test.ts b/src/adb/AdbProvider.test.ts index 4c4aed2f87..40bc2951fc 100644 --- a/src/adb/AdbProvider.test.ts +++ b/src/adb/AdbProvider.test.ts @@ -239,6 +239,36 @@ fi`, expect(result.error.message).toContain(malformedLine); }); +it("lists a USB device adb has no permission to open alongside online devices", async () => { + const { binary } = await writeAdbStub( + () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi +if [ "$1" = "devices" ]; then + printf 'List of devices attached\\n' + printf '07ef9c2a no permissions; see [http://developer.android.com/tools/device.html] usb:1-1 transport_id:4\\n' + printf 'emulator-5554 device transport_id:1\\n' + exit 0 +fi`, + ); + const result = await evidenceResult(providerOver(binary), { + operation: "list_adb_devices", + input: {}, + }); + expect(result.devices).toEqual([ + { + serial: "07ef9c2a", + state: "no permissions", + transport: "usb", + product: null, + model: null, + device: null, + transport_id: 4, + kind: "physical", + kind_basis: "usb_transport", + }, + expect.objectContaining({ serial: "emulator-5554", state: "device" }), + ]); +}); + it("lists rescue and detached transports alongside online devices", async () => { const { binary } = await writeAdbStub( () => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi