Loading apps/demo/src/components/connect.tsx +53 −35 Original line number Diff line number Diff line import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react'; import { Adb, AdbBackend, InspectStream, pipeFrom } from '@yume-chan/adb'; import { Adb, AdbBackend, AdbPacketData, AdbPacketInit, InspectStream, pipeFrom, ReadableStream, WritableStream } from '@yume-chan/adb'; import AdbDirectSocketsBackend from "@yume-chan/adb-backend-direct-sockets"; import AdbWebUsbBackend, { AdbWebUsbBackendWatcher } from '@yume-chan/adb-backend-webusb'; import AdbWsBackend from '@yume-chan/adb-backend-ws'; Loading Loading @@ -138,49 +138,67 @@ function _Connect(): JSX.Element | null { }, [updateUsbBackendList]); const connect = useCallback(async () => { try { if (selectedBackend) { let device: Adb | undefined; try { if (!selectedBackend) { return; } setConnecting(true); let readable: ReadableStream<AdbPacketData>; let writable: WritableStream<AdbPacketInit>; try { const streams = await selectedBackend.connect(); // Use `TransformStream` to intercept packets and log them const readable = streams.readable // Use `InspectStream`s to intercept and log packets readable = streams.readable .pipeThrough( new InspectStream(packet => { globalState.appendLog('in', packet); }) ); const writable = pipeFrom( writable = pipeFrom( streams.writable, new InspectStream(packet => { new InspectStream((packet: AdbPacketInit) => { globalState.appendLog('out', packet); }) ); device = await Adb.authenticate({ readable, writable }, CredentialStore, undefined); } catch (e: any) { globalState.showErrorDialog(e); setConnecting(false); return; } try { const device = await Adb.authenticate( { readable, writable }, CredentialStore, undefined ); device.disconnected.then(() => { globalState.setDevice(undefined, undefined); }, (e) => { globalState.showErrorDialog(e); globalState.setDevice(undefined, undefined); }); globalState.setDevice(selectedBackend, device); } catch (e) { device?.dispose(); throw e; } } } catch (e: any) { globalState.showErrorDialog(e); // The streams are still open when Adb authentication failed, // manually close them to release the device. readable.cancel(); writable.close(); } finally { setConnecting(false); } }, [selectedBackend]); const disconnect = useCallback(async () => { try { await globalState.device!.dispose(); await globalState.device!.close(); globalState.setDevice(undefined, undefined); } catch (e: any) { globalState.showErrorDialog(e); Loading apps/demo/src/pages/shell.tsx +56 −50 Original line number Diff line number Diff line import { IconButton, SearchBox, Stack, StackItem } from '@fluentui/react'; import { reaction } from "mobx"; import { action, autorun, makeAutoObservable } from "mobx"; import { observer } from "mobx-react-lite"; import { NextPage } from "next"; import Head from "next/head"; import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect } from 'react'; import 'xterm/css/xterm.css'; import { ResizeObserver } from '../components'; import { globalState } from "../state"; Loading @@ -15,52 +15,51 @@ if (typeof window !== 'undefined') { terminal = new AdbTerminal(); } const UpIconProps = { iconName: Icons.ChevronUp }; const DownIconProps = { iconName: Icons.ChevronDown }; const state = makeAutoObservable({ visible: false, setVisible(value: boolean) { this.visible = value; }, const Shell: NextPage = (): JSX.Element | null => { const [searchKeyword, setSearchKeyword] = useState(''); const handleSearchKeywordChange = useCallback((e, newValue?: string) => { setSearchKeyword(newValue ?? ''); if (newValue) { terminal.searchAddon.findNext(newValue, { incremental: true }); searchKeyword: '', setSearchKeyword(value: string) { this.searchKeyword = value; terminal.searchAddon.findNext(value, { incremental: true }); }, searchPrevious() { terminal.searchAddon.findPrevious(this.searchKeyword); }, searchNext() { terminal.searchAddon.findNext(this.searchKeyword); } }, []); const findPrevious = useCallback(() => { terminal.searchAddon.findPrevious(searchKeyword); }, [searchKeyword]); const findNext = useCallback(() => { terminal.searchAddon.findNext(searchKeyword); }, [searchKeyword]); }, { searchPrevious: action.bound, searchNext: action.bound, }); const connectingRef = useRef(false); useEffect(() => { return reaction( () => globalState.device, async () => { autorun(() => { if (!globalState.device) { terminal.socket = undefined; return; } if (!!terminal.socket || connectingRef.current) { return; } try { connectingRef.current = true; const socket = await globalState.device.subprocess.shell(); terminal.socket = socket; } catch (e: any) { if (!terminal.socket && state.visible) { globalState.device.subprocess.shell() .then(action(shell => { terminal.socket = shell; }), (e) => { globalState.showErrorDialog(e); } finally { connectingRef.current = false; }); } }, { fireImmediately: true, } ); }); const UpIconProps = { iconName: Icons.ChevronUp }; const DownIconProps = { iconName: Icons.ChevronDown }; const Shell: NextPage = (): JSX.Element | null => { const handleSearchKeywordChange = useCallback((e, value?: string) => { state.setSearchKeyword(value ?? ''); }, []); const handleResize = useCallback(() => { Loading @@ -73,6 +72,13 @@ const Shell: NextPage = (): JSX.Element | null => { } }, []); useEffect(() => { state.setVisible(true); return () => { state.setVisible(false); }; }, []); return ( <Stack {...RouteStackProps}> <Head> Loading @@ -84,23 +90,23 @@ const Shell: NextPage = (): JSX.Element | null => { <StackItem grow> <SearchBox placeholder="Find" value={searchKeyword} value={state.searchKeyword} onChange={handleSearchKeywordChange} onSearch={findNext} onSearch={state.searchNext} /> </StackItem> <StackItem> <IconButton disabled={!searchKeyword} disabled={!state.searchKeyword} iconProps={UpIconProps} onClick={findPrevious} onClick={state.searchPrevious} /> </StackItem> <StackItem> <IconButton disabled={!searchKeyword} disabled={!state.searchKeyword} iconProps={DownIconProps} onClick={findNext} onClick={state.searchNext} /> </StackItem> </Stack> Loading apps/demo/src/state/global.ts +1 −1 Original line number Diff line number Diff line Loading @@ -37,7 +37,7 @@ export class GlobalState { showErrorDialog(message: Error | string) { this.errorDialogVisible = true; if (message instanceof Error) { this.errorDialogMessage = message.stack!; this.errorDialogMessage = message.stack || message.message; } else { this.errorDialogMessage = message; } Loading libraries/adb-backend-webusb/src/backend.ts +7 −8 Original line number Diff line number Diff line Loading @@ -34,31 +34,30 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketDat public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) { const factory = new DuplexStreamFactory<AdbPacketData, Uint8Array>({ close: async () => { try { await device.close(); } catch { /* device may have already disconnected */ } }, dispose: async () => { navigator.usb.removeEventListener('disconnect', handleUsbDisconnect); try { await device.close(); } catch { // device may have already disconnected } }, }); function handleUsbDisconnect(e: USBConnectionEvent) { if (e.device === device) { factory.close(); factory.dispose(); } } navigator.usb.addEventListener('disconnect', handleUsbDisconnect); this._readable = factory.createWrapReadable(new ReadableStream<AdbPacketData>({ this._readable = factory.wrapReadable(new ReadableStream<AdbPacketData>({ async pull(controller) { // The `length` argument in `transferIn` must not be smaller than what the device sent, // otherwise it will return `babble` status without any data. // Here we read exactly 24 bytes (packet header) followed by exactly `payloadLength`. const result = await device.transferIn(inEndpoint.endpointNumber, 24); // TODO: webusb-backend: handle `babble` by discarding the data and receive again // TODO: webusb: handle `babble` by discarding the data and receive again // TODO: webusb: on Windows, `transferIn` throws an NetworkError when device disconnected, check with other OSs. // From spec, the `result.data` always covers the whole `buffer`. const buffer = new Uint8Array(result.data!.buffer); Loading libraries/adb-backend-ws/src/index.ts +4 −4 Original line number Diff line number Diff line import { AdbPacket, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, StructDeserializeStream, type AdbBackend } from '@yume-chan/adb'; import { AdbPacket, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, ReadableStream, StructDeserializeStream, type AdbBackend } from '@yume-chan/adb'; export default class AdbWsBackend implements AdbBackend { public readonly serial: string; Loading Loading @@ -28,10 +28,10 @@ export default class AdbWsBackend implements AdbBackend { }); socket.onclose = () => { factory.close(); factory.dispose(); }; const readable = factory.createReadable({ const readable = factory.wrapReadable(new ReadableStream({ start: (controller) => { socket.onmessage = ({ data }: { data: ArrayBuffer; }) => { controller.enqueue(new Uint8Array(data)); Loading @@ -40,7 +40,7 @@ export default class AdbWsBackend implements AdbBackend { }, { highWaterMark: 16 * 1024, size(chunk) { return chunk.byteLength; }, }); })); const writable = factory.createWritable({ write: (chunk) => { Loading Loading
apps/demo/src/components/connect.tsx +53 −35 Original line number Diff line number Diff line import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react'; import { Adb, AdbBackend, InspectStream, pipeFrom } from '@yume-chan/adb'; import { Adb, AdbBackend, AdbPacketData, AdbPacketInit, InspectStream, pipeFrom, ReadableStream, WritableStream } from '@yume-chan/adb'; import AdbDirectSocketsBackend from "@yume-chan/adb-backend-direct-sockets"; import AdbWebUsbBackend, { AdbWebUsbBackendWatcher } from '@yume-chan/adb-backend-webusb'; import AdbWsBackend from '@yume-chan/adb-backend-ws'; Loading Loading @@ -138,49 +138,67 @@ function _Connect(): JSX.Element | null { }, [updateUsbBackendList]); const connect = useCallback(async () => { try { if (selectedBackend) { let device: Adb | undefined; try { if (!selectedBackend) { return; } setConnecting(true); let readable: ReadableStream<AdbPacketData>; let writable: WritableStream<AdbPacketInit>; try { const streams = await selectedBackend.connect(); // Use `TransformStream` to intercept packets and log them const readable = streams.readable // Use `InspectStream`s to intercept and log packets readable = streams.readable .pipeThrough( new InspectStream(packet => { globalState.appendLog('in', packet); }) ); const writable = pipeFrom( writable = pipeFrom( streams.writable, new InspectStream(packet => { new InspectStream((packet: AdbPacketInit) => { globalState.appendLog('out', packet); }) ); device = await Adb.authenticate({ readable, writable }, CredentialStore, undefined); } catch (e: any) { globalState.showErrorDialog(e); setConnecting(false); return; } try { const device = await Adb.authenticate( { readable, writable }, CredentialStore, undefined ); device.disconnected.then(() => { globalState.setDevice(undefined, undefined); }, (e) => { globalState.showErrorDialog(e); globalState.setDevice(undefined, undefined); }); globalState.setDevice(selectedBackend, device); } catch (e) { device?.dispose(); throw e; } } } catch (e: any) { globalState.showErrorDialog(e); // The streams are still open when Adb authentication failed, // manually close them to release the device. readable.cancel(); writable.close(); } finally { setConnecting(false); } }, [selectedBackend]); const disconnect = useCallback(async () => { try { await globalState.device!.dispose(); await globalState.device!.close(); globalState.setDevice(undefined, undefined); } catch (e: any) { globalState.showErrorDialog(e); Loading
apps/demo/src/pages/shell.tsx +56 −50 Original line number Diff line number Diff line import { IconButton, SearchBox, Stack, StackItem } from '@fluentui/react'; import { reaction } from "mobx"; import { action, autorun, makeAutoObservable } from "mobx"; import { observer } from "mobx-react-lite"; import { NextPage } from "next"; import Head from "next/head"; import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect } from 'react'; import 'xterm/css/xterm.css'; import { ResizeObserver } from '../components'; import { globalState } from "../state"; Loading @@ -15,52 +15,51 @@ if (typeof window !== 'undefined') { terminal = new AdbTerminal(); } const UpIconProps = { iconName: Icons.ChevronUp }; const DownIconProps = { iconName: Icons.ChevronDown }; const state = makeAutoObservable({ visible: false, setVisible(value: boolean) { this.visible = value; }, const Shell: NextPage = (): JSX.Element | null => { const [searchKeyword, setSearchKeyword] = useState(''); const handleSearchKeywordChange = useCallback((e, newValue?: string) => { setSearchKeyword(newValue ?? ''); if (newValue) { terminal.searchAddon.findNext(newValue, { incremental: true }); searchKeyword: '', setSearchKeyword(value: string) { this.searchKeyword = value; terminal.searchAddon.findNext(value, { incremental: true }); }, searchPrevious() { terminal.searchAddon.findPrevious(this.searchKeyword); }, searchNext() { terminal.searchAddon.findNext(this.searchKeyword); } }, []); const findPrevious = useCallback(() => { terminal.searchAddon.findPrevious(searchKeyword); }, [searchKeyword]); const findNext = useCallback(() => { terminal.searchAddon.findNext(searchKeyword); }, [searchKeyword]); }, { searchPrevious: action.bound, searchNext: action.bound, }); const connectingRef = useRef(false); useEffect(() => { return reaction( () => globalState.device, async () => { autorun(() => { if (!globalState.device) { terminal.socket = undefined; return; } if (!!terminal.socket || connectingRef.current) { return; } try { connectingRef.current = true; const socket = await globalState.device.subprocess.shell(); terminal.socket = socket; } catch (e: any) { if (!terminal.socket && state.visible) { globalState.device.subprocess.shell() .then(action(shell => { terminal.socket = shell; }), (e) => { globalState.showErrorDialog(e); } finally { connectingRef.current = false; }); } }, { fireImmediately: true, } ); }); const UpIconProps = { iconName: Icons.ChevronUp }; const DownIconProps = { iconName: Icons.ChevronDown }; const Shell: NextPage = (): JSX.Element | null => { const handleSearchKeywordChange = useCallback((e, value?: string) => { state.setSearchKeyword(value ?? ''); }, []); const handleResize = useCallback(() => { Loading @@ -73,6 +72,13 @@ const Shell: NextPage = (): JSX.Element | null => { } }, []); useEffect(() => { state.setVisible(true); return () => { state.setVisible(false); }; }, []); return ( <Stack {...RouteStackProps}> <Head> Loading @@ -84,23 +90,23 @@ const Shell: NextPage = (): JSX.Element | null => { <StackItem grow> <SearchBox placeholder="Find" value={searchKeyword} value={state.searchKeyword} onChange={handleSearchKeywordChange} onSearch={findNext} onSearch={state.searchNext} /> </StackItem> <StackItem> <IconButton disabled={!searchKeyword} disabled={!state.searchKeyword} iconProps={UpIconProps} onClick={findPrevious} onClick={state.searchPrevious} /> </StackItem> <StackItem> <IconButton disabled={!searchKeyword} disabled={!state.searchKeyword} iconProps={DownIconProps} onClick={findNext} onClick={state.searchNext} /> </StackItem> </Stack> Loading
apps/demo/src/state/global.ts +1 −1 Original line number Diff line number Diff line Loading @@ -37,7 +37,7 @@ export class GlobalState { showErrorDialog(message: Error | string) { this.errorDialogVisible = true; if (message instanceof Error) { this.errorDialogMessage = message.stack!; this.errorDialogMessage = message.stack || message.message; } else { this.errorDialogMessage = message; } Loading
libraries/adb-backend-webusb/src/backend.ts +7 −8 Original line number Diff line number Diff line Loading @@ -34,31 +34,30 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketDat public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) { const factory = new DuplexStreamFactory<AdbPacketData, Uint8Array>({ close: async () => { try { await device.close(); } catch { /* device may have already disconnected */ } }, dispose: async () => { navigator.usb.removeEventListener('disconnect', handleUsbDisconnect); try { await device.close(); } catch { // device may have already disconnected } }, }); function handleUsbDisconnect(e: USBConnectionEvent) { if (e.device === device) { factory.close(); factory.dispose(); } } navigator.usb.addEventListener('disconnect', handleUsbDisconnect); this._readable = factory.createWrapReadable(new ReadableStream<AdbPacketData>({ this._readable = factory.wrapReadable(new ReadableStream<AdbPacketData>({ async pull(controller) { // The `length` argument in `transferIn` must not be smaller than what the device sent, // otherwise it will return `babble` status without any data. // Here we read exactly 24 bytes (packet header) followed by exactly `payloadLength`. const result = await device.transferIn(inEndpoint.endpointNumber, 24); // TODO: webusb-backend: handle `babble` by discarding the data and receive again // TODO: webusb: handle `babble` by discarding the data and receive again // TODO: webusb: on Windows, `transferIn` throws an NetworkError when device disconnected, check with other OSs. // From spec, the `result.data` always covers the whole `buffer`. const buffer = new Uint8Array(result.data!.buffer); Loading
libraries/adb-backend-ws/src/index.ts +4 −4 Original line number Diff line number Diff line import { AdbPacket, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, StructDeserializeStream, type AdbBackend } from '@yume-chan/adb'; import { AdbPacket, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, ReadableStream, StructDeserializeStream, type AdbBackend } from '@yume-chan/adb'; export default class AdbWsBackend implements AdbBackend { public readonly serial: string; Loading Loading @@ -28,10 +28,10 @@ export default class AdbWsBackend implements AdbBackend { }); socket.onclose = () => { factory.close(); factory.dispose(); }; const readable = factory.createReadable({ const readable = factory.wrapReadable(new ReadableStream({ start: (controller) => { socket.onmessage = ({ data }: { data: ArrayBuffer; }) => { controller.enqueue(new Uint8Array(data)); Loading @@ -40,7 +40,7 @@ export default class AdbWsBackend implements AdbBackend { }, { highWaterMark: 16 * 1024, size(chunk) { return chunk.byteLength; }, }); })); const writable = factory.createWritable({ write: (chunk) => { Loading