UNPKG

@yume-chan/adb

Version:

TypeScript implementation of Android Debug Bridge (ADB) protocol.

147 lines 5.6 kB
// cspell: ignore killforward import { BufferedReadableStream } from "@yume-chan/stream-extra"; import { encodeUtf8, ExactReadableEndedError, extend, string, struct, } from "@yume-chan/struct"; import { hexToNumber, sequenceEqual } from "../utils/index.js"; import { AdbServiceBase } from "./base.js"; const AdbReverseStringResponse = struct({ length: string(4), content: string({ field: "length", convert(value) { return Number.parseInt(value, 16); }, back(value) { return value.toString(16).padStart(4, "0"); }, }), }, { littleEndian: true }); export class AdbReverseError extends Error { constructor(message) { 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) { 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 { #deviceAddressToLocalAddress = new Map(); async createBufferedStream(service) { const socket = await this.adb.createSocket(service); return new BufferedReadableStream(socket.readable); } async sendRequest(service) { 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() { 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(" "); 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, localAddress) { 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, handler, localAddress) { 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) { 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() { await this.adb.transport.clearReverseTunnels(); this.#deviceAddressToLocalAddress.clear(); await this.sendRequest(`reverse:killforward-all`); // No need to close the stream, device will close it } } //# sourceMappingURL=reverse.js.map