Loading libraries/adb/src/commands/sync/socket.ts +5 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,10 @@ export class AdbSyncSocketLocked implements AsyncExactReadable { this.#combiner.flush(); this.#socketLock.notifyOne(); } async close() { await this.#readable.cancel(); } } export class AdbSyncSocket { Loading @@ -94,6 +98,7 @@ export class AdbSyncSocket { } async close() { await this.#locked.close(); await this.#socket.close(); } } libraries/adb/src/daemon/dispatcher.ts +70 −55 Original line number Diff line number Diff line import { AsyncOperationManager, PromiseResolver } from "@yume-chan/async"; import { AsyncOperationManager, PromiseResolver, delay, } from "@yume-chan/async"; import type { Consumable, ReadableWritablePair, Loading @@ -12,7 +16,7 @@ import { import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct"; import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js"; import { decodeUtf8, encodeUtf8, unreachable } from "../utils/index.js"; import { decodeUtf8, encodeUtf8 } from "../utils/index.js"; import type { AdbPacketData, AdbPacketInit } from "./packet.js"; import { AdbCommand, calculateChecksum } from "./packet.js"; Loading Loading @@ -80,18 +84,18 @@ export class AdbPacketDispatcher implements Closeable { new WritableStream({ write: async (packet) => { switch (packet.command) { case AdbCommand.OK: this.#handleOk(packet); break; case AdbCommand.Close: await this.#handleClose(packet); break; case AdbCommand.Write: await this.#handleWrite(packet); case AdbCommand.Okay: this.#handleOkay(packet); break; case AdbCommand.Open: await this.#handleOpen(packet); break; case AdbCommand.Write: await this.#handleWrite(packet); break; default: // Junk data may only appear in the authentication phase, // since the dispatcher only works after authentication, Loading Loading @@ -125,24 +129,6 @@ export class AdbPacketDispatcher implements Closeable { this.#writer = connection.writable.getWriter(); } #handleOk(packet: AdbPacketData) { if (this.#initializers.resolve(packet.arg1, packet.arg0)) { // Device successfully created the socket return; } const socket = this.#sockets.get(packet.arg1); if (socket) { // Device has received last `WRTE` to the socket socket.ack(); return; } // Maybe the device is responding to a packet of last connection // Tell the device to close the socket void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); } async #handleClose(packet: AdbPacketData) { // If the socket is still pending if ( Loading Loading @@ -170,15 +156,8 @@ export class AdbPacketDispatcher implements Closeable { // Ignore `arg0` and search for the socket const socket = this.#sockets.get(packet.arg1); if (socket) { // The device want to close the socket if (!socket.closed) { await this.sendPacket( AdbCommand.Close, packet.arg1, packet.arg0, ); } await socket.dispose(); await socket.close(); socket.dispose(); this.#sockets.delete(packet.arg1); return; } Loading @@ -188,27 +167,22 @@ export class AdbPacketDispatcher implements Closeable { // the device may also respond with two `CLSE` packets. } async #handleWrite(packet: AdbPacketData) { const socket = this.#sockets.get(packet.arg1); if (!socket) { throw new Error(`Unknown local socket id: ${packet.arg1}`); } await socket.enqueue(packet.payload); await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0); #handleOkay(packet: AdbPacketData) { if (this.#initializers.resolve(packet.arg1, packet.arg0)) { // Device successfully created the socket return; } addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) { this.#incomingSocketHandlers.set(service, handler); } removeReverseTunnel(address: string) { this.#incomingSocketHandlers.delete(address); const socket = this.#sockets.get(packet.arg1); if (socket) { // Device has received last `WRTE` to the socket socket.ack(); return; } clearReverseTunnels() { this.#incomingSocketHandlers.clear(); // Maybe the device is responding to a packet of last connection // Tell the device to close the socket void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); } async #handleOpen(packet: AdbPacketData) { Loading Loading @@ -240,12 +214,41 @@ export class AdbPacketDispatcher implements Closeable { try { await handler(controller.socket); this.#sockets.set(localId, controller); await this.sendPacket(AdbCommand.OK, localId, remoteId); await this.sendPacket(AdbCommand.Okay, localId, remoteId); } catch (e) { await this.sendPacket(AdbCommand.Close, 0, remoteId); } } async #handleWrite(packet: AdbPacketData) { const socket = this.#sockets.get(packet.arg1); if (!socket) { throw new Error(`Unknown local socket id: ${packet.arg1}`); } let handled = false; await Promise.race([ delay(5000).then(() => { if (!handled) { throw new Error( `packet for \`${socket.service}\` not handled in 5 seconds`, ); } }), (async () => { await socket.enqueue(packet.payload); await this.sendPacket( AdbCommand.Okay, packet.arg1, packet.arg0, ); handled = true; })(), ]); return; } async createSocket(service: string): Promise<AdbSocket> { if (this.options.appendNullToServiceString) { service += "\0"; Loading @@ -268,6 +271,18 @@ export class AdbPacketDispatcher implements Closeable { return controller.socket; } addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) { this.#incomingSocketHandlers.set(service, handler); } removeReverseTunnel(address: string) { this.#incomingSocketHandlers.delete(address); } clearReverseTunnels() { this.#incomingSocketHandlers.clear(); } async sendPacket( command: AdbCommand, arg0: number, Loading Loading @@ -306,7 +321,7 @@ export class AdbPacketDispatcher implements Closeable { this.#closed = true; this.#readAbortController.abort(); if (this.options.preserveConnection ?? false) { if (this.options.preserveConnection) { this.#writer.releaseLock(); } else { await this.#writer.close(); Loading @@ -317,7 +332,7 @@ export class AdbPacketDispatcher implements Closeable { #dispose() { for (const socket of this.#sockets.values()) { socket.dispose().catch(unreachable); socket.dispose(); } this.#disconnected.resolve(); Loading libraries/adb/src/daemon/packet.ts +1 −1 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ export enum AdbCommand { Auth = 0x48545541, // 'AUTH' Close = 0x45534c43, // 'CLSE' Connect = 0x4e584e43, // 'CNXN' OK = 0x59414b4f, // 'OKAY' Okay = 0x59414b4f, // 'OKAY' Open = 0x4e45504f, // 'OPEN' Write = 0x45545257, // 'WRTE' } Loading libraries/adb/src/daemon/socket.ts +60 −66 Original line number Diff line number Diff line Loading @@ -5,16 +5,15 @@ import type { PushReadableStreamController, ReadableStream, WritableStream, WritableStreamDefaultController, } from "@yume-chan/stream-extra"; import { ConsumableWritableStream, DistributionStream, DuplexStreamFactory, PushReadableStream, pipeFrom, } from "@yume-chan/stream-extra"; import type { AdbSocket } from "../adb.js"; import { raceSignal } from "../server/index.js"; import type { AdbPacketDispatcher } from "./dispatcher.js"; import { AdbCommand } from "./packet.js"; Loading Loading @@ -44,8 +43,6 @@ export class AdbDaemonSocketController readonly localCreated!: boolean; readonly service!: string; #duplex: DuplexStreamFactory<Uint8Array, Consumable<Uint8Array>>; #readable: ReadableStream<Uint8Array>; #readableController!: PushReadableStreamController<Uint8Array>; get readable() { Loading @@ -53,16 +50,14 @@ export class AdbDaemonSocketController } #writePromise: PromiseResolver<void> | undefined; #writableController!: WritableStreamDefaultController; readonly writable: WritableStream<Consumable<Uint8Array>>; #closed = false; /** * Whether the socket is half-closed (i.e. the local side initiated the close). * * It's only used by dispatcher to avoid sending another `CLSE` packet to remote. */ #closedPromise = new PromiseResolver<void>(); get closed() { return this.#closed; return this.#closedPromise.promise; } #socket: AdbDaemonSocket; Loading @@ -77,66 +72,44 @@ export class AdbDaemonSocketController this.localCreated = options.localCreated; this.service = options.service; // Check this image to help you understand the stream graph // cspell: disable-next-line // https://www.plantuml.com/plantuml/png/TL0zoeGm4ErpYc3l5JxyS0yWM6mX5j4C6p4cxcJ25ejttuGX88ZftizxUKmJI275pGhXl0PP_UkfK_CAz5Z2hcWsW9Ny2fdU4C1f5aSchFVxA8vJjlTPRhqZzDQMRB7AklwJ0xXtX0ZSKH1h24ghoKAdGY23FhxC4nS2pDvxzIvxb-8THU0XlEQJ-ZB7SnXTAvc_LhOckhMdLBnbtndpb-SB7a8q2SRD_W00 this.#duplex = new DuplexStreamFactory< Uint8Array, Consumable<Uint8Array> >({ close: async () => { this.#closed = true; await this.#dispatcher.sendPacket( AdbCommand.Close, this.localId, this.remoteId, ); // Don't `dispose` here, we need to wait for `CLSE` response packet. return false; }, dispose: () => { // Error out the pending writes this.#writePromise?.reject(new Error("Socket closed")); }, this.#readable = new PushReadableStream((controller) => { this.#readableController = controller; }); this.#readable = this.#duplex.wrapReadable( new PushReadableStream( (controller) => { this.#readableController = controller; this.writable = new ConsumableWritableStream<Uint8Array>({ start: (controller) => { this.#writableController = controller; }, { highWaterMark: 0 }, ), ); this.writable = pipeFrom( this.#duplex.createWritable( new ConsumableWritableStream<Uint8Array>({ write: async (chunk) => { // Wait for an ack packet write: async (data, controller) => { const size = data.length; const chunkSize = this.#dispatcher.options.maxPayloadSize; for ( let start = 0, end = chunkSize; start < size; start = end, end += chunkSize ) { this.#writePromise = new PromiseResolver(); await this.#dispatcher.sendPacket( AdbCommand.Write, this.localId, this.remoteId, chunk, data.subarray(start, end), ); await this.#writePromise.promise; }, }), ), new DistributionStream(this.#dispatcher.options.maxPayloadSize), // Wait for ack packet await raceSignal( () => this.#writePromise!.promise, controller.signal, ); } }, }); this.#socket = new AdbDaemonSocket(this); } async enqueue(data: Uint8Array) { // Consumer may abort the `ReadableStream` to close the socket, // it's OK to throw away further packets in this case. // Consumers can `cancel` the `readable` if they are not interested in future data. // Throw away the data if that happens. if (this.#readableController.abortSignal.aborted) { return; } Loading @@ -149,11 +122,32 @@ export class AdbDaemonSocketController } async close(): Promise<void> { await this.#duplex.close(); if (this.#closed) { return; } this.#closed = true; try { this.#writableController.error(new Error("Socket closed")); } catch { // ignore } await this.#dispatcher.sendPacket( AdbCommand.Close, this.localId, this.remoteId, ); } dispose() { return this.#duplex.dispose(); try { this.#readableController.close(); } catch { // ignore } this.#closedPromise.resolve(); } } Loading Loading @@ -188,7 +182,7 @@ export class AdbDaemonSocket implements AdbDaemonSocketInfo, AdbSocket { return this.#controller.writable; } get closed(): boolean { get closed(): Promise<void> { return this.#controller.closed; } Loading libraries/stream-extra/src/buffered.ts +2 −2 Original line number Diff line number Diff line Loading @@ -154,7 +154,7 @@ export class BufferedReadableStream implements AsyncExactReadable { } } cancel(reason?: unknown) { return this.reader.cancel(reason); async cancel(reason?: unknown) { await this.reader.cancel(reason); } } Loading
libraries/adb/src/commands/sync/socket.ts +5 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,10 @@ export class AdbSyncSocketLocked implements AsyncExactReadable { this.#combiner.flush(); this.#socketLock.notifyOne(); } async close() { await this.#readable.cancel(); } } export class AdbSyncSocket { Loading @@ -94,6 +98,7 @@ export class AdbSyncSocket { } async close() { await this.#locked.close(); await this.#socket.close(); } }
libraries/adb/src/daemon/dispatcher.ts +70 −55 Original line number Diff line number Diff line import { AsyncOperationManager, PromiseResolver } from "@yume-chan/async"; import { AsyncOperationManager, PromiseResolver, delay, } from "@yume-chan/async"; import type { Consumable, ReadableWritablePair, Loading @@ -12,7 +16,7 @@ import { import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct"; import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js"; import { decodeUtf8, encodeUtf8, unreachable } from "../utils/index.js"; import { decodeUtf8, encodeUtf8 } from "../utils/index.js"; import type { AdbPacketData, AdbPacketInit } from "./packet.js"; import { AdbCommand, calculateChecksum } from "./packet.js"; Loading Loading @@ -80,18 +84,18 @@ export class AdbPacketDispatcher implements Closeable { new WritableStream({ write: async (packet) => { switch (packet.command) { case AdbCommand.OK: this.#handleOk(packet); break; case AdbCommand.Close: await this.#handleClose(packet); break; case AdbCommand.Write: await this.#handleWrite(packet); case AdbCommand.Okay: this.#handleOkay(packet); break; case AdbCommand.Open: await this.#handleOpen(packet); break; case AdbCommand.Write: await this.#handleWrite(packet); break; default: // Junk data may only appear in the authentication phase, // since the dispatcher only works after authentication, Loading Loading @@ -125,24 +129,6 @@ export class AdbPacketDispatcher implements Closeable { this.#writer = connection.writable.getWriter(); } #handleOk(packet: AdbPacketData) { if (this.#initializers.resolve(packet.arg1, packet.arg0)) { // Device successfully created the socket return; } const socket = this.#sockets.get(packet.arg1); if (socket) { // Device has received last `WRTE` to the socket socket.ack(); return; } // Maybe the device is responding to a packet of last connection // Tell the device to close the socket void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); } async #handleClose(packet: AdbPacketData) { // If the socket is still pending if ( Loading Loading @@ -170,15 +156,8 @@ export class AdbPacketDispatcher implements Closeable { // Ignore `arg0` and search for the socket const socket = this.#sockets.get(packet.arg1); if (socket) { // The device want to close the socket if (!socket.closed) { await this.sendPacket( AdbCommand.Close, packet.arg1, packet.arg0, ); } await socket.dispose(); await socket.close(); socket.dispose(); this.#sockets.delete(packet.arg1); return; } Loading @@ -188,27 +167,22 @@ export class AdbPacketDispatcher implements Closeable { // the device may also respond with two `CLSE` packets. } async #handleWrite(packet: AdbPacketData) { const socket = this.#sockets.get(packet.arg1); if (!socket) { throw new Error(`Unknown local socket id: ${packet.arg1}`); } await socket.enqueue(packet.payload); await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0); #handleOkay(packet: AdbPacketData) { if (this.#initializers.resolve(packet.arg1, packet.arg0)) { // Device successfully created the socket return; } addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) { this.#incomingSocketHandlers.set(service, handler); } removeReverseTunnel(address: string) { this.#incomingSocketHandlers.delete(address); const socket = this.#sockets.get(packet.arg1); if (socket) { // Device has received last `WRTE` to the socket socket.ack(); return; } clearReverseTunnels() { this.#incomingSocketHandlers.clear(); // Maybe the device is responding to a packet of last connection // Tell the device to close the socket void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); } async #handleOpen(packet: AdbPacketData) { Loading Loading @@ -240,12 +214,41 @@ export class AdbPacketDispatcher implements Closeable { try { await handler(controller.socket); this.#sockets.set(localId, controller); await this.sendPacket(AdbCommand.OK, localId, remoteId); await this.sendPacket(AdbCommand.Okay, localId, remoteId); } catch (e) { await this.sendPacket(AdbCommand.Close, 0, remoteId); } } async #handleWrite(packet: AdbPacketData) { const socket = this.#sockets.get(packet.arg1); if (!socket) { throw new Error(`Unknown local socket id: ${packet.arg1}`); } let handled = false; await Promise.race([ delay(5000).then(() => { if (!handled) { throw new Error( `packet for \`${socket.service}\` not handled in 5 seconds`, ); } }), (async () => { await socket.enqueue(packet.payload); await this.sendPacket( AdbCommand.Okay, packet.arg1, packet.arg0, ); handled = true; })(), ]); return; } async createSocket(service: string): Promise<AdbSocket> { if (this.options.appendNullToServiceString) { service += "\0"; Loading @@ -268,6 +271,18 @@ export class AdbPacketDispatcher implements Closeable { return controller.socket; } addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) { this.#incomingSocketHandlers.set(service, handler); } removeReverseTunnel(address: string) { this.#incomingSocketHandlers.delete(address); } clearReverseTunnels() { this.#incomingSocketHandlers.clear(); } async sendPacket( command: AdbCommand, arg0: number, Loading Loading @@ -306,7 +321,7 @@ export class AdbPacketDispatcher implements Closeable { this.#closed = true; this.#readAbortController.abort(); if (this.options.preserveConnection ?? false) { if (this.options.preserveConnection) { this.#writer.releaseLock(); } else { await this.#writer.close(); Loading @@ -317,7 +332,7 @@ export class AdbPacketDispatcher implements Closeable { #dispose() { for (const socket of this.#sockets.values()) { socket.dispose().catch(unreachable); socket.dispose(); } this.#disconnected.resolve(); Loading
libraries/adb/src/daemon/packet.ts +1 −1 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ export enum AdbCommand { Auth = 0x48545541, // 'AUTH' Close = 0x45534c43, // 'CLSE' Connect = 0x4e584e43, // 'CNXN' OK = 0x59414b4f, // 'OKAY' Okay = 0x59414b4f, // 'OKAY' Open = 0x4e45504f, // 'OPEN' Write = 0x45545257, // 'WRTE' } Loading
libraries/adb/src/daemon/socket.ts +60 −66 Original line number Diff line number Diff line Loading @@ -5,16 +5,15 @@ import type { PushReadableStreamController, ReadableStream, WritableStream, WritableStreamDefaultController, } from "@yume-chan/stream-extra"; import { ConsumableWritableStream, DistributionStream, DuplexStreamFactory, PushReadableStream, pipeFrom, } from "@yume-chan/stream-extra"; import type { AdbSocket } from "../adb.js"; import { raceSignal } from "../server/index.js"; import type { AdbPacketDispatcher } from "./dispatcher.js"; import { AdbCommand } from "./packet.js"; Loading Loading @@ -44,8 +43,6 @@ export class AdbDaemonSocketController readonly localCreated!: boolean; readonly service!: string; #duplex: DuplexStreamFactory<Uint8Array, Consumable<Uint8Array>>; #readable: ReadableStream<Uint8Array>; #readableController!: PushReadableStreamController<Uint8Array>; get readable() { Loading @@ -53,16 +50,14 @@ export class AdbDaemonSocketController } #writePromise: PromiseResolver<void> | undefined; #writableController!: WritableStreamDefaultController; readonly writable: WritableStream<Consumable<Uint8Array>>; #closed = false; /** * Whether the socket is half-closed (i.e. the local side initiated the close). * * It's only used by dispatcher to avoid sending another `CLSE` packet to remote. */ #closedPromise = new PromiseResolver<void>(); get closed() { return this.#closed; return this.#closedPromise.promise; } #socket: AdbDaemonSocket; Loading @@ -77,66 +72,44 @@ export class AdbDaemonSocketController this.localCreated = options.localCreated; this.service = options.service; // Check this image to help you understand the stream graph // cspell: disable-next-line // https://www.plantuml.com/plantuml/png/TL0zoeGm4ErpYc3l5JxyS0yWM6mX5j4C6p4cxcJ25ejttuGX88ZftizxUKmJI275pGhXl0PP_UkfK_CAz5Z2hcWsW9Ny2fdU4C1f5aSchFVxA8vJjlTPRhqZzDQMRB7AklwJ0xXtX0ZSKH1h24ghoKAdGY23FhxC4nS2pDvxzIvxb-8THU0XlEQJ-ZB7SnXTAvc_LhOckhMdLBnbtndpb-SB7a8q2SRD_W00 this.#duplex = new DuplexStreamFactory< Uint8Array, Consumable<Uint8Array> >({ close: async () => { this.#closed = true; await this.#dispatcher.sendPacket( AdbCommand.Close, this.localId, this.remoteId, ); // Don't `dispose` here, we need to wait for `CLSE` response packet. return false; }, dispose: () => { // Error out the pending writes this.#writePromise?.reject(new Error("Socket closed")); }, this.#readable = new PushReadableStream((controller) => { this.#readableController = controller; }); this.#readable = this.#duplex.wrapReadable( new PushReadableStream( (controller) => { this.#readableController = controller; this.writable = new ConsumableWritableStream<Uint8Array>({ start: (controller) => { this.#writableController = controller; }, { highWaterMark: 0 }, ), ); this.writable = pipeFrom( this.#duplex.createWritable( new ConsumableWritableStream<Uint8Array>({ write: async (chunk) => { // Wait for an ack packet write: async (data, controller) => { const size = data.length; const chunkSize = this.#dispatcher.options.maxPayloadSize; for ( let start = 0, end = chunkSize; start < size; start = end, end += chunkSize ) { this.#writePromise = new PromiseResolver(); await this.#dispatcher.sendPacket( AdbCommand.Write, this.localId, this.remoteId, chunk, data.subarray(start, end), ); await this.#writePromise.promise; }, }), ), new DistributionStream(this.#dispatcher.options.maxPayloadSize), // Wait for ack packet await raceSignal( () => this.#writePromise!.promise, controller.signal, ); } }, }); this.#socket = new AdbDaemonSocket(this); } async enqueue(data: Uint8Array) { // Consumer may abort the `ReadableStream` to close the socket, // it's OK to throw away further packets in this case. // Consumers can `cancel` the `readable` if they are not interested in future data. // Throw away the data if that happens. if (this.#readableController.abortSignal.aborted) { return; } Loading @@ -149,11 +122,32 @@ export class AdbDaemonSocketController } async close(): Promise<void> { await this.#duplex.close(); if (this.#closed) { return; } this.#closed = true; try { this.#writableController.error(new Error("Socket closed")); } catch { // ignore } await this.#dispatcher.sendPacket( AdbCommand.Close, this.localId, this.remoteId, ); } dispose() { return this.#duplex.dispose(); try { this.#readableController.close(); } catch { // ignore } this.#closedPromise.resolve(); } } Loading Loading @@ -188,7 +182,7 @@ export class AdbDaemonSocket implements AdbDaemonSocketInfo, AdbSocket { return this.#controller.writable; } get closed(): boolean { get closed(): Promise<void> { return this.#controller.closed; } Loading
libraries/stream-extra/src/buffered.ts +2 −2 Original line number Diff line number Diff line Loading @@ -154,7 +154,7 @@ export class BufferedReadableStream implements AsyncExactReadable { } } cancel(reason?: unknown) { return this.reader.cancel(reason); async cancel(reason?: unknown) { await this.reader.cancel(reason); } }