@yume-chan/adb
Version:
TypeScript implementation of Android Debug Bridge (ADB) protocol.
215 lines (179 loc) • 6.31 kB
text/typescript
import { PromiseResolver } from "@yume-chan/async";
import type { Disposable } from "@yume-chan/event";
import type {
PushReadableStreamController,
ReadableStream,
WritableStream,
WritableStreamDefaultController,
} from "@yume-chan/stream-extra";
import { MaybeConsumable, PushReadableStream } from "@yume-chan/stream-extra";
import { EmptyUint8Array } from "@yume-chan/struct";
import type { AdbSocket } from "../adb.js";
import type { AdbPacketDispatcher } from "./dispatcher.js";
import { AdbCommand } from "./packet.js";
export interface AdbDaemonSocketInfo {
localId: number;
remoteId: number;
localCreated: boolean;
service: string;
}
export interface AdbDaemonSocketInit extends AdbDaemonSocketInfo {
dispatcher: AdbPacketDispatcher;
highWaterMark?: number | undefined;
/**
* The initial delayed ack byte count, or `Infinity` if delayed ack is disabled.
*/
availableWriteBytes: number;
}
export class AdbDaemonSocketController
implements AdbDaemonSocketInfo, AdbSocket, Disposable
{
readonly #dispatcher!: AdbPacketDispatcher;
readonly localId!: number;
readonly remoteId!: number;
readonly localCreated!: boolean;
readonly service!: string;
readonly #readable: ReadableStream<Uint8Array>;
#readableController!: PushReadableStreamController<Uint8Array>;
get readable() {
return this.#readable;
}
#writableController!: WritableStreamDefaultController;
readonly writable: WritableStream<MaybeConsumable<Uint8Array>>;
#closed = false;
readonly #closedPromise = new PromiseResolver<undefined>();
get closed() {
return this.#closedPromise.promise;
}
readonly #socket: AdbDaemonSocket;
get socket() {
return this.#socket;
}
#availableWriteBytesChanged: PromiseResolver<void> | undefined;
/**
* When delayed ack is disabled, returns `Infinity` if the socket is ready to write
* (exactly one packet can be written no matter how large it is), or `-1` if the socket
* is waiting for ack message.
*
* When delayed ack is enabled, returns a non-negative finite number indicates the number of
* bytes that can be written to the socket before waiting for ack message.
*/
#availableWriteBytes = 0;
constructor(options: AdbDaemonSocketInit) {
this.#dispatcher = options.dispatcher;
this.localId = options.localId;
this.remoteId = options.remoteId;
this.localCreated = options.localCreated;
this.service = options.service;
this.#readable = new PushReadableStream((controller) => {
this.#readableController = controller;
});
this.writable = new MaybeConsumable.WritableStream<Uint8Array>({
start: (controller) => {
this.#writableController = controller;
controller.signal.addEventListener("abort", () => {
this.#availableWriteBytesChanged?.reject(
controller.signal.reason,
);
});
},
write: async (data) => {
const size = data.length;
const chunkSize = this.#dispatcher.options.maxPayloadSize;
for (
let start = 0, end = chunkSize;
start < size;
start = end, end += chunkSize
) {
const chunk = data.subarray(start, end);
await this.#writeChunk(chunk);
}
},
});
this.#socket = new AdbDaemonSocket(this);
this.#availableWriteBytes = options.availableWriteBytes;
}
async #writeChunk(data: Uint8Array) {
const length = data.length;
while (this.#availableWriteBytes < length) {
// Only one lock is required because Web Streams API guarantees
// that `write` is not reentrant.
const resolver = new PromiseResolver<void>();
this.#availableWriteBytesChanged = resolver;
await resolver.promise;
}
if (this.#availableWriteBytes === Infinity) {
this.#availableWriteBytes = -1;
} else {
this.#availableWriteBytes -= length;
}
await this.#dispatcher.sendPacket(
AdbCommand.Write,
this.localId,
this.remoteId,
data,
);
}
async enqueue(data: Uint8Array) {
await this.#readableController.enqueue(data);
}
public ack(bytes: number) {
this.#availableWriteBytes += bytes;
this.#availableWriteBytesChanged?.resolve();
}
async close(): Promise<void> {
if (this.#closed) {
return;
}
this.#closed = true;
this.#availableWriteBytesChanged?.reject(new Error("Socket closed"));
try {
this.#writableController.error(new Error("Socket closed"));
} catch {
// ignore
}
await this.#dispatcher.sendPacket(
AdbCommand.Close,
this.localId,
this.remoteId,
EmptyUint8Array,
);
}
dispose() {
this.#readableController.close();
this.#closedPromise.resolve(undefined);
}
}
/**
* A duplex stream representing a socket to ADB daemon.
*/
export class AdbDaemonSocket implements AdbDaemonSocketInfo, AdbSocket {
readonly #controller: AdbDaemonSocketController;
get localId(): number {
return this.#controller.localId;
}
get remoteId(): number {
return this.#controller.remoteId;
}
get localCreated(): boolean {
return this.#controller.localCreated;
}
get service(): string {
return this.#controller.service;
}
get readable(): ReadableStream<Uint8Array> {
return this.#controller.readable;
}
get writable(): WritableStream<MaybeConsumable<Uint8Array>> {
return this.#controller.writable;
}
get closed(): Promise<undefined> {
return this.#controller.closed;
}
constructor(controller: AdbDaemonSocketController) {
this.#controller = controller;
}
close() {
return this.#controller.close();
}
}