mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 01:39:21 +02:00
feat(webusb): allow filters
to be empty arrays
This commit is contained in:
parent
10ed1848f5
commit
af0acb577a
15 changed files with 321 additions and 82 deletions
15
common/config/rush/pnpm-lock.yaml
generated
15
common/config/rush/pnpm-lock.yaml
generated
|
@ -130,15 +130,30 @@ importers:
|
||||||
specifier: workspace:^0.0.24
|
specifier: workspace:^0.0.24
|
||||||
version: link:../struct
|
version: link:../struct
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@jest/globals':
|
||||||
|
specifier: ^30.0.0-alpha.4
|
||||||
|
version: 30.0.0-alpha.5
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.14.9
|
||||||
|
version: 20.14.9
|
||||||
'@yume-chan/eslint-config':
|
'@yume-chan/eslint-config':
|
||||||
specifier: workspace:^1.0.0
|
specifier: workspace:^1.0.0
|
||||||
version: link:../../toolchain/eslint-config
|
version: link:../../toolchain/eslint-config
|
||||||
'@yume-chan/tsconfig':
|
'@yume-chan/tsconfig':
|
||||||
specifier: workspace:^1.0.0
|
specifier: workspace:^1.0.0
|
||||||
version: link:../../toolchain/tsconfig
|
version: link:../../toolchain/tsconfig
|
||||||
|
cross-env:
|
||||||
|
specifier: ^7.0.3
|
||||||
|
version: 7.0.3
|
||||||
|
jest:
|
||||||
|
specifier: ^30.0.0-alpha.4
|
||||||
|
version: 30.0.0-alpha.5(@types/node@20.14.9)
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
|
ts-jest:
|
||||||
|
specifier: ^29.1.5
|
||||||
|
version: 29.1.5(@babel/core@7.24.7)(@jest/types@29.6.3)(jest@30.0.0-alpha.5)(typescript@5.5.2)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.2
|
specifier: ^5.5.2
|
||||||
version: 5.5.2
|
version: 5.5.2
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||||
{
|
{
|
||||||
"pnpmShrinkwrapHash": "4268d369f3d5b02e089540e98cb1fa9074cba8c9",
|
"pnpmShrinkwrapHash": "e045703b662b53e585b409888c662653aa4196da",
|
||||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,11 @@
|
||||||
<a href="https://github.com/yume-chan/ya-webadb/releases">
|
<a href="https://github.com/yume-chan/ya-webadb/releases">
|
||||||
<img alt="GitHub release" src="https://img.shields.io/github/v/release/yume-chan/ya-webadb?logo=github">
|
<img alt="GitHub release" src="https://img.shields.io/github/v/release/yume-chan/ya-webadb?logo=github">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://bundlephobia.com/package/@yume-chan/adb-credential-web">
|
||||||
|
<img alt="Package Size" src="https://img.shields.io/bundlephobia/minzip/%40yume-chan%2Fadb-credential-web">
|
||||||
|
</a>
|
||||||
<a href="https://www.npmjs.com/package/@yume-chan/adb">
|
<a href="https://www.npmjs.com/package/@yume-chan/adb">
|
||||||
<img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/adb?logo=npm">
|
<img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/adb-credential-web?logo=npm">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/26k3ttC2PN">
|
<a href="https://discord.gg/26k3ttC2PN">
|
||||||
<img alt="Discord" src="https://img.shields.io/discord/1120215514732564502?logo=discord&logoColor=%23ffffff&label=Discord">
|
<img alt="Discord" src="https://img.shields.io/discord/1120215514732564502?logo=discord&logoColor=%23ffffff&label=Discord">
|
||||||
|
|
|
@ -15,8 +15,11 @@
|
||||||
<a href="https://github.com/yume-chan/ya-webadb/releases">
|
<a href="https://github.com/yume-chan/ya-webadb/releases">
|
||||||
<img alt="GitHub release" src="https://img.shields.io/github/v/release/yume-chan/ya-webadb?logo=github">
|
<img alt="GitHub release" src="https://img.shields.io/github/v/release/yume-chan/ya-webadb?logo=github">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.npmjs.com/package/@yume-chan/adb">
|
<a href="https://bundlephobia.com/package/@yume-chan/adb-daemon-webusb">
|
||||||
<img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/adb?logo=npm">
|
<img alt="Package Size" src="https://img.shields.io/bundlephobia/minzip/%40yume-chan%2Fadb-daemon-webusb">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/@yume-chan/adb-daemon-webusb">
|
||||||
|
<img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/adb-daemon-webusb?logo=npm">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/26k3ttC2PN">
|
<a href="https://discord.gg/26k3ttC2PN">
|
||||||
<img alt="Discord" src="https://img.shields.io/discord/1120215514732564502?logo=discord&logoColor=%23ffffff&label=Discord">
|
<img alt="Discord" src="https://img.shields.io/discord/1120215514732564502?logo=discord&logoColor=%23ffffff&label=Discord">
|
||||||
|
|
14
libraries/adb-daemon-webusb/jest.config.js
Normal file
14
libraries/adb-daemon-webusb/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
export default {
|
||||||
|
preset: "ts-jest/presets/default-esm",
|
||||||
|
extensionsToTreatAsEsm: [".ts"],
|
||||||
|
transform: {
|
||||||
|
"^.+\\.tsx?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{ tsconfig: "tsconfig.test.json", useESM: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||||
|
},
|
||||||
|
};
|
|
@ -28,7 +28,8 @@
|
||||||
"build": "tsc -b tsconfig.build.json",
|
"build": "tsc -b tsconfig.build.json",
|
||||||
"build:watch": "tsc -b tsconfig.build.json",
|
"build:watch": "tsc -b tsconfig.build.json",
|
||||||
"lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4",
|
"lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build",
|
||||||
|
"test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" TS_JEST_DISABLE_VER_CHECKER=true jest --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/w3c-web-usb": "^1.0.10",
|
"@types/w3c-web-usb": "^1.0.10",
|
||||||
|
@ -37,9 +38,14 @@
|
||||||
"@yume-chan/struct": "workspace:^0.0.24"
|
"@yume-chan/struct": "workspace:^0.0.24"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^30.0.0-alpha.4",
|
||||||
|
"@types/node": "^20.14.9",
|
||||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||||
"@yume-chan/tsconfig": "workspace:^1.0.0",
|
"@yume-chan/tsconfig": "workspace:^1.0.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"jest": "^30.0.0-alpha.4",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
|
"ts-jest": "^29.1.5",
|
||||||
"typescript": "^5.5.2"
|
"typescript": "^5.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,10 @@ import {
|
||||||
import type { ExactReadable } from "@yume-chan/struct";
|
import type { ExactReadable } from "@yume-chan/struct";
|
||||||
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
|
||||||
|
|
||||||
import type { AdbDeviceFilter } from "./utils.js";
|
import type { UsbInterfaceFilter } from "./utils.js";
|
||||||
import {
|
import {
|
||||||
findUsbAlternateInterface,
|
findUsbAlternateInterface,
|
||||||
|
findUsbEndpoints,
|
||||||
getSerialNumber,
|
getSerialNumber,
|
||||||
isErrorName,
|
isErrorName,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
|
@ -32,49 +33,30 @@ import {
|
||||||
/**
|
/**
|
||||||
* The default filter for ADB devices, as defined by Google.
|
* The default filter for ADB devices, as defined by Google.
|
||||||
*/
|
*/
|
||||||
export const ADB_DEFAULT_DEVICE_FILTER = {
|
export const ADB_DEFAULT_INTERFACE_FILTER = {
|
||||||
classCode: 0xff,
|
classCode: 0xff,
|
||||||
subclassCode: 0x42,
|
subclassCode: 0x42,
|
||||||
protocolCode: 1,
|
protocolCode: 1,
|
||||||
} as const satisfies AdbDeviceFilter;
|
} as const satisfies UsbInterfaceFilter;
|
||||||
|
|
||||||
/**
|
export function toAdbDeviceFilters(
|
||||||
* Find the first pair of input and output endpoints from an alternate interface.
|
filters: USBDeviceFilter[] | undefined,
|
||||||
*
|
): (USBDeviceFilter & UsbInterfaceFilter)[] {
|
||||||
* ADB interface only has two endpoints, one for input and one for output.
|
if (!filters || filters.length === 0) {
|
||||||
*/
|
return [ADB_DEFAULT_INTERFACE_FILTER];
|
||||||
function findUsbEndpoints(endpoints: USBEndpoint[]) {
|
} else {
|
||||||
if (endpoints.length === 0) {
|
return filters.map((filter) => ({
|
||||||
throw new TypeError("No endpoints given");
|
...filter,
|
||||||
|
classCode:
|
||||||
|
filter.classCode ?? ADB_DEFAULT_INTERFACE_FILTER.classCode,
|
||||||
|
subclassCode:
|
||||||
|
filter.subclassCode ??
|
||||||
|
ADB_DEFAULT_INTERFACE_FILTER.subclassCode,
|
||||||
|
protocolCode:
|
||||||
|
filter.protocolCode ??
|
||||||
|
ADB_DEFAULT_INTERFACE_FILTER.protocolCode,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let inEndpoint: USBEndpoint | undefined;
|
|
||||||
let outEndpoint: USBEndpoint | undefined;
|
|
||||||
|
|
||||||
for (const endpoint of endpoints) {
|
|
||||||
switch (endpoint.direction) {
|
|
||||||
case "in":
|
|
||||||
inEndpoint = endpoint;
|
|
||||||
if (outEndpoint) {
|
|
||||||
return { inEndpoint, outEndpoint };
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "out":
|
|
||||||
outEndpoint = endpoint;
|
|
||||||
if (inEndpoint) {
|
|
||||||
return { inEndpoint, outEndpoint };
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inEndpoint) {
|
|
||||||
throw new TypeError("No input endpoint found.");
|
|
||||||
}
|
|
||||||
if (!outEndpoint) {
|
|
||||||
throw new TypeError("No output endpoint found.");
|
|
||||||
}
|
|
||||||
throw new Error("unreachable");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Uint8ArrayExactReadable implements ExactReadable {
|
class Uint8ArrayExactReadable implements ExactReadable {
|
||||||
|
@ -282,7 +264,7 @@ export class AdbDaemonWebUsbConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
||||||
#filters: AdbDeviceFilter[];
|
#filters: UsbInterfaceFilter[];
|
||||||
#usbManager: USB;
|
#usbManager: USB;
|
||||||
|
|
||||||
#raw: USBDevice;
|
#raw: USBDevice;
|
||||||
|
@ -307,7 +289,7 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
device: USBDevice,
|
device: USBDevice,
|
||||||
filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
|
filters: UsbInterfaceFilter[],
|
||||||
usbManager: USB,
|
usbManager: USB,
|
||||||
) {
|
) {
|
||||||
this.#raw = device;
|
this.#raw = device;
|
||||||
|
|
167
libraries/adb-daemon-webusb/src/manager.spec.ts
Normal file
167
libraries/adb-daemon-webusb/src/manager.spec.ts
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/* eslint-disable @typescript-eslint/require-await */
|
||||||
|
import { describe, expect, it, jest } from "@jest/globals";
|
||||||
|
|
||||||
|
import { ADB_DEFAULT_INTERFACE_FILTER, AdbDaemonWebUsbDevice } from "./device";
|
||||||
|
import { AdbDaemonWebUsbDeviceManager } from "./manager.js";
|
||||||
|
|
||||||
|
class MockUsb implements USB {
|
||||||
|
onconnect: (ev: USBConnectionEvent) => void = jest.fn();
|
||||||
|
ondisconnect: (ev: USBConnectionEvent) => void = jest.fn();
|
||||||
|
|
||||||
|
getDevices: () => Promise<USBDevice[]> = jest.fn(async () => []);
|
||||||
|
requestDevice: (options?: USBDeviceRequestOptions) => Promise<USBDevice> =
|
||||||
|
jest.fn(async () => ({ serialNumber: "abcdefgh" }) as never);
|
||||||
|
|
||||||
|
addEventListener(
|
||||||
|
type: "connect" | "disconnect",
|
||||||
|
listener: (this: this, ev: USBConnectionEvent) => void,
|
||||||
|
useCapture?: boolean,
|
||||||
|
): void;
|
||||||
|
addEventListener(
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject | null,
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void;
|
||||||
|
addEventListener(
|
||||||
|
_type: unknown,
|
||||||
|
_listener: unknown,
|
||||||
|
_options?: unknown,
|
||||||
|
): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
removeEventListener(
|
||||||
|
type: "connect" | "disconnect",
|
||||||
|
callback: (this: this, ev: USBConnectionEvent) => void,
|
||||||
|
useCapture?: boolean,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject | null,
|
||||||
|
options?: EventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
_type: unknown,
|
||||||
|
_callback: unknown,
|
||||||
|
_options?: unknown,
|
||||||
|
): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
dispatchEvent(_event: Event): boolean {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("AdbDaemonWebUsbDeviceManager", () => {
|
||||||
|
describe("requestDevice", () => {
|
||||||
|
it("should accept 0 args", async () => {
|
||||||
|
const usb = new MockUsb();
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(usb);
|
||||||
|
await expect(manager.requestDevice()).resolves.toBeInstanceOf(
|
||||||
|
AdbDaemonWebUsbDevice,
|
||||||
|
);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledWith({
|
||||||
|
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept undefined filters", async () => {
|
||||||
|
const usb = new MockUsb();
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(usb);
|
||||||
|
await expect(
|
||||||
|
manager.requestDevice({ filters: undefined }),
|
||||||
|
).resolves.toBeInstanceOf(AdbDaemonWebUsbDevice);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledWith({
|
||||||
|
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept empty filters array", async () => {
|
||||||
|
const usb = new MockUsb();
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(usb);
|
||||||
|
await expect(
|
||||||
|
manager.requestDevice({ filters: [] }),
|
||||||
|
).resolves.toBeInstanceOf(AdbDaemonWebUsbDevice);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledWith({
|
||||||
|
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept empty filter object", async () => {
|
||||||
|
const usb = new MockUsb();
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(usb);
|
||||||
|
await expect(
|
||||||
|
manager.requestDevice({ filters: [{}] }),
|
||||||
|
).resolves.toBeInstanceOf(AdbDaemonWebUsbDevice);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledWith({
|
||||||
|
filters: [ADB_DEFAULT_INTERFACE_FILTER],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge missing fields with default values", async () => {
|
||||||
|
const usb = new MockUsb();
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(usb);
|
||||||
|
const filter: USBDeviceFilter = { vendorId: 0x1234 };
|
||||||
|
await expect(
|
||||||
|
manager.requestDevice({ filters: [filter] }),
|
||||||
|
).resolves.toBeInstanceOf(AdbDaemonWebUsbDevice);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledWith({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||||
|
...filter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge undefined fields with default values", async () => {
|
||||||
|
const usb = new MockUsb();
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(usb);
|
||||||
|
const filter: USBDeviceFilter = {
|
||||||
|
classCode: undefined,
|
||||||
|
vendorId: 0x1234,
|
||||||
|
};
|
||||||
|
await expect(
|
||||||
|
manager.requestDevice({ filters: [filter] }),
|
||||||
|
).resolves.toBeInstanceOf(AdbDaemonWebUsbDevice);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledWith({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
...filter,
|
||||||
|
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept multiple filters", async () => {
|
||||||
|
const usb = new MockUsb();
|
||||||
|
const manager = new AdbDaemonWebUsbDeviceManager(usb);
|
||||||
|
const filter1: USBDeviceFilter = { vendorId: 0x1234 };
|
||||||
|
const filter2: USBDeviceFilter = { classCode: 0xaa };
|
||||||
|
await expect(
|
||||||
|
manager.requestDevice({ filters: [filter1, filter2] }),
|
||||||
|
).resolves.toBeInstanceOf(AdbDaemonWebUsbDevice);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(usb.requestDevice).toHaveBeenCalledWith({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||||
|
...filter1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...ADB_DEFAULT_INTERFACE_FILTER,
|
||||||
|
...filter2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,4 @@
|
||||||
import { ADB_DEFAULT_DEVICE_FILTER, AdbDaemonWebUsbDevice } from "./device.js";
|
import { AdbDaemonWebUsbDevice, toAdbDeviceFilters } from "./device.js";
|
||||||
import type { AdbDeviceFilter } from "./utils.js";
|
|
||||||
import {
|
import {
|
||||||
findUsbAlternateInterface,
|
findUsbAlternateInterface,
|
||||||
getSerialNumber,
|
getSerialNumber,
|
||||||
|
@ -8,7 +7,7 @@ import {
|
||||||
|
|
||||||
export namespace AdbDaemonWebUsbDeviceManager {
|
export namespace AdbDaemonWebUsbDeviceManager {
|
||||||
export interface RequestDeviceOptions {
|
export interface RequestDeviceOptions {
|
||||||
filters?: AdbDeviceFilter[] | undefined;
|
filters?: USBDeviceFilter[] | undefined;
|
||||||
exclusionFilters?: USBDeviceFilter[] | undefined;
|
exclusionFilters?: USBDeviceFilter[] | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,28 +43,21 @@ export class AdbDaemonWebUsbDeviceManager {
|
||||||
* It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
|
* It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
|
||||||
* but might also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.
|
* but might also have `vendorId`, `productId` or `serialNumber` fields to limit the displayed device list.
|
||||||
*
|
*
|
||||||
* Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
|
* Defaults to {@link ADB_DEFAULT_INTERFACE_FILTER}.
|
||||||
* @returns An {@link AdbDaemonWebUsbDevice} instance if the user selected a device,
|
* @returns An {@link AdbDaemonWebUsbDevice} instance if the user selected a device,
|
||||||
* or `undefined` if the user cancelled the device picker.
|
* or `undefined` if the user cancelled the device picker.
|
||||||
*/
|
*/
|
||||||
async requestDevice(
|
async requestDevice(
|
||||||
options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
|
options: AdbDaemonWebUsbDeviceManager.RequestDeviceOptions = {},
|
||||||
): Promise<AdbDaemonWebUsbDevice | undefined> {
|
): Promise<AdbDaemonWebUsbDevice | undefined> {
|
||||||
if (!options.filters) {
|
const filters = toAdbDeviceFilters(options.filters);
|
||||||
options.filters = [ADB_DEFAULT_DEVICE_FILTER];
|
|
||||||
} else if (options.filters.length === 0) {
|
|
||||||
throw new TypeError("filters must not be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const device = await this.#usbManager.requestDevice(
|
const device = await this.#usbManager.requestDevice({
|
||||||
options as USBDeviceRequestOptions,
|
filters,
|
||||||
);
|
exclusionFilters: options.exclusionFilters,
|
||||||
return new AdbDaemonWebUsbDevice(
|
});
|
||||||
device,
|
return new AdbDaemonWebUsbDevice(device, filters, this.#usbManager);
|
||||||
options.filters,
|
|
||||||
this.#usbManager,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No device selected
|
// No device selected
|
||||||
if (isErrorName(e, "NotFoundError")) {
|
if (isErrorName(e, "NotFoundError")) {
|
||||||
|
@ -85,34 +77,35 @@ export class AdbDaemonWebUsbDeviceManager {
|
||||||
* It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
|
* It must have `classCode`, `subclassCode` and `protocolCode` fields for selecting the ADB interface,
|
||||||
* but might also have `vendorId`, `productId` or `serialNumber` fields to limit the device list.
|
* but might also have `vendorId`, `productId` or `serialNumber` fields to limit the device list.
|
||||||
*
|
*
|
||||||
* Defaults to {@link ADB_DEFAULT_DEVICE_FILTER}.
|
* Defaults to {@link ADB_DEFAULT_INTERFACE_FILTER}.
|
||||||
* @returns An array of {@link AdbDaemonWebUsbDevice} instances for all connected and authenticated devices.
|
* @returns An array of {@link AdbDaemonWebUsbDevice} instances for all connected and authenticated devices.
|
||||||
*/
|
*/
|
||||||
|
getDevices(
|
||||||
|
filters?: USBDeviceFilter[] | undefined,
|
||||||
|
): Promise<AdbDaemonWebUsbDevice[]>;
|
||||||
async getDevices(
|
async getDevices(
|
||||||
filters: AdbDeviceFilter[] = [ADB_DEFAULT_DEVICE_FILTER],
|
filters_: USBDeviceFilter[] | undefined,
|
||||||
): Promise<AdbDaemonWebUsbDevice[]> {
|
): Promise<AdbDaemonWebUsbDevice[]> {
|
||||||
if (filters.length === 0) {
|
const filters = toAdbDeviceFilters(filters_);
|
||||||
throw new TypeError("filters must not be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
const devices = await this.#usbManager.getDevices();
|
const devices = await this.#usbManager.getDevices();
|
||||||
return devices
|
return devices
|
||||||
.filter((device) => {
|
.filter((device) => {
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
if (
|
if (
|
||||||
"vendorId" in filter &&
|
filter.vendorId !== undefined &&
|
||||||
device.vendorId !== filter.vendorId
|
device.vendorId !== filter.vendorId
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
"productId" in filter &&
|
filter.productId !== undefined &&
|
||||||
device.productId !== filter.productId
|
device.productId !== filter.productId
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
"serialNumber" in filter &&
|
filter.serialNumber !== undefined &&
|
||||||
getSerialNumber(device) !== filter.serialNumber
|
getSerialNumber(device) !== filter.serialNumber
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -7,18 +7,22 @@ export function isErrorName(e: unknown, name: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PickNonNullable<T, K extends keyof T> = {
|
||||||
|
[P in K]-?: NonNullable<T[P]>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `classCode`, `subclassCode` and `protocolCode` are required
|
* `classCode`, `subclassCode` and `protocolCode` are required
|
||||||
* for selecting correct USB configuration and interface.
|
* for selecting correct USB configuration and interface.
|
||||||
*/
|
*/
|
||||||
export type AdbDeviceFilter = USBDeviceFilter &
|
export type UsbInterfaceFilter = PickNonNullable<
|
||||||
Required<
|
USBDeviceFilter,
|
||||||
Pick<USBDeviceFilter, "classCode" | "subclassCode" | "protocolCode">
|
"classCode" | "subclassCode" | "protocolCode"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function alternateMatchesFilter(
|
function alternateMatchesFilter(
|
||||||
alternate: USBAlternateInterface,
|
alternate: USBAlternateInterface,
|
||||||
filters: AdbDeviceFilter[],
|
filters: UsbInterfaceFilter[],
|
||||||
) {
|
) {
|
||||||
return filters.some(
|
return filters.some(
|
||||||
(filter) =>
|
(filter) =>
|
||||||
|
@ -30,7 +34,7 @@ function alternateMatchesFilter(
|
||||||
|
|
||||||
export function findUsbAlternateInterface(
|
export function findUsbAlternateInterface(
|
||||||
device: USBDevice,
|
device: USBDevice,
|
||||||
filters: AdbDeviceFilter[],
|
filters: UsbInterfaceFilter[],
|
||||||
) {
|
) {
|
||||||
for (const configuration of device.configurations) {
|
for (const configuration of device.configurations) {
|
||||||
for (const interface_ of configuration.interfaces) {
|
for (const interface_ of configuration.interfaces) {
|
||||||
|
@ -56,3 +60,42 @@ export function getSerialNumber(device: USBDevice) {
|
||||||
|
|
||||||
return padNumber(device.vendorId) + "x" + padNumber(device.productId);
|
return padNumber(device.vendorId) + "x" + padNumber(device.productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first pair of input and output endpoints from an alternate interface.
|
||||||
|
*
|
||||||
|
* ADB interface only has two endpoints, one for input and one for output.
|
||||||
|
*/
|
||||||
|
export function findUsbEndpoints(endpoints: USBEndpoint[]) {
|
||||||
|
if (endpoints.length === 0) {
|
||||||
|
throw new TypeError("No endpoints given");
|
||||||
|
}
|
||||||
|
|
||||||
|
let inEndpoint: USBEndpoint | undefined;
|
||||||
|
let outEndpoint: USBEndpoint | undefined;
|
||||||
|
|
||||||
|
for (const endpoint of endpoints) {
|
||||||
|
switch (endpoint.direction) {
|
||||||
|
case "in":
|
||||||
|
inEndpoint = endpoint;
|
||||||
|
if (outEndpoint) {
|
||||||
|
return { inEndpoint, outEndpoint };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "out":
|
||||||
|
outEndpoint = endpoint;
|
||||||
|
if (inEndpoint) {
|
||||||
|
return { inEndpoint, outEndpoint };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inEndpoint) {
|
||||||
|
throw new TypeError("No input endpoint found.");
|
||||||
|
}
|
||||||
|
if (!outEndpoint) {
|
||||||
|
throw new TypeError("No output endpoint found.");
|
||||||
|
}
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
|
10
libraries/adb-daemon-webusb/tsconfig.test.json
Normal file
10
libraries/adb-daemon-webusb/tsconfig.test.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"w3c-web-usb",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": []
|
||||||
|
}
|
|
@ -15,6 +15,9 @@
|
||||||
<a href="https://github.com/yume-chan/ya-webadb/releases">
|
<a href="https://github.com/yume-chan/ya-webadb/releases">
|
||||||
<img alt="GitHub release" src="https://img.shields.io/github/v/release/yume-chan/ya-webadb?logo=github">
|
<img alt="GitHub release" src="https://img.shields.io/github/v/release/yume-chan/ya-webadb?logo=github">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://bundlephobia.com/package/@yume-chan/adb">
|
||||||
|
<img alt="Package Size" src="https://img.shields.io/bundlephobia/minzip/%40yume-chan%2Fadb">
|
||||||
|
</a>
|
||||||
<a href="https://www.npmjs.com/package/@yume-chan/adb">
|
<a href="https://www.npmjs.com/package/@yume-chan/adb">
|
||||||
<img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/adb?logo=npm">
|
<img alt="npm" src="https://img.shields.io/npm/dm/%40yume-chan/adb?logo=npm">
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -27,9 +27,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b tsconfig.build.json",
|
"build": "tsc -b tsconfig.build.json",
|
||||||
"build:watch": "tsc -b tsconfig.build.json",
|
"build:watch": "tsc -b tsconfig.build.json",
|
||||||
"test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" TS_JEST_DISABLE_VER_CHECKER=true jest --coverage",
|
|
||||||
"lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4",
|
"lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build",
|
||||||
|
"test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" TS_JEST_DISABLE_VER_CHECKER=true jest --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yume-chan/async": "^2.2.0",
|
"@yume-chan/async": "^2.2.0",
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
{
|
{
|
||||||
"path": "../event/tsconfig.build.json"
|
"path": "../event/tsconfig.build.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "../no-data-view/tsconfig.build.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../stream-extra/tsconfig.build.json"
|
"path": "../stream-extra/tsconfig.build.json"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"references": [
|
"references": [
|
||||||
{
|
|
||||||
"path": "../no-data-view/tsconfig.build.json"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.test.json"
|
"path": "./tsconfig.test.json"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue