UNPKG

@yume-chan/adb

Version:

TypeScript implementation of Android Debug Bridge (ADB) protocol.

128 lines (112 loc) 4.54 kB
import type { MaybePromiseLike } from "@yume-chan/async"; import { PromiseResolver } from "@yume-chan/async"; import type { AbortSignal, PushReadableStreamController, ReadableStream, WritableStreamDefaultWriter, } from "@yume-chan/stream-extra"; import { MaybeConsumable, PushReadableStream, StructDeserializeStream, WritableStream, } from "@yume-chan/stream-extra"; import type { AdbSocket } from "../../../adb.js"; import { AdbShellProtocolId, AdbShellProtocolPacket } from "./shared.js"; import type { AdbShellProtocolProcess } from "./spawner.js"; export class AdbShellProtocolProcessImpl implements AdbShellProtocolProcess { readonly #socket: AdbSocket; readonly #writer: WritableStreamDefaultWriter<MaybeConsumable<Uint8Array>>; readonly #stdin: WritableStream<MaybeConsumable<Uint8Array>>; get stdin() { return this.#stdin; } readonly #stdout: ReadableStream<Uint8Array>; get stdout() { return this.#stdout; } readonly #stderr: ReadableStream<Uint8Array>; get stderr() { return this.#stderr; } readonly #exited: Promise<number>; get exited() { return this.#exited; } constructor(socket: AdbSocket, signal?: AbortSignal) { this.#socket = socket; let stdoutController!: PushReadableStreamController<Uint8Array>; let stderrController!: PushReadableStreamController<Uint8Array>; this.#stdout = new PushReadableStream<Uint8Array>((controller) => { stdoutController = controller; }); this.#stderr = new PushReadableStream<Uint8Array>((controller) => { stderrController = controller; }); const exited = new PromiseResolver<number>(); this.#exited = exited.promise; socket.readable .pipeThrough(new StructDeserializeStream(AdbShellProtocolPacket)) .pipeTo( new WritableStream<AdbShellProtocolPacket>({ write: async (chunk) => { switch (chunk.id) { case AdbShellProtocolId.Exit: exited.resolve(chunk.data[0]!); break; case AdbShellProtocolId.Stdout: await stdoutController.enqueue(chunk.data); break; case AdbShellProtocolId.Stderr: await stderrController.enqueue(chunk.data); break; default: // Ignore unknown messages like Google ADB does // https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/adb/daemon/shell_service.cpp;l=684;drc=61197364367c9e404c7da6900658f1b16c42d0da break; } }, }), ) .then( () => { stdoutController.close(); stderrController.close(); // If `exited` has already settled, this will be a no-op exited.reject( new Error("Socket ended without exit message"), ); }, (e) => { stdoutController.error(e); stderrController.error(e); // If `exited` has already settled, this will be a no-op exited.reject(e); }, ); if (signal) { // `signal` won't affect `this.stdout` and `this.stderr` // So remaining data can still be read // (call `controller.error` will discard all pending data) signal.addEventListener("abort", () => { exited.reject(signal.reason); this.#socket.close(); }); } this.#writer = this.#socket.writable.getWriter(); this.#stdin = new MaybeConsumable.WritableStream<Uint8Array>({ write: async (chunk) => { await this.#writer.write( AdbShellProtocolPacket.serialize({ id: AdbShellProtocolId.Stdin, data: chunk, }), ); }, }); } kill(): MaybePromiseLike<void> { return this.#socket.close(); } }