Loading libraries/adb-daemon-webusb/src/device.ts +74 −80 Original line number Diff line number Diff line Loading @@ -104,6 +104,8 @@ export class AdbDaemonWebUsbConnection return this.#device; } #inEndpoint: USBEndpoint; #readable: ReadableStream<AdbPacketData>; get readable() { return this.#readable; Loading @@ -121,6 +123,7 @@ export class AdbDaemonWebUsbConnection usbManager: USB, ) { this.#device = device; this.#inEndpoint = inEndpoint; let closed = false; Loading Loading @@ -154,26 +157,68 @@ export class AdbDaemonWebUsbConnection usbManager.addEventListener("disconnect", handleUsbDisconnect); this.#readable = duplex.wrapReadable( new ReadableStream<AdbPacketData>({ async pull(controller) { new ReadableStream<AdbPacketData>( { pull: async (controller) => { const packet = await this.#transferIn(); if (packet) { controller.enqueue(packet); } else { controller.close(); } }, }, { highWaterMark: 0 }, ), ); const zeroMask = outEndpoint.packetSize - 1; this.#writable = pipeFrom( duplex.createWritable( new ConsumableWritableStream({ write: async (chunk) => { try { await device.raw.transferOut( outEndpoint.endpointNumber, chunk, ); // In USB protocol, a not-full packet indicates the end of a transfer. // If the payload size is a multiple of the packet size, // we need to send an empty packet to indicate the end, // so the OS will send it to the device immediately. if ( zeroMask && (chunk.byteLength & zeroMask) === 0 ) { await device.raw.transferOut( outEndpoint.endpointNumber, EMPTY_UINT8_ARRAY, ); } } catch (e) { if (closed) { return; } throw e; } }, }), ), new AdbPacketSerializeStream(), ); } async #transferIn(): Promise<AdbPacketData | undefined> { try { while (true) { // The `length` argument in `transferIn` must not be smaller than what the device sent, // otherwise it will return `babble` status without any data. // ADB daemon sends each packet in two parts, the 24-byte header and the payload. const result = await device.raw.transferIn( inEndpoint.endpointNumber, 24, const result = await this.#device.raw.transferIn( this.#inEndpoint.endpointNumber, this.#inEndpoint.packetSize, ); // Maximum payload size is 1MB, so reading 1MB data will always success, // and always discards all lingering data. // FIXME: Chrome on Windows doesn't support babble status. See the HACK below. if (result.status === "babble") { await device.raw.transferIn( inEndpoint.endpointNumber, 1024 * 1024, ); if (result.data!.byteLength !== 24) { continue; } Loading @@ -185,34 +230,22 @@ export class AdbDaemonWebUsbConnection const packet = AdbPacketHeader.deserialize( stream, ) as AdbPacketHeader & { payload: Uint8Array }; if (packet.payloadLength !== 0) { // HACK: Chrome on Windows doesn't support babble status, // so maybe we are not actually reading an ADB packet header. // Currently the maximum payload size is 1MB, // so if the payload length is larger than that, // try to discard the data and receive again. // https://crbug.com/1314358 if (packet.payloadLength > 1024 * 1024) { await device.raw.transferIn( inEndpoint.endpointNumber, 1024 * 1024, ); if (packet.magic !== (packet.command ^ 0xffffffff)) { continue; } const result = await device.raw.transferIn( inEndpoint.endpointNumber, if (packet.payloadLength !== 0) { const result = await this.#device.raw.transferIn( this.#inEndpoint.endpointNumber, packet.payloadLength, ); packet.payload = new Uint8Array( result.data!.buffer, ); packet.payload = new Uint8Array(result.data!.buffer); } else { packet.payload = EMPTY_UINT8_ARRAY; } controller.enqueue(packet); return; return packet; } } catch (e) { // On Windows, disconnecting the device will cause `NetworkError` to be thrown, Loading @@ -227,7 +260,7 @@ export class AdbDaemonWebUsbConnection }); if (closed) { controller.close(); return undefined; } else { throw e; } Loading @@ -235,45 +268,6 @@ export class AdbDaemonWebUsbConnection throw e; } }, }), ); const zeroMask = outEndpoint.packetSize - 1; this.#writable = pipeFrom( duplex.createWritable( new ConsumableWritableStream({ write: async (chunk) => { try { await device.raw.transferOut( outEndpoint.endpointNumber, chunk, ); // In USB protocol, a not-full packet indicates the end of a transfer. // If the payload size is a multiple of the packet size, // we need to send an empty packet to indicate the end, // so the OS will send it to the device immediately. if ( zeroMask && (chunk.byteLength & zeroMask) === 0 ) { await device.raw.transferOut( outEndpoint.endpointNumber, EMPTY_UINT8_ARRAY, ); } } catch (e) { if (closed) { return; } throw e; } }, }), ), new AdbPacketSerializeStream(), ); } } Loading libraries/adb/src/daemon/dispatcher.ts +13 −14 Original line number Diff line number Diff line Loading @@ -87,20 +87,8 @@ export class AdbPacketDispatcher implements Closeable { await this.#handleClose(packet); break; case AdbCommand.Write: if (this.#sockets.has(packet.arg1)) { await this.#sockets .get(packet.arg1)! .enqueue(packet.payload); await this.sendPacket( AdbCommand.OK, packet.arg1, packet.arg0, ); await this.#handleWrite(packet); break; } throw new Error( `Unknown local socket id: ${packet.arg1}`, ); case AdbCommand.Open: await this.#handleOpen(packet); break; Loading Loading @@ -200,6 +188,17 @@ 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); return; } addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) { this.#incomingSocketHandlers.set(service, handler); } Loading libraries/adb/src/daemon/socket.ts +1 −6 Original line number Diff line number Diff line Loading @@ -108,12 +108,7 @@ export class AdbDaemonSocketController (controller) => { this.#readableController = controller; }, { highWaterMark: options.highWaterMark ?? 16 * 1024, size(chunk) { return chunk.byteLength; }, }, { highWaterMark: 0 }, ), ); Loading libraries/stream-extra/src/buffered.ts +20 −13 Original line number Diff line number Diff line Loading @@ -31,7 +31,6 @@ export class BufferedReadableStream implements AsyncExactReadable { if (done) { throw new ExactReadableEndedError(); } this.#position += value.byteLength; return value; } Loading @@ -42,45 +41,51 @@ export class BufferedReadableStream implements AsyncExactReadable { if (initial) { result = new Uint8Array(length); result.set(initial); index = initial.byteLength; length -= initial.byteLength; index = initial.length; length -= initial.length; } else { const array = await this.#readSource(); if (array.byteLength === length) { if (array.length === length) { this.#position += length; return array; } if (array.byteLength > length) { if (array.length > length) { this.#buffered = array; this.#bufferedOffset = length; this.#bufferedLength = array.byteLength - length; this.#bufferedLength = array.length - length; this.#position += length; return array.subarray(0, length); } result = new Uint8Array(length); result.set(array); index = array.byteLength; length -= array.byteLength; index = array.length; length -= array.length; this.#position += array.length; } while (length > 0) { const array = await this.#readSource(); if (array.byteLength === length) { if (array.length === length) { result.set(array, index); this.#position += length; return result; } if (array.byteLength > length) { if (array.length > length) { this.#buffered = array; this.#bufferedOffset = length; this.#bufferedLength = array.byteLength - length; this.#bufferedLength = array.length - length; result.set(array.subarray(0, length), index); this.#position += length; return result; } result.set(array, index); index += array.byteLength; length -= array.byteLength; index += array.length; length -= array.length; this.#position += array.length; } return result; Loading @@ -101,12 +106,14 @@ export class BufferedReadableStream implements AsyncExactReadable { // don't use it until absolutely necessary this.#bufferedOffset += length; this.#bufferedLength -= length; this.#position += length; return array.subarray(offset, offset + length); } this.#buffered = undefined; this.#bufferedLength = 0; this.#bufferedOffset = 0; this.#position += array.length - offset; return this.#readAsync(length, array.subarray(offset)); } Loading libraries/stream-extra/src/duplex.ts +21 −14 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import { PromiseResolver } from "@yume-chan/async"; import type { ValueOrPromise } from "@yume-chan/struct"; import type { QueuingStrategy, ReadableStream, ReadableStreamDefaultController, WritableStreamDefaultWriter, Loading Loading @@ -65,8 +66,12 @@ export class DuplexStreamFactory<R, W> { this.#options = options ?? {}; } wrapReadable(readable: ReadableStream<R>): WrapReadableStream<R> { return new WrapReadableStream<R>({ wrapReadable( readable: ReadableStream<R>, strategy?: QueuingStrategy<R>, ): WrapReadableStream<R> { return new WrapReadableStream<R>( { start: (controller) => { this.#readableControllers.push(controller); return readable; Loading @@ -79,7 +84,9 @@ export class DuplexStreamFactory<R, W> { // stream end means the remote peer closed the connection first. await this.dispose(); }, }); }, strategy, ); } createWritable(stream: WritableStream<W>): WritableStream<W> { Loading Loading
libraries/adb-daemon-webusb/src/device.ts +74 −80 Original line number Diff line number Diff line Loading @@ -104,6 +104,8 @@ export class AdbDaemonWebUsbConnection return this.#device; } #inEndpoint: USBEndpoint; #readable: ReadableStream<AdbPacketData>; get readable() { return this.#readable; Loading @@ -121,6 +123,7 @@ export class AdbDaemonWebUsbConnection usbManager: USB, ) { this.#device = device; this.#inEndpoint = inEndpoint; let closed = false; Loading Loading @@ -154,26 +157,68 @@ export class AdbDaemonWebUsbConnection usbManager.addEventListener("disconnect", handleUsbDisconnect); this.#readable = duplex.wrapReadable( new ReadableStream<AdbPacketData>({ async pull(controller) { new ReadableStream<AdbPacketData>( { pull: async (controller) => { const packet = await this.#transferIn(); if (packet) { controller.enqueue(packet); } else { controller.close(); } }, }, { highWaterMark: 0 }, ), ); const zeroMask = outEndpoint.packetSize - 1; this.#writable = pipeFrom( duplex.createWritable( new ConsumableWritableStream({ write: async (chunk) => { try { await device.raw.transferOut( outEndpoint.endpointNumber, chunk, ); // In USB protocol, a not-full packet indicates the end of a transfer. // If the payload size is a multiple of the packet size, // we need to send an empty packet to indicate the end, // so the OS will send it to the device immediately. if ( zeroMask && (chunk.byteLength & zeroMask) === 0 ) { await device.raw.transferOut( outEndpoint.endpointNumber, EMPTY_UINT8_ARRAY, ); } } catch (e) { if (closed) { return; } throw e; } }, }), ), new AdbPacketSerializeStream(), ); } async #transferIn(): Promise<AdbPacketData | undefined> { try { while (true) { // The `length` argument in `transferIn` must not be smaller than what the device sent, // otherwise it will return `babble` status without any data. // ADB daemon sends each packet in two parts, the 24-byte header and the payload. const result = await device.raw.transferIn( inEndpoint.endpointNumber, 24, const result = await this.#device.raw.transferIn( this.#inEndpoint.endpointNumber, this.#inEndpoint.packetSize, ); // Maximum payload size is 1MB, so reading 1MB data will always success, // and always discards all lingering data. // FIXME: Chrome on Windows doesn't support babble status. See the HACK below. if (result.status === "babble") { await device.raw.transferIn( inEndpoint.endpointNumber, 1024 * 1024, ); if (result.data!.byteLength !== 24) { continue; } Loading @@ -185,34 +230,22 @@ export class AdbDaemonWebUsbConnection const packet = AdbPacketHeader.deserialize( stream, ) as AdbPacketHeader & { payload: Uint8Array }; if (packet.payloadLength !== 0) { // HACK: Chrome on Windows doesn't support babble status, // so maybe we are not actually reading an ADB packet header. // Currently the maximum payload size is 1MB, // so if the payload length is larger than that, // try to discard the data and receive again. // https://crbug.com/1314358 if (packet.payloadLength > 1024 * 1024) { await device.raw.transferIn( inEndpoint.endpointNumber, 1024 * 1024, ); if (packet.magic !== (packet.command ^ 0xffffffff)) { continue; } const result = await device.raw.transferIn( inEndpoint.endpointNumber, if (packet.payloadLength !== 0) { const result = await this.#device.raw.transferIn( this.#inEndpoint.endpointNumber, packet.payloadLength, ); packet.payload = new Uint8Array( result.data!.buffer, ); packet.payload = new Uint8Array(result.data!.buffer); } else { packet.payload = EMPTY_UINT8_ARRAY; } controller.enqueue(packet); return; return packet; } } catch (e) { // On Windows, disconnecting the device will cause `NetworkError` to be thrown, Loading @@ -227,7 +260,7 @@ export class AdbDaemonWebUsbConnection }); if (closed) { controller.close(); return undefined; } else { throw e; } Loading @@ -235,45 +268,6 @@ export class AdbDaemonWebUsbConnection throw e; } }, }), ); const zeroMask = outEndpoint.packetSize - 1; this.#writable = pipeFrom( duplex.createWritable( new ConsumableWritableStream({ write: async (chunk) => { try { await device.raw.transferOut( outEndpoint.endpointNumber, chunk, ); // In USB protocol, a not-full packet indicates the end of a transfer. // If the payload size is a multiple of the packet size, // we need to send an empty packet to indicate the end, // so the OS will send it to the device immediately. if ( zeroMask && (chunk.byteLength & zeroMask) === 0 ) { await device.raw.transferOut( outEndpoint.endpointNumber, EMPTY_UINT8_ARRAY, ); } } catch (e) { if (closed) { return; } throw e; } }, }), ), new AdbPacketSerializeStream(), ); } } Loading
libraries/adb/src/daemon/dispatcher.ts +13 −14 Original line number Diff line number Diff line Loading @@ -87,20 +87,8 @@ export class AdbPacketDispatcher implements Closeable { await this.#handleClose(packet); break; case AdbCommand.Write: if (this.#sockets.has(packet.arg1)) { await this.#sockets .get(packet.arg1)! .enqueue(packet.payload); await this.sendPacket( AdbCommand.OK, packet.arg1, packet.arg0, ); await this.#handleWrite(packet); break; } throw new Error( `Unknown local socket id: ${packet.arg1}`, ); case AdbCommand.Open: await this.#handleOpen(packet); break; Loading Loading @@ -200,6 +188,17 @@ 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); return; } addReverseTunnel(service: string, handler: AdbIncomingSocketHandler) { this.#incomingSocketHandlers.set(service, handler); } Loading
libraries/adb/src/daemon/socket.ts +1 −6 Original line number Diff line number Diff line Loading @@ -108,12 +108,7 @@ export class AdbDaemonSocketController (controller) => { this.#readableController = controller; }, { highWaterMark: options.highWaterMark ?? 16 * 1024, size(chunk) { return chunk.byteLength; }, }, { highWaterMark: 0 }, ), ); Loading
libraries/stream-extra/src/buffered.ts +20 −13 Original line number Diff line number Diff line Loading @@ -31,7 +31,6 @@ export class BufferedReadableStream implements AsyncExactReadable { if (done) { throw new ExactReadableEndedError(); } this.#position += value.byteLength; return value; } Loading @@ -42,45 +41,51 @@ export class BufferedReadableStream implements AsyncExactReadable { if (initial) { result = new Uint8Array(length); result.set(initial); index = initial.byteLength; length -= initial.byteLength; index = initial.length; length -= initial.length; } else { const array = await this.#readSource(); if (array.byteLength === length) { if (array.length === length) { this.#position += length; return array; } if (array.byteLength > length) { if (array.length > length) { this.#buffered = array; this.#bufferedOffset = length; this.#bufferedLength = array.byteLength - length; this.#bufferedLength = array.length - length; this.#position += length; return array.subarray(0, length); } result = new Uint8Array(length); result.set(array); index = array.byteLength; length -= array.byteLength; index = array.length; length -= array.length; this.#position += array.length; } while (length > 0) { const array = await this.#readSource(); if (array.byteLength === length) { if (array.length === length) { result.set(array, index); this.#position += length; return result; } if (array.byteLength > length) { if (array.length > length) { this.#buffered = array; this.#bufferedOffset = length; this.#bufferedLength = array.byteLength - length; this.#bufferedLength = array.length - length; result.set(array.subarray(0, length), index); this.#position += length; return result; } result.set(array, index); index += array.byteLength; length -= array.byteLength; index += array.length; length -= array.length; this.#position += array.length; } return result; Loading @@ -101,12 +106,14 @@ export class BufferedReadableStream implements AsyncExactReadable { // don't use it until absolutely necessary this.#bufferedOffset += length; this.#bufferedLength -= length; this.#position += length; return array.subarray(offset, offset + length); } this.#buffered = undefined; this.#bufferedLength = 0; this.#bufferedOffset = 0; this.#position += array.length - offset; return this.#readAsync(length, array.subarray(offset)); } Loading
libraries/stream-extra/src/duplex.ts +21 −14 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import { PromiseResolver } from "@yume-chan/async"; import type { ValueOrPromise } from "@yume-chan/struct"; import type { QueuingStrategy, ReadableStream, ReadableStreamDefaultController, WritableStreamDefaultWriter, Loading Loading @@ -65,8 +66,12 @@ export class DuplexStreamFactory<R, W> { this.#options = options ?? {}; } wrapReadable(readable: ReadableStream<R>): WrapReadableStream<R> { return new WrapReadableStream<R>({ wrapReadable( readable: ReadableStream<R>, strategy?: QueuingStrategy<R>, ): WrapReadableStream<R> { return new WrapReadableStream<R>( { start: (controller) => { this.#readableControllers.push(controller); return readable; Loading @@ -79,7 +84,9 @@ export class DuplexStreamFactory<R, W> { // stream end means the remote peer closed the connection first. await this.dispose(); }, }); }, strategy, ); } createWritable(stream: WritableStream<W>): WritableStream<W> { Loading