UNPKG

@yume-chan/adb

Version:

TypeScript implementation of Android Debug Bridge (ADB) protocol.

209 lines (177 loc) 6.33 kB
// cspell: ignore killforward import { BufferedReadableStream } from "@yume-chan/stream-extra"; import { encodeUtf8, ExactReadableEndedError, extend, string, struct, } from "@yume-chan/struct"; import type { AdbIncomingSocketHandler } from "../adb.js"; import { hexToNumber, sequenceEqual } from "../utils/index.js"; import { AdbServiceBase } from "./base.js"; export interface AdbForwardListener { deviceSerial: string; localName: string; remoteName: string; } const AdbReverseStringResponse = struct( { length: string(4), content: string({ field: "length", convert(value: string) { return Number.parseInt(value, 16); }, back(value) { return value.toString(16).padStart(4, "0"); }, }), }, { littleEndian: true }, ); export class AdbReverseError extends Error { constructor(message: string) { super(message); } } export class AdbReverseNotSupportedError extends AdbReverseError { constructor() { super( "ADB reverse tunnel is not supported on this device when connected wirelessly.", ); } } const AdbReverseErrorResponse = extend( AdbReverseStringResponse, {}, { postDeserialize(value) { // https://issuetracker.google.com/issues/37066218 // ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over Wi-Fi), // and returns this confusing "more than one device/emulator" error. if (value.content === "more than one device/emulator") { throw new AdbReverseNotSupportedError(); } else { throw new AdbReverseError(value.content); } }, }, ); // Like `hexToNumber`, it's much faster than first converting `buffer` to a string function decimalToNumber(buffer: Uint8Array) { let value = 0; for (const byte of buffer) { // Like `parseInt`, return when it encounters a non-digit character if (byte < 48 || byte > 57) { return value; } value = value * 10 + byte - 48; } return value; } const OKAY = encodeUtf8("OKAY"); export class AdbReverseService extends AdbServiceBase { readonly #deviceAddressToLocalAddress = new Map<string, string>(); protected async createBufferedStream(service: string) { const socket = await this.adb.createSocket(service); return new BufferedReadableStream(socket.readable); } protected async sendRequest(service: string) { const stream = await this.createBufferedStream(service); const response = await stream.readExactly(4); if (!sequenceEqual(response, OKAY)) { await AdbReverseErrorResponse.deserialize(stream); } return stream; } /** * Get a list of all reverse port forwarding on the device. */ async list(): Promise<AdbForwardListener[]> { const stream = await this.createBufferedStream("reverse:list-forward"); const response = await AdbReverseStringResponse.deserialize(stream); return response.content .split("\n") .filter((line) => !!line) .map((line) => { const [deviceSerial, localName, remoteName] = line.split( " ", ) as [string, string, string]; return { deviceSerial, localName, remoteName }; }); // No need to close the stream, device will close it } /** * Add a reverse port forwarding for a program that already listens on a port. */ async addExternal(deviceAddress: string, localAddress: string) { const stream = await this.sendRequest( `reverse:forward:${deviceAddress};${localAddress}`, ); // `tcp:0` tells the device to pick an available port. // On Android >=8, device will respond with the selected port for all `tcp:` requests. if (deviceAddress.startsWith("tcp:")) { const position = stream.position; try { const length = hexToNumber(await stream.readExactly(4)); const port = decimalToNumber(await stream.readExactly(length)); deviceAddress = `tcp:${port}`; } catch (e) { if ( e instanceof ExactReadableEndedError && stream.position === position ) { // Android <8 doesn't have this response. // (the stream is closed now) // Can be safely ignored. } else { throw e; } } } return deviceAddress; } /** * Add a reverse port forwarding. */ async add( deviceAddress: string, handler: AdbIncomingSocketHandler, localAddress?: string, ): Promise<string> { localAddress = await this.adb.transport.addReverseTunnel( handler, localAddress, ); try { deviceAddress = await this.addExternal(deviceAddress, localAddress); this.#deviceAddressToLocalAddress.set(deviceAddress, localAddress); return deviceAddress; } catch (e) { await this.adb.transport.removeReverseTunnel(localAddress); throw e; } } /** * Remove a reverse port forwarding. */ async remove(deviceAddress: string): Promise<void> { const localAddress = this.#deviceAddressToLocalAddress.get(deviceAddress); if (localAddress) { await this.adb.transport.removeReverseTunnel(localAddress); } await this.sendRequest(`reverse:killforward:${deviceAddress}`); // No need to close the stream, device will close it } /** * Remove all reverse port forwarding, including the ones added by other programs. */ async removeAll(): Promise<void> { await this.adb.transport.clearReverseTunnels(); this.#deviceAddressToLocalAddress.clear(); await this.sendRequest(`reverse:killforward-all`); // No need to close the stream, device will close it } }