Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 7a7f38b3 authored by Simon Chan's avatar Simon Chan
Browse files

feat: correctly close adb connection

parent be4dfcd6
Loading
Loading
Loading
Loading
+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';
@@ -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);
+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";
@@ -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(() => {
@@ -73,6 +72,13 @@ const Shell: NextPage = (): JSX.Element | null => {
        }
    }, []);

    useEffect(() => {
        state.setVisible(true);
        return () => {
            state.setVisible(false);
        };
    }, []);

    return (
        <Stack {...RouteStackProps}>
            <Head>
@@ -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>
+1 −1
Original line number Diff line number Diff line
@@ -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;
        }
+7 −8
Original line number Diff line number Diff line
@@ -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);
+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;
@@ -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));
@@ -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