Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit e1b0444c authored by Simon Chan's avatar Simon Chan
Browse files

fix(webusb): use packet size in `transferIn` to fix `babble` status on non-Windows

fix #395
parent 67fcb5b0
Loading
Loading
Loading
Loading
+36 −24
Original line number Diff line number Diff line
import { AdbPacket, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, ReadableStream, type AdbBackend, type AdbPacketCore, type AdbPacketInit, type ReadableWritablePair, type WritableStream } from '@yume-chan/adb';
import type { StructAsyncDeserializeStream } from "@yume-chan/struct";
import { AdbPacketHeader, AdbPacketSerializeStream, DuplexStreamFactory, pipeFrom, ReadableStream, type AdbBackend, type AdbPacketCore, type AdbPacketInit, type ReadableWritablePair, type WritableStream } from '@yume-chan/adb';
import type { StructDeserializeStream } from "@yume-chan/struct";

export const ADB_DEVICE_FILTER: USBDeviceFilter = {
    classCode: 0xFF,
@@ -7,6 +7,23 @@ export const ADB_DEVICE_FILTER: USBDeviceFilter = {
    protocolCode: 1,
};

class Uint8ArrayStructDeserializeStream implements StructDeserializeStream {
    private buffer: Uint8Array;

    private offset: number;

    public constructor(buffer: Uint8Array) {
        this.buffer = buffer;
        this.offset = 0;
    }

    public read(length: number): Uint8Array {
        const result = this.buffer.subarray(this.offset, this.offset + length);
        this.offset += length;
        return result;
    }
}

export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketCore, AdbPacketInit>{
    private _readable: ReadableStream<AdbPacketCore>;
    public get readable() { return this._readable; }
@@ -34,30 +51,25 @@ export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketCor

        navigator.usb.addEventListener('disconnect', handleUsbDisconnect);

        const incomingStream: StructAsyncDeserializeStream = {
            async read(length) {
                // `ReadableStream<Uin8Array>` don't know how many bytes the consumer need in each `pull`,
                // But `transferIn(endpointNumber, packetSize)` is much slower than `transferIn(endpointNumber, length)`
                // So `AdbBackend` is refactored to use `ReadableStream<AdbPacketCore>` directly,
                // (let each backend deserialize the packets in their own way)
                const result = await device.transferIn(inEndpoint.endpointNumber, length);

                // TODO: The WebUSB spec requires `transferIn` to return `status: babble` when
                // the `length` argument is smaller than what the device actually returns.
                // But Chrome's implementation on Windows never returns `babble` because WinUSB
                // allows partial reads by default.
                // When `Struct.deserialize` calls this `read`, the `length` is each fields' length,
                // instead of packet length. So this only works on Windows.

                // From spec, the `result.data` always covers the whole `buffer`.
                return new Uint8Array(result.data!.buffer);
            }
        };

        this._readable = factory.createWrapReadable(new ReadableStream<AdbPacketCore>({
            async pull(controller) {
                const value = await AdbPacket.deserialize(incomingStream);
                controller.enqueue(value);
                // The `length` argument in `transferIn` must not be smaller than what the device sent,
                // otherwise it will return `babble` status without any data.
                // But using `inEndpoint.packetSize` as `length` (ensures it can read packets in any size)
                // leads to poor performance due to unnecessarily large allocations and corresponding GCs.
                // So we read exactly 24 bytes (packet header) followed by exactly `payloadLength`.
                const result = await device.transferIn(inEndpoint.endpointNumber, 24);
                // TODO: webusb-backend: handle `babble` by discarding the data and receive again
                // From spec, the `result.data` always covers the whole `buffer`.
                const buffer = new Uint8Array(result.data!.buffer);
                const stream = new Uint8ArrayStructDeserializeStream(buffer);
                const packet = AdbPacketHeader.deserialize(stream);
                if (packet.payloadLength !== 0) {
                    const payload = await device.transferIn(inEndpoint.endpointNumber, packet.payloadLength!);
                    // Use the cast to avoid allocate another object.
                    (packet as unknown as AdbPacketCore).payload = new Uint8Array(payload.data!.buffer);
                }
                controller.enqueue(packet as unknown as AdbPacketCore);
            },
        }));

+4 −2
Original line number Diff line number Diff line
@@ -96,7 +96,7 @@ Syncbird.config({
const _then = (Syncbird.prototype as any)._then;
Syncbird.prototype._then = function <T, TResult1 = T, TResult2 = never>(
    this: Syncbird<T>,
    onfulfilled: ((value: T) => unknown) | undefined | null,
    onfulfilled: ((value: T, internalData?: unknown) => unknown) | undefined | null,
    onrejected: ((reason: any) => unknown) | undefined | null,
    _: never,
    receiver: unknown,
@@ -110,7 +110,9 @@ Syncbird.prototype._then = function <T, TResult1 = T, TResult2 = never>(
            return Syncbird.resolve(
                onfulfilled.call(
                    receiver,
                    this.value()
                    this.value(),
                    // Some Bluebird internal methods (for example `reduce`) need this `internalData`
                    internalData
                )
            );
        }