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

Unverified Commit e9c82f94 authored by Simon Chan's avatar Simon Chan
Browse files

feat(adb): server client mdns services

parent fdf0a863
Loading
Loading
Loading
Loading
+9 −10
Original line number Original line Diff line number Diff line
import type { AddressInfo, SocketConnectOpts } from "net";
import type { AddressInfo, SocketConnectOpts } from "net";
import { Server, Socket } from "net";
import { Server, Socket } from "net";


import type {
import type { AdbIncomingSocketHandler, AdbServerClient } from "@yume-chan/adb";
    AdbIncomingSocketHandler,
    AdbServerConnection,
    AdbServerConnectionOptions,
    AdbServerConnector,
} from "@yume-chan/adb";
import {
import {
    MaybeConsumable,
    MaybeConsumable,
    PushReadableStream,
    PushReadableStream,
@@ -15,7 +10,9 @@ import {
} from "@yume-chan/stream-extra";
} from "@yume-chan/stream-extra";
import type { ValueOrPromise } from "@yume-chan/struct";
import type { ValueOrPromise } from "@yume-chan/struct";


function nodeSocketToConnection(socket: Socket): AdbServerConnection {
function nodeSocketToConnection(
    socket: Socket,
): AdbServerClient.ServerConnection {
    socket.setNoDelay(true);
    socket.setNoDelay(true);


    const closed = new Promise<void>((resolve) => {
    const closed = new Promise<void>((resolve) => {
@@ -64,7 +61,9 @@ function nodeSocketToConnection(socket: Socket): AdbServerConnection {
    };
    };
}
}


export class AdbServerNodeTcpConnector implements AdbServerConnector {
export class AdbServerNodeTcpConnector
    implements AdbServerClient.ServerConnector
{
    readonly spec: SocketConnectOpts;
    readonly spec: SocketConnectOpts;


    readonly #listeners = new Map<string, Server>();
    readonly #listeners = new Map<string, Server>();
@@ -74,8 +73,8 @@ export class AdbServerNodeTcpConnector implements AdbServerConnector {
    }
    }


    async connect(
    async connect(
        { unref }: AdbServerConnectionOptions = { unref: false },
        { unref }: AdbServerClient.ServerConnectionOptions = { unref: false },
    ): Promise<AdbServerConnection> {
    ): Promise<AdbServerClient.ServerConnection> {
        const socket = new Socket();
        const socket = new Socket();
        if (unref) {
        if (unref) {
            socket.unref();
            socket.unref();
+98 −65
Original line number Original line Diff line number Diff line
@@ -27,52 +27,6 @@ import { NOOP, hexToNumber, write4HexDigits } from "../utils/index.js";


import { AdbServerTransport } from "./transport.js";
import { AdbServerTransport } from "./transport.js";


export interface AdbServerConnectionOptions {
    unref?: boolean | undefined;
    signal?: AbortSignal | undefined;
}

export interface AdbServerConnection
    extends ReadableWritablePair<Uint8Array, Uint8Array>,
        Closeable {
    get closed(): Promise<void>;
}

export interface AdbServerConnector {
    connect(
        options?: AdbServerConnectionOptions,
    ): ValueOrPromise<AdbServerConnection>;

    addReverseTunnel(
        handler: AdbIncomingSocketHandler,
        address?: string,
    ): ValueOrPromise<string>;

    removeReverseTunnel(address: string): ValueOrPromise<void>;

    clearReverseTunnels(): ValueOrPromise<void>;
}

export interface AdbServerSocket extends AdbSocket {
    transportId: bigint;
}

export type AdbServerDeviceSelector =
    | { transportId: bigint }
    | { serial: string }
    | { usb: true }
    | { tcp: true }
    | undefined;

export interface AdbServerDevice {
    serial: string;
    authenticating: boolean;
    product?: string | undefined;
    model?: string | undefined;
    device?: string | undefined;
    transportId: bigint;
}

function sequenceEqual(a: Uint8Array, b: Uint8Array): boolean {
function sequenceEqual(a: Uint8Array, b: Uint8Array): boolean {
    if (a.length !== b.length) {
    if (a.length !== b.length) {
        return false;
        return false;
@@ -91,11 +45,11 @@ const OKAY = encodeUtf8("OKAY");
const FAIL = encodeUtf8("FAIL");
const FAIL = encodeUtf8("FAIL");


class AdbServerStream {
class AdbServerStream {
    #connection: AdbServerConnection;
    #connection: AdbServerClient.ServerConnection;
    #buffered: BufferedReadableStream;
    #buffered: BufferedReadableStream;
    #writer: WritableStreamDefaultWriter<Uint8Array>;
    #writer: WritableStreamDefaultWriter<Uint8Array>;


    constructor(connection: AdbServerConnection) {
    constructor(connection: AdbServerClient.ServerConnection) {
        this.#connection = connection;
        this.#connection = connection;
        this.#buffered = new BufferedReadableStream(connection.readable);
        this.#buffered = new BufferedReadableStream(connection.readable);
        this.#writer = connection.writable.getWriter();
        this.#writer = connection.writable.getWriter();
@@ -191,15 +145,15 @@ class AdbServerStream {
export class AdbServerClient {
export class AdbServerClient {
    static readonly VERSION = 41;
    static readonly VERSION = 41;


    readonly connection: AdbServerConnector;
    readonly connection: AdbServerClient.ServerConnector;


    constructor(connection: AdbServerConnector) {
    constructor(connection: AdbServerClient.ServerConnector) {
        this.connection = connection;
        this.connection = connection;
    }
    }


    async createConnection(
    async createConnection(
        request: string,
        request: string,
        options?: AdbServerConnectionOptions,
        options?: AdbServerClient.ServerConnectionOptions,
    ): Promise<AdbServerStream> {
    ): Promise<AdbServerStream> {
        const connection = await this.connection.connect(options);
        const connection = await this.connection.connect(options);
        const stream = new AdbServerStream(connection);
        const stream = new AdbServerStream(connection);
@@ -326,8 +280,8 @@ export class AdbServerClient {
        }
        }
    }
    }


    parseDeviceList(value: string): AdbServerDevice[] {
    parseDeviceList(value: string): AdbServerClient.Device[] {
        const devices: AdbServerDevice[] = [];
        const devices: AdbServerClient.Device[] = [];
        for (const line of value.split("\n")) {
        for (const line of value.split("\n")) {
            if (!line) {
            if (!line) {
                continue;
                continue;
@@ -379,7 +333,7 @@ export class AdbServerClient {
    /**
    /**
     * `adb devices -l`
     * `adb devices -l`
     */
     */
    async getDevices(): Promise<AdbServerDevice[]> {
    async getDevices(): Promise<AdbServerClient.Device[]> {
        const connection = await this.createConnection("host:devices-l");
        const connection = await this.createConnection("host:devices-l");
        try {
        try {
            const response = await connection.readString();
            const response = await connection.readString();
@@ -398,7 +352,7 @@ export class AdbServerClient {
     */
     */
    async *trackDevices(
    async *trackDevices(
        signal?: AbortSignal,
        signal?: AbortSignal,
    ): AsyncGenerator<AdbServerDevice[], void, void> {
    ): AsyncGenerator<AdbServerClient.Device[], void, void> {
        const connection = await this.createConnection("host:track-devices-l");
        const connection = await this.createConnection("host:track-devices-l");
        try {
        try {
            while (true) {
            while (true) {
@@ -418,7 +372,40 @@ export class AdbServerClient {
        }
        }
    }
    }


    formatDeviceService(device: AdbServerDeviceSelector, command: string) {
    async mDnsCheck() {
        const connection = await this.createConnection("host:mdns:check");
        try {
            const response = await connection.readString();
            return !response.startsWith("ERROR:");
        } finally {
            await connection.dispose();
        }
    }

    async mDnsGetServices() {
        const connection = await this.createConnection("host:mdns:services");
        try {
            const response = await connection.readString();
            return response
                .split("\n")
                .filter(Boolean)
                .map((line) => {
                    const parts = line.split("\t");
                    return {
                        name: parts[0]!,
                        service: parts[1]!,
                        address: parts[2]!,
                    };
                });
        } finally {
            await connection.dispose();
        }
    }

    formatDeviceService(
        device: AdbServerClient.DeviceSelector,
        command: string,
    ) {
        if (!device) {
        if (!device) {
            return `host:${command}`;
            return `host:${command}`;
        }
        }
@@ -440,7 +427,7 @@ export class AdbServerClient {
    /**
    /**
     * `adb -s <device> reconnect` or `adb reconnect offline`
     * `adb -s <device> reconnect` or `adb reconnect offline`
     */
     */
    async reconnectDevice(device: AdbServerDeviceSelector | "offline") {
    async reconnectDevice(device: AdbServerClient.DeviceSelector | "offline") {
        const connection = await this.createConnection(
        const connection = await this.createConnection(
            device === "offline"
            device === "offline"
                ? "host:reconnect-offline"
                ? "host:reconnect-offline"
@@ -461,7 +448,7 @@ export class AdbServerClient {
     * @returns The transport ID of the selected device, and the features supported by the device.
     * @returns The transport ID of the selected device, and the features supported by the device.
     */
     */
    async getDeviceFeatures(
    async getDeviceFeatures(
        device: AdbServerDeviceSelector,
        device: AdbServerClient.DeviceSelector,
    ): Promise<{ transportId: bigint; features: AdbFeature[] }> {
    ): Promise<{ transportId: bigint; features: AdbFeature[] }> {
        // On paper, `host:features` is a host service (device features are cached in host),
        // On paper, `host:features` is a host service (device features are cached in host),
        // so it shouldn't use `createDeviceConnection`,
        // so it shouldn't use `createDeviceConnection`,
@@ -483,7 +470,7 @@ export class AdbServerClient {
            device,
            device,
            "host:features",
            "host:features",
        );
        );
        // Luckily `AdbServerSocket` is compatible with `AdbServerConnection`
        // Luckily `AdbServerClient.Socket` is compatible with `AdbServerClient.ServerConnection`
        const stream = new AdbServerStream(connection);
        const stream = new AdbServerStream(connection);
        try {
        try {
            const featuresString = await stream.readString();
            const featuresString = await stream.readString();
@@ -498,12 +485,12 @@ export class AdbServerClient {
     * Creates a connection that will forward the service to device.
     * Creates a connection that will forward the service to device.
     * @param device The device selector
     * @param device The device selector
     * @param service The service to forward
     * @param service The service to forward
     * @returns An `AdbServerSocket` that can be used to communicate with the service
     * @returns An `AdbServerClient.Socket` that can be used to communicate with the service
     */
     */
    async createDeviceConnection(
    async createDeviceConnection(
        device: AdbServerDeviceSelector,
        device: AdbServerClient.DeviceSelector,
        service: string,
        service: string,
    ): Promise<AdbServerSocket> {
    ): Promise<AdbServerClient.Socket> {
        await this.validateVersion();
        await this.validateVersion();


        let switchService: string;
        let switchService: string;
@@ -573,9 +560,9 @@ export class AdbServerClient {
     * @returns A promise that resolves when the condition is met.
     * @returns A promise that resolves when the condition is met.
     */
     */
    async waitFor(
    async waitFor(
        device: AdbServerDeviceSelector,
        device: AdbServerClient.DeviceSelector,
        state: "device" | "disconnect",
        state: "device" | "disconnect",
        options?: AdbServerConnectionOptions,
        options?: AdbServerClient.ServerConnectionOptions,
    ): Promise<void> {
    ): Promise<void> {
        let type: string;
        let type: string;
        if (!device) {
        if (!device) {
@@ -608,7 +595,7 @@ export class AdbServerClient {
    }
    }


    async createTransport(
    async createTransport(
        device: AdbServerDeviceSelector,
        device: AdbServerClient.DeviceSelector,
    ): Promise<AdbServerTransport> {
    ): Promise<AdbServerTransport> {
        const { transportId, features } = await this.getDeviceFeatures(device);
        const { transportId, features } = await this.getDeviceFeatures(device);


@@ -665,6 +652,52 @@ export async function raceSignal<T>(
}
}


export namespace AdbServerClient {
export namespace AdbServerClient {
    export interface ServerConnectionOptions {
        unref?: boolean | undefined;
        signal?: AbortSignal | undefined;
    }

    export interface ServerConnection
        extends ReadableWritablePair<Uint8Array, Uint8Array>,
            Closeable {
        get closed(): Promise<void>;
    }

    export interface ServerConnector {
        connect(
            options?: ServerConnectionOptions,
        ): ValueOrPromise<ServerConnection>;

        addReverseTunnel(
            handler: AdbIncomingSocketHandler,
            address?: string,
        ): ValueOrPromise<string>;

        removeReverseTunnel(address: string): ValueOrPromise<void>;

        clearReverseTunnels(): ValueOrPromise<void>;
    }

    export interface Socket extends AdbSocket {
        transportId: bigint;
    }

    export type DeviceSelector =
        | { transportId: bigint }
        | { serial: string }
        | { usb: true }
        | { tcp: true }
        | undefined;

    export interface Device {
        serial: string;
        authenticating: boolean;
        product?: string | undefined;
        model?: string | undefined;
        device?: string | undefined;
        transportId: bigint;
    }

    export class NetworkError extends Error {
    export class NetworkError extends Error {
        constructor(message: string) {
        constructor(message: string) {
            super(message);
            super(message);