mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 01:39:21 +02:00
feat(adb0daemon-webusb): accept exclusionFilters in getDevices and DeviceObserver
This commit is contained in:
parent
ea5002bc87
commit
c68e216613
6 changed files with 190 additions and 101 deletions
5
.changeset/cyan-readers-fail.md
Normal file
5
.changeset/cyan-readers-fail.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@yume-chan/adb-daemon-webusb": patch
|
||||
---
|
||||
|
||||
Accept exclusionFilters in getDevices and DeviceObserver
|
|
@ -22,39 +22,31 @@ import {
|
|||
import type { ExactReadable } from "@yume-chan/struct";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
|
||||
import type { UsbInterfaceFilter } from "./utils.js";
|
||||
import {
|
||||
findUsbAlternateInterface,
|
||||
findUsbEndpoints,
|
||||
getSerialNumber,
|
||||
isErrorName,
|
||||
} from "./utils.js";
|
||||
import type { UsbInterfaceFilter, UsbInterfaceIdentifier } from "./utils.js";
|
||||
import { findUsbEndpoints, getSerialNumber, isErrorName } from "./utils.js";
|
||||
|
||||
/**
|
||||
* The default filter for ADB devices, as defined by Google.
|
||||
*/
|
||||
export const ADB_DEFAULT_INTERFACE_FILTER = {
|
||||
export const AdbDefaultInterfaceFilter = {
|
||||
classCode: 0xff,
|
||||
subclassCode: 0x42,
|
||||
protocolCode: 1,
|
||||
} as const satisfies UsbInterfaceFilter;
|
||||
|
||||
export function toAdbDeviceFilters(
|
||||
export function mergeDefaultAdbInterfaceFilter(
|
||||
filters: USBDeviceFilter[] | undefined,
|
||||
): (USBDeviceFilter & UsbInterfaceFilter)[] {
|
||||
if (!filters || filters.length === 0) {
|
||||
return [ADB_DEFAULT_INTERFACE_FILTER];
|
||||
return [AdbDefaultInterfaceFilter];
|
||||
} else {
|
||||
return filters.map((filter) => ({
|
||||
...filter,
|
||||
classCode:
|
||||
filter.classCode ?? ADB_DEFAULT_INTERFACE_FILTER.classCode,
|
||||
classCode: filter.classCode ?? AdbDefaultInterfaceFilter.classCode,
|
||||
subclassCode:
|
||||
filter.subclassCode ??
|
||||
ADB_DEFAULT_INTERFACE_FILTER.subclassCode,
|
||||
filter.subclassCode ?? AdbDefaultInterfaceFilter.subclassCode,
|
||||
protocolCode:
|
||||
filter.protocolCode ??
|
||||
ADB_DEFAULT_INTERFACE_FILTER.protocolCode,
|
||||
filter.protocolCode ?? AdbDefaultInterfaceFilter.protocolCode,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -262,7 +254,7 @@ export class AdbDaemonWebUsbConnection
|
|||
}
|
||||
|
||||
export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
||||
#filters: UsbInterfaceFilter[];
|
||||
#interface: UsbInterfaceIdentifier;
|
||||
#usbManager: USB;
|
||||
|
||||
#raw: USBDevice;
|
||||
|
@ -287,22 +279,24 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
|||
*/
|
||||
constructor(
|
||||
device: USBDevice,
|
||||
filters: UsbInterfaceFilter[],
|
||||
interface_: UsbInterfaceIdentifier,
|
||||
usbManager: USB,
|
||||
) {
|
||||
this.#raw = device;
|
||||
this.#serial = getSerialNumber(device);
|
||||
this.#filters = filters;
|
||||
this.#interface = interface_;
|
||||
this.#usbManager = usbManager;
|
||||
}
|
||||
|
||||
async #claimInterface(): Promise<[USBEndpoint, USBEndpoint]> {
|
||||
async #claimInterface(): Promise<{
|
||||
inEndpoint: USBEndpoint;
|
||||
outEndpoint: USBEndpoint;
|
||||
}> {
|
||||
if (!this.#raw.opened) {
|
||||
await this.#raw.open();
|
||||
}
|
||||
|
||||
const { configuration, interface_, alternate } =
|
||||
findUsbAlternateInterface(this.#raw, this.#filters);
|
||||
const { configuration, interface_, alternate } = this.#interface;
|
||||
|
||||
if (
|
||||
this.#raw.configuration?.configurationValue !==
|
||||
|
@ -336,17 +330,14 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
|||
);
|
||||
}
|
||||
|
||||
const { inEndpoint, outEndpoint } = findUsbEndpoints(
|
||||
alternate.endpoints,
|
||||
);
|
||||
return [inEndpoint, outEndpoint];
|
||||
return findUsbEndpoints(alternate.endpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the device and create a new connection to the ADB Daemon.
|
||||
*/
|
||||
async connect(): Promise<AdbDaemonWebUsbConnection> {
|
||||
const [inEndpoint, outEndpoint] = await this.#claimInterface();
|
||||
const { inEndpoint, outEndpoint } = await this.#claimInterface();
|
||||
return new AdbDaemonWebUsbConnection(
|
||||
this,
|
||||
inEndpoint,
|
||||
|
|
|
@ -3,10 +3,7 @@
|
|||
import * as assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
|
||||
import {
|
||||
ADB_DEFAULT_INTERFACE_FILTER,
|
||||
AdbDaemonWebUsbDevice,
|
||||
} from "./device.js";
|
||||
import { AdbDaemonWebUsbDevice, AdbDefaultInterfaceFilter } from "./device.js";
|
||||
import { AdbDaemonWebUsbDeviceManager } from "./manager.js";
|
||||
|
||||
class MockUsb implements USB {
|
||||
|
@ -69,7 +66,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
|
|||
assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
|
||||
assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
|
||||
{
|
||||
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||
filters: [AdbDefaultInterfaceFilter],
|
||||
exclusionFilters: undefined,
|
||||
},
|
||||
]);
|
||||
|
@ -85,7 +82,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
|
|||
assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
|
||||
assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
|
||||
{
|
||||
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||
filters: [AdbDefaultInterfaceFilter],
|
||||
exclusionFilters: undefined,
|
||||
},
|
||||
]);
|
||||
|
@ -101,7 +98,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
|
|||
assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
|
||||
assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
|
||||
{
|
||||
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||
filters: [AdbDefaultInterfaceFilter],
|
||||
exclusionFilters: undefined,
|
||||
},
|
||||
]);
|
||||
|
@ -117,7 +114,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
|
|||
assert.strictEqual(usb.requestDevice.mock.callCount(), 1);
|
||||
assert.deepEqual(usb.requestDevice.mock.calls[0]?.arguments, [
|
||||
{
|
||||
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||
filters: [AdbDefaultInterfaceFilter],
|
||||
exclusionFilters: undefined,
|
||||
},
|
||||
]);
|
||||
|
@ -136,7 +133,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
|
|||
{
|
||||
filters: [
|
||||
{
|
||||
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||
...AdbDefaultInterfaceFilter,
|
||||
...filter,
|
||||
},
|
||||
],
|
||||
|
@ -162,7 +159,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
|
|||
filters: [
|
||||
{
|
||||
...filter,
|
||||
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||
...AdbDefaultInterfaceFilter,
|
||||
},
|
||||
],
|
||||
exclusionFilters: undefined,
|
||||
|
@ -185,11 +182,11 @@ describe("AdbDaemonWebUsbDeviceManager", () => {
|
|||
{
|
||||
filters: [
|
||||
{
|
||||
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||
...AdbDefaultInterfaceFilter,
|
||||
...filter1,
|
||||
},
|
||||
{
|
||||
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||
...AdbDefaultInterfaceFilter,
|
||||
...filter2,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { AdbDaemonWebUsbDevice, toAdbDeviceFilters } from "./device.js";
|
||||
import {
|
||||
AdbDaemonWebUsbDevice,
|
||||
mergeDefaultAdbInterfaceFilter,
|
||||
} from "./device.js";
|
||||
import { AdbDaemonWebUsbDeviceObserver } from "./observer.js";
|
||||
import { isErrorName, matchesFilters } from "./utils.js";
|
||||
import { isErrorName, matchFilters } from "./utils.js";
|
||||
|
||||
export namespace AdbDaemonWebUsbDeviceManager {
|
||||
export interface RequestDeviceOptions {
|
||||
|
@ -37,14 +40,30 @@ export class AdbDaemonWebUsbDeviceManager {
|
|||
async requestDevice(
|
||||
options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
|
||||
): Promise<AdbDaemonWebUsbDevice | undefined> {
|
||||
const filters = toAdbDeviceFilters(options.filters);
|
||||
const filters = mergeDefaultAdbInterfaceFilter(options.filters);
|
||||
|
||||
try {
|
||||
const device = await this.#usbManager.requestDevice({
|
||||
filters,
|
||||
exclusionFilters: options.exclusionFilters,
|
||||
});
|
||||
return new AdbDaemonWebUsbDevice(device, filters, this.#usbManager);
|
||||
|
||||
const interface_ = matchFilters(
|
||||
device,
|
||||
filters,
|
||||
options.exclusionFilters,
|
||||
);
|
||||
if (!interface_) {
|
||||
// `#usbManager` doesn't support `exclusionFilters`,
|
||||
// selected device is invalid
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new AdbDaemonWebUsbDevice(
|
||||
device,
|
||||
interface_,
|
||||
this.#usbManager,
|
||||
);
|
||||
} catch (e) {
|
||||
// No device selected
|
||||
if (isErrorName(e, "NotFoundError")) {
|
||||
|
@ -58,26 +77,37 @@ export class AdbDaemonWebUsbDeviceManager {
|
|||
/**
|
||||
* Get all connected and requested devices that match the specified filters.
|
||||
*/
|
||||
getDevices(filters?: USBDeviceFilter[]): Promise<AdbDaemonWebUsbDevice[]>;
|
||||
async getDevices(
|
||||
filters_: USBDeviceFilter[] | undefined,
|
||||
options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
|
||||
): Promise<AdbDaemonWebUsbDevice[]> {
|
||||
const filters = toAdbDeviceFilters(filters_);
|
||||
const filters = mergeDefaultAdbInterfaceFilter(options.filters);
|
||||
|
||||
const devices = await this.#usbManager.getDevices();
|
||||
return devices
|
||||
.filter((device) => matchesFilters(device, filters))
|
||||
.map(
|
||||
(device) =>
|
||||
// filter map
|
||||
const result: AdbDaemonWebUsbDevice[] = [];
|
||||
for (const device of devices) {
|
||||
const interface_ = matchFilters(
|
||||
device,
|
||||
filters,
|
||||
options.exclusionFilters,
|
||||
);
|
||||
if (interface_) {
|
||||
result.push(
|
||||
new AdbDaemonWebUsbDevice(
|
||||
device,
|
||||
filters,
|
||||
interface_,
|
||||
this.#usbManager,
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
trackDevices(filters?: USBDeviceFilter[]): AdbDaemonWebUsbDeviceObserver {
|
||||
return new AdbDaemonWebUsbDeviceObserver(this.#usbManager, filters);
|
||||
trackDevices(
|
||||
options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
|
||||
): AdbDaemonWebUsbDeviceObserver {
|
||||
return new AdbDaemonWebUsbDeviceObserver(this.#usbManager, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import type { DeviceObserver } from "@yume-chan/adb";
|
||||
import { EventEmitter } from "@yume-chan/event";
|
||||
|
||||
import { AdbDaemonWebUsbDevice, toAdbDeviceFilters } from "./device.js";
|
||||
import {
|
||||
AdbDaemonWebUsbDevice,
|
||||
mergeDefaultAdbInterfaceFilter,
|
||||
} from "./device.js";
|
||||
import type { AdbDaemonWebUsbDeviceManager } from "./manager.js";
|
||||
import type { UsbInterfaceFilter } from "./utils.js";
|
||||
import { matchesFilters } from "./utils.js";
|
||||
import { matchFilters } from "./utils.js";
|
||||
|
||||
/**
|
||||
* A watcher that listens for new WebUSB devices and notifies the callback when
|
||||
|
@ -12,7 +16,8 @@ import { matchesFilters } from "./utils.js";
|
|||
export class AdbDaemonWebUsbDeviceObserver
|
||||
implements DeviceObserver<AdbDaemonWebUsbDevice>
|
||||
{
|
||||
#filters: UsbInterfaceFilter[];
|
||||
#filters: (USBDeviceFilter & UsbInterfaceFilter)[];
|
||||
#exclusionFilters?: USBDeviceFilter[] | undefined;
|
||||
#usbManager: USB;
|
||||
|
||||
#onError = new EventEmitter<Error>();
|
||||
|
@ -29,8 +34,12 @@ export class AdbDaemonWebUsbDeviceObserver
|
|||
|
||||
current: AdbDaemonWebUsbDevice[] = [];
|
||||
|
||||
constructor(usb: USB, filters?: USBDeviceFilter[]) {
|
||||
this.#filters = toAdbDeviceFilters(filters);
|
||||
constructor(
|
||||
usb: USB,
|
||||
options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
|
||||
) {
|
||||
this.#filters = mergeDefaultAdbInterfaceFilter(options.filters);
|
||||
this.#exclusionFilters = options.exclusionFilters;
|
||||
this.#usbManager = usb;
|
||||
|
||||
this.#usbManager.addEventListener("connect", this.#handleConnect);
|
||||
|
@ -38,13 +47,18 @@ export class AdbDaemonWebUsbDeviceObserver
|
|||
}
|
||||
|
||||
#handleConnect = (e: USBConnectionEvent) => {
|
||||
if (!matchesFilters(e.device, this.#filters)) {
|
||||
const interface_ = matchFilters(
|
||||
e.device,
|
||||
this.#filters,
|
||||
this.#exclusionFilters,
|
||||
);
|
||||
if (!interface_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = new AdbDaemonWebUsbDevice(
|
||||
e.device,
|
||||
this.#filters,
|
||||
interface_,
|
||||
this.#usbManager,
|
||||
);
|
||||
this.#onDeviceAdd.fire([device]);
|
||||
|
@ -71,5 +85,10 @@ export class AdbDaemonWebUsbDeviceObserver
|
|||
"disconnect",
|
||||
this.#handleDisconnect,
|
||||
);
|
||||
|
||||
this.#onError.dispose();
|
||||
this.#onDeviceAdd.dispose();
|
||||
this.#onDeviceRemove.dispose();
|
||||
this.#onListChange.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,33 +20,47 @@ export type UsbInterfaceFilter = PickNonNullable<
|
|||
"classCode" | "subclassCode" | "protocolCode"
|
||||
>;
|
||||
|
||||
function alternateMatchesFilter(
|
||||
alternate: USBAlternateInterface,
|
||||
filters: UsbInterfaceFilter[],
|
||||
) {
|
||||
return filters.some(
|
||||
(filter) =>
|
||||
alternate.interfaceClass === filter.classCode &&
|
||||
alternate.interfaceSubclass === filter.subclassCode &&
|
||||
alternate.interfaceProtocol === filter.protocolCode,
|
||||
export function isUsbInterfaceFilter(
|
||||
filter: USBDeviceFilter,
|
||||
): filter is UsbInterfaceFilter {
|
||||
return (
|
||||
filter.classCode !== undefined &&
|
||||
filter.subclassCode !== undefined &&
|
||||
filter.protocolCode !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function findUsbAlternateInterface(
|
||||
device: USBDevice,
|
||||
filters: UsbInterfaceFilter[],
|
||||
function matchUsbInterfaceFilter(
|
||||
alternate: USBAlternateInterface,
|
||||
filter: UsbInterfaceFilter,
|
||||
) {
|
||||
return (
|
||||
alternate.interfaceClass === filter.classCode &&
|
||||
alternate.interfaceSubclass === filter.subclassCode &&
|
||||
alternate.interfaceProtocol === filter.protocolCode
|
||||
);
|
||||
}
|
||||
|
||||
export interface UsbInterfaceIdentifier {
|
||||
configuration: USBConfiguration;
|
||||
interface_: USBInterface;
|
||||
alternate: USBAlternateInterface;
|
||||
}
|
||||
|
||||
export function findUsbInterface(
|
||||
device: USBDevice,
|
||||
filter: UsbInterfaceFilter,
|
||||
): UsbInterfaceIdentifier | undefined {
|
||||
for (const configuration of device.configurations) {
|
||||
for (const interface_ of configuration.interfaces) {
|
||||
for (const alternate of interface_.alternates) {
|
||||
if (alternateMatchesFilter(alternate, filters)) {
|
||||
if (matchUsbInterfaceFilter(alternate, filter)) {
|
||||
return { configuration, interface_, alternate };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError("No matched alternate interface found");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function padNumber(value: number) {
|
||||
|
@ -100,35 +114,68 @@ export function findUsbEndpoints(endpoints: USBEndpoint[]) {
|
|||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
export function matchesFilters(
|
||||
export function matchFilter(
|
||||
device: USBDevice,
|
||||
filter: USBDeviceFilter & UsbInterfaceFilter,
|
||||
): UsbInterfaceIdentifier | false;
|
||||
export function matchFilter(
|
||||
device: USBDevice,
|
||||
filter: USBDeviceFilter,
|
||||
): boolean;
|
||||
export function matchFilter(
|
||||
device: USBDevice,
|
||||
filter: USBDeviceFilter,
|
||||
): UsbInterfaceIdentifier | boolean {
|
||||
if (filter.vendorId !== undefined && device.vendorId !== filter.vendorId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filter.productId !== undefined &&
|
||||
device.productId !== filter.productId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filter.serialNumber !== undefined &&
|
||||
getSerialNumber(device) !== filter.serialNumber
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isUsbInterfaceFilter(filter)) {
|
||||
return findUsbInterface(device, filter) || false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function matchFilters(
|
||||
device: USBDevice,
|
||||
filters: (USBDeviceFilter & UsbInterfaceFilter)[],
|
||||
) {
|
||||
for (const filter of filters) {
|
||||
if (
|
||||
filter.vendorId !== undefined &&
|
||||
device.vendorId !== filter.vendorId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
filter.productId !== undefined &&
|
||||
device.productId !== filter.productId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
filter.serialNumber !== undefined &&
|
||||
getSerialNumber(device) !== filter.serialNumber
|
||||
) {
|
||||
continue;
|
||||
exclusionFilters?: USBDeviceFilter[],
|
||||
): UsbInterfaceIdentifier | false;
|
||||
export function matchFilters(
|
||||
device: USBDevice,
|
||||
filters: USBDeviceFilter[],
|
||||
exclusionFilters?: USBDeviceFilter[],
|
||||
): boolean;
|
||||
export function matchFilters(
|
||||
device: USBDevice,
|
||||
filters: USBDeviceFilter[],
|
||||
exclusionFilters?: USBDeviceFilter[],
|
||||
): UsbInterfaceIdentifier | boolean {
|
||||
if (exclusionFilters && exclusionFilters.length > 0) {
|
||||
if (matchFilters(device, exclusionFilters)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
findUsbAlternateInterface(device, filters);
|
||||
return true;
|
||||
} catch {
|
||||
continue;
|
||||
for (const filter of filters) {
|
||||
const result = matchFilter(device, filter);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue