Loading .changeset/cyan-readers-fail.md 0 → 100644 +5 −0 Original line number Diff line number Diff line --- "@yume-chan/adb-daemon-webusb": patch --- Accept exclusionFilters in getDevices and DeviceObserver libraries/adb-daemon-webusb/src/device.ts +18 −27 Original line number Diff line number Diff line Loading @@ -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, })); } } Loading Loading @@ -262,7 +254,7 @@ export class AdbDaemonWebUsbConnection } export class AdbDaemonWebUsbDevice implements AdbDaemonDevice { #filters: UsbInterfaceFilter[]; #interface: UsbInterfaceIdentifier; #usbManager: USB; #raw: USBDevice; Loading @@ -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 !== Loading Loading @@ -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, Loading libraries/adb-daemon-webusb/src/manager.spec.ts +9 −12 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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, }, ]); Loading @@ -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, }, ]); Loading @@ -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, }, ]); Loading @@ -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, }, ]); Loading @@ -136,7 +133,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => { { filters: [ { ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, ...filter, }, ], Loading @@ -162,7 +159,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => { filters: [ { ...filter, ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, }, ], exclusionFilters: undefined, Loading @@ -185,11 +182,11 @@ describe("AdbDaemonWebUsbDeviceManager", () => { { filters: [ { ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, ...filter1, }, { ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, ...filter2, }, ], Loading libraries/adb-daemon-webusb/src/manager.ts +45 −15 Original line number Diff line number Diff line 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 { Loading Loading @@ -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")) { Loading @@ -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) => new AdbDaemonWebUsbDevice( // filter map const result: AdbDaemonWebUsbDevice[] = []; for (const device of devices) { const interface_ = matchFilters( device, filters, options.exclusionFilters, ); if (interface_) { result.push( new AdbDaemonWebUsbDevice( device, 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); } } libraries/adb-daemon-webusb/src/observer.ts +26 −7 Original line number Diff line number Diff line 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 Loading @@ -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>(); Loading @@ -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); Loading @@ -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]); Loading @@ -71,5 +85,10 @@ export class AdbDaemonWebUsbDeviceObserver "disconnect", this.#handleDisconnect, ); this.#onError.dispose(); this.#onDeviceAdd.dispose(); this.#onDeviceRemove.dispose(); this.#onListChange.dispose(); } } Loading
.changeset/cyan-readers-fail.md 0 → 100644 +5 −0 Original line number Diff line number Diff line --- "@yume-chan/adb-daemon-webusb": patch --- Accept exclusionFilters in getDevices and DeviceObserver
libraries/adb-daemon-webusb/src/device.ts +18 −27 Original line number Diff line number Diff line Loading @@ -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, })); } } Loading Loading @@ -262,7 +254,7 @@ export class AdbDaemonWebUsbConnection } export class AdbDaemonWebUsbDevice implements AdbDaemonDevice { #filters: UsbInterfaceFilter[]; #interface: UsbInterfaceIdentifier; #usbManager: USB; #raw: USBDevice; Loading @@ -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 !== Loading Loading @@ -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, Loading
libraries/adb-daemon-webusb/src/manager.spec.ts +9 −12 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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, }, ]); Loading @@ -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, }, ]); Loading @@ -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, }, ]); Loading @@ -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, }, ]); Loading @@ -136,7 +133,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => { { filters: [ { ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, ...filter, }, ], Loading @@ -162,7 +159,7 @@ describe("AdbDaemonWebUsbDeviceManager", () => { filters: [ { ...filter, ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, }, ], exclusionFilters: undefined, Loading @@ -185,11 +182,11 @@ describe("AdbDaemonWebUsbDeviceManager", () => { { filters: [ { ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, ...filter1, }, { ...ADB_DEFAULT_INTERFACE_FILTER, ...AdbDefaultInterfaceFilter, ...filter2, }, ], Loading
libraries/adb-daemon-webusb/src/manager.ts +45 −15 Original line number Diff line number Diff line 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 { Loading Loading @@ -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")) { Loading @@ -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) => new AdbDaemonWebUsbDevice( // filter map const result: AdbDaemonWebUsbDevice[] = []; for (const device of devices) { const interface_ = matchFilters( device, filters, options.exclusionFilters, ); if (interface_) { result.push( new AdbDaemonWebUsbDevice( device, 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); } }
libraries/adb-daemon-webusb/src/observer.ts +26 −7 Original line number Diff line number Diff line 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 Loading @@ -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>(); Loading @@ -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); Loading @@ -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]); Loading @@ -71,5 +85,10 @@ export class AdbDaemonWebUsbDeviceObserver "disconnect", this.#handleDisconnect, ); this.#onError.dispose(); this.#onDeviceAdd.dispose(); this.#onDeviceRemove.dispose(); this.#onListChange.dispose(); } }