fix(webusb): try to discard lingering data

fixes #597
This commit is contained in:
Simon Chan 2023-09-08 01:28:31 +08:00
parent 2743a9088f
commit 2d1cf1fafa
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
3 changed files with 84 additions and 42 deletions

View file

@ -99,6 +99,11 @@ class Uint8ArrayExactReadable implements ExactReadable {
export class AdbDaemonWebUsbConnection export class AdbDaemonWebUsbConnection
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>> implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
{ {
#device: AdbDaemonWebUsbDevice;
get device() {
return this.#device;
}
#readable: ReadableStream<AdbPacketData>; #readable: ReadableStream<AdbPacketData>;
get readable() { get readable() {
return this.#readable; return this.#readable;
@ -110,11 +115,13 @@ export class AdbDaemonWebUsbConnection
} }
constructor( constructor(
device: USBDevice, device: AdbDaemonWebUsbDevice,
inEndpoint: USBEndpoint, inEndpoint: USBEndpoint,
outEndpoint: USBEndpoint, outEndpoint: USBEndpoint,
usbManager: USB, usbManager: USB,
) { ) {
this.#device = device;
let closed = false; let closed = false;
const duplex = new DuplexStreamFactory< const duplex = new DuplexStreamFactory<
@ -124,7 +131,7 @@ export class AdbDaemonWebUsbConnection
close: async () => { close: async () => {
try { try {
closed = true; closed = true;
await device.close(); await device.raw.close();
} catch { } catch {
/* device may have already disconnected */ /* device may have already disconnected */
} }
@ -139,7 +146,7 @@ export class AdbDaemonWebUsbConnection
}); });
function handleUsbDisconnect(e: USBConnectionEvent) { function handleUsbDisconnect(e: USBConnectionEvent) {
if (e.device === device) { if (e.device === device.raw) {
duplex.dispose().catch(unreachable); duplex.dispose().catch(unreachable);
} }
} }
@ -150,37 +157,63 @@ export class AdbDaemonWebUsbConnection
new ReadableStream<AdbPacketData>({ new ReadableStream<AdbPacketData>({
async pull(controller) { async pull(controller) {
try { try {
// The `length` argument in `transferIn` must not be smaller than what the device sent, while (true) {
// otherwise it will return `babble` status without any data. // The `length` argument in `transferIn` must not be smaller than what the device sent,
// ADB daemon sends each packet in two parts, the 24-byte header and the payload. // otherwise it will return `babble` status without any data.
const result = await device.transferIn( // ADB daemon sends each packet in two parts, the 24-byte header and the payload.
inEndpoint.endpointNumber, const result = await device.raw.transferIn(
24,
);
// TODO: webusb: handle `babble` by discarding the data and receive again
// Per spec, the `result.data` always covers the whole `buffer`.
const buffer = new Uint8Array(result.data!.buffer);
const stream = new Uint8ArrayExactReadable(buffer);
// Add `payload` field to its type, it's assigned below.
const packet = AdbPacketHeader.deserialize(
stream,
) as AdbPacketHeader & { payload: Uint8Array };
if (packet.payloadLength !== 0) {
const result = await device.transferIn(
inEndpoint.endpointNumber, inEndpoint.endpointNumber,
packet.payloadLength, 24,
); );
packet.payload = new Uint8Array(
result.data!.buffer,
);
} else {
packet.payload = EMPTY_UINT8_ARRAY;
}
controller.enqueue(packet); // 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,
);
continue;
}
// Per spec, the `result.data` always covers the whole `buffer`.
const buffer = new Uint8Array(result.data!.buffer);
const stream = new Uint8ArrayExactReadable(buffer);
// Add `payload` field to its type, it's assigned below.
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,
);
continue;
}
const result = await device.raw.transferIn(
inEndpoint.endpointNumber,
packet.payloadLength,
);
packet.payload = new Uint8Array(
result.data!.buffer,
);
} else {
packet.payload = EMPTY_UINT8_ARRAY;
}
controller.enqueue(packet);
return;
}
} catch (e) { } catch (e) {
// On Windows, disconnecting the device will cause `NetworkError` to be thrown, // On Windows, disconnecting the device will cause `NetworkError` to be thrown,
// even before the `disconnect` event is fired. // even before the `disconnect` event is fired.
@ -212,7 +245,7 @@ export class AdbDaemonWebUsbConnection
new ConsumableWritableStream({ new ConsumableWritableStream({
write: async (chunk) => { write: async (chunk) => {
try { try {
await device.transferOut( await device.raw.transferOut(
outEndpoint.endpointNumber, outEndpoint.endpointNumber,
chunk, chunk,
); );
@ -225,7 +258,7 @@ export class AdbDaemonWebUsbConnection
zeroMask && zeroMask &&
(chunk.byteLength & zeroMask) === 0 (chunk.byteLength & zeroMask) === 0
) { ) {
await device.transferOut( await device.raw.transferOut(
outEndpoint.endpointNumber, outEndpoint.endpointNumber,
EMPTY_UINT8_ARRAY, EMPTY_UINT8_ARRAY,
); );
@ -281,9 +314,7 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
* Claim the device and create a pair of `AdbPacket` streams to the ADB interface. * Claim the device and create a pair of `AdbPacket` streams to the ADB interface.
* @returns The pair of `AdbPacket` streams. * @returns The pair of `AdbPacket` streams.
*/ */
async connect(): Promise< async connect(): Promise<AdbDaemonWebUsbConnection> {
ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
> {
if (!this.#raw.opened) { if (!this.#raw.opened) {
await this.#raw.open(); await this.#raw.open();
} }
@ -319,7 +350,7 @@ export class AdbDaemonWebUsbDevice implements AdbDaemonDevice {
alternate.endpoints, alternate.endpoints,
); );
return new AdbDaemonWebUsbConnection( return new AdbDaemonWebUsbConnection(
this.#raw, this,
inEndpoint, inEndpoint,
outEndpoint, outEndpoint,
this.#usbManager, this.#usbManager,

View file

@ -27,9 +27,14 @@ import { AdbCommand, calculateChecksum } from "./packet.js";
export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001; export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001;
export type AdbDaemonConnection = ReadableWritablePair<
AdbPacketData,
Consumable<AdbPacketInit>
>;
interface AdbDaemonAuthenticationOptions { interface AdbDaemonAuthenticationOptions {
serial: string; serial: string;
connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>; connection: AdbDaemonConnection;
credentialStore: AdbCredentialStore; credentialStore: AdbCredentialStore;
authenticators?: AdbAuthenticator[]; authenticators?: AdbAuthenticator[];
/** /**
@ -40,7 +45,7 @@ interface AdbDaemonAuthenticationOptions {
interface AdbDaemonSocketConnectorConstructionOptions { interface AdbDaemonSocketConnectorConstructionOptions {
serial: string; serial: string;
connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>; connection: AdbDaemonConnection;
version: number; version: number;
maxPayloadSize: number; maxPayloadSize: number;
banner: string; banner: string;
@ -94,9 +99,8 @@ export class AdbDaemonTransport implements AdbTransport {
resolver.resolve(decodeUtf8(packet.payload)); resolver.resolve(decodeUtf8(packet.payload));
break; break;
case AdbCommand.Auth: { case AdbCommand.Auth: {
const response = await authProcessor.process( const response =
packet, await authProcessor.process(packet);
);
await sendPacket(response); await sendPacket(response);
break; break;
} }
@ -193,6 +197,11 @@ export class AdbDaemonTransport implements AdbTransport {
}); });
} }
#connection: AdbDaemonConnection;
get connection() {
return this.#connection;
}
readonly #dispatcher: AdbPacketDispatcher; readonly #dispatcher: AdbPacketDispatcher;
#serial: string; #serial: string;
@ -228,6 +237,7 @@ export class AdbDaemonTransport implements AdbTransport {
preserveConnection, preserveConnection,
}: AdbDaemonSocketConnectorConstructionOptions) { }: AdbDaemonSocketConnectorConstructionOptions) {
this.#serial = serial; this.#serial = serial;
this.#connection = connection;
this.#banner = AdbBanner.parse(banner); this.#banner = AdbBanner.parse(banner);
let calculateChecksum: boolean; let calculateChecksum: boolean;

View file

@ -9,3 +9,4 @@ export * from "./logcat.js";
export * from "./overlay-display.js"; export * from "./overlay-display.js";
export * from "./pm.js"; export * from "./pm.js";
export * from "./settings.js"; export * from "./settings.js";
export * from "./string-format.js";