doc(adb): add note about top-level await

This commit is contained in:
Simon Chan 2022-02-19 22:41:21 +08:00
parent 5bc3d49334
commit 5aab894875
25 changed files with 160 additions and 93 deletions

View file

@ -36,18 +36,19 @@ TypeScript implementation of Android Debug Bridge (ADB) protocol.
## Compatibility
This table only applies to this library itself. Specific backend may require higher runtime versions.
This table only applies to this library itself. Specific backend may have different requirements.
This library only uses standard JavaScript features.
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
| --------------- | ------ | ---- | ------- | ----------------- | ------------------ | -------------------- |
| **Basic usage** | 68 | 79 | 68 | No | 14<sup>1</sup>, 15 | 10.4<sup>2</sup>, 11 |
| | Chrome | Edge | Firefox | Internet Explorer | Safari | Node.js |
| -------------------------------- | ------ | ---- | ------- | ----------------- | ------------------ | -------------------- |
| Basic usage | 68 | 79 | 68 | No | 14<sup>1</sup>, 15 | 10.4<sup>2</sup>, 11 |
| Use without bundlers<sup>3</sup> | 89 | 89 | 89 | No | 15 | 14.8 |
<sup>1</sup> Requires a polyfill for `DataView#getBigInt64`, `DataView#getBigUint64`, `DataView#setBigInt64` and `DataView#setBigUint64`
<sup>2</sup> `TextEncoder` and `TextDecoder` are only available in `util` module. Must be assigned to global object.
<sup>3</sup> Because usage of Top-Level Await.
## Connection
This library doesn't tie to a specific transportation method.

View file

@ -6,7 +6,8 @@ import { AdbFrameBuffer, AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, Ad
import { AdbFeatures } from './features';
import { AdbCommand } from './packet';
import { AdbLogger, AdbPacketDispatcher, AdbSocket } from './socket';
import { decodeUtf8, ReadableStream, WritableStream } from "./utils";
import { ReadableStream, WritableStream } from "./stream";
import { decodeUtf8 } from "./utils";
export enum AdbPropKey {
Product = 'ro.product.name',

View file

@ -1,5 +1,5 @@
import type { ValueOrPromise } from '@yume-chan/struct';
import type { ReadableWritablePair } from "./utils";
import type { ReadableWritablePair } from "./stream";
export interface AdbBackend {
readonly serial: string;

View file

@ -1,5 +1,5 @@
import { Adb } from "../adb";
import { HookWritableStream, WritableStream } from "../utils";
import { WrapWritableStream, WritableStream } from "../stream";
import { escapeArg } from "./subprocess";
import { AdbSync } from "./sync";
@ -8,7 +8,7 @@ export function install(
): WritableStream<ArrayBuffer> {
const filename = `/data/local/tmp/${Math.random().toString().substring(2)}.apk`;
return new HookWritableStream<ArrayBuffer, WritableStream<ArrayBuffer>, AdbSync>({
return new WrapWritableStream<ArrayBuffer, WritableStream<ArrayBuffer>, AdbSync>({
async start() {
// Upload apk file to tmp folder
const sync = await adb.sync();

View file

@ -1,5 +1,5 @@
import type { Adb } from '../../adb';
import { DecodeUtf8Stream, GatherStringStream } from "../../utils";
import { DecodeUtf8Stream, GatherStringStream } from "../../stream";
import { AdbNoneSubprocessProtocol } from './legacy';
import { AdbShellSubprocessProtocol } from './protocol';
import type { AdbSubprocessProtocol, AdbSubprocessProtocolConstructor } from './types';

View file

@ -1,7 +1,7 @@
import { PromiseResolver } from "@yume-chan/async";
import type { Adb } from "../../adb";
import type { AdbSocket } from "../../socket";
import { ReadableStream, TransformStream } from "../../utils";
import { ReadableStream, TransformStream } from "../../stream";
import type { AdbSubprocessProtocol } from "./types";
/**

View file

@ -3,7 +3,8 @@ import Struct, { placeholder, StructValueType } from "@yume-chan/struct";
import type { Adb } from "../../adb";
import { AdbFeatures } from "../../features";
import type { AdbSocket } from "../../socket";
import { encodeUtf8, ReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from "../../utils";
import { ReadableStream, StructDeserializeStream, StructSerializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from "../../stream";
import { encodeUtf8 } from "../../utils";
import type { AdbSubprocessProtocol } from "./types";
export enum AdbShellProtocolId {

View file

@ -1,7 +1,7 @@
import type { ValueOrPromise } from "@yume-chan/struct";
import type { Adb } from "../../adb";
import type { AdbSocket } from "../../socket";
import type { ReadableStream, WritableStream } from "../../utils";
import type { ReadableStream, WritableStream } from "../../stream";
export interface AdbSubprocessProtocol {
/**

View file

@ -1,6 +1,5 @@
import Struct from '@yume-chan/struct';
import { AdbBufferedStream } from '../../stream';
import { WritableStreamDefaultWriter } from "../../utils";
import { AdbBufferedStream, WritableStreamDefaultWriter } from '../../stream';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response';
import { AdbSyncLstatResponse } from './stat';

View file

@ -1,6 +1,5 @@
import Struct from '@yume-chan/struct';
import { AdbBufferedStream } from '../../stream';
import { ReadableStream, WritableStreamDefaultWriter } from "../../utils";
import { AdbBufferedStream, ReadableStream, WritableStreamDefaultWriter } from '../../stream';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
import { AdbSyncDoneResponse, adbSyncReadResponse, AdbSyncResponseId } from './response';

View file

@ -1,6 +1,6 @@
import Struct from '@yume-chan/struct';
import { AdbBufferedStream } from '../../stream';
import { chunkArrayLike, WritableStream, WritableStreamDefaultWriter } from '../../utils';
import { AdbBufferedStream, WritableStream, WritableStreamDefaultWriter } from '../../stream';
import { chunkArrayLike } from '../../utils';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
import { adbSyncReadResponse, AdbSyncResponseId } from './response';
import { LinuxFileType } from './stat';

View file

@ -1,5 +1,6 @@
import Struct from '@yume-chan/struct';
import { encodeUtf8, WritableStreamDefaultWriter } from "../../utils";
import { WritableStreamDefaultWriter } from "../../stream";
import { encodeUtf8 } from "../../utils";
export enum AdbSyncRequestId {
List = 'LIST',

View file

@ -1,6 +1,5 @@
import Struct, { placeholder } from '@yume-chan/struct';
import { AdbBufferedStream } from '../../stream';
import { WritableStreamDefaultWriter } from "../../utils";
import { AdbBufferedStream, WritableStreamDefaultWriter } from '../../stream';
import { AdbSyncRequestId, adbSyncWriteRequest } from './request';
import { adbSyncReadResponse, AdbSyncResponseId } from './response';

View file

@ -2,8 +2,8 @@ import { AutoDisposable } from '@yume-chan/event';
import { Adb } from '../../adb';
import { AdbFeatures } from '../../features';
import { AdbSocket } from '../../socket';
import { AdbBufferedStream } from '../../stream';
import { AutoResetEvent, HookWritableStream, ReadableStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../../utils';
import { AdbBufferedStream, ReadableStream, WrapReadableStream, WrapWritableStream, WritableStream, WritableStreamDefaultWriter } from '../../stream';
import { AutoResetEvent } from '../../utils';
import { AdbSyncEntryResponse, adbSyncOpenDir } from './list';
import { adbSyncPull } from './pull';
import { adbSyncPush } from './push';
@ -87,24 +87,21 @@ export class AdbSync extends AutoDisposable {
* Read the content of a file on device.
*
* @param filename The full path of the file on device to read.
* @returns
* A promise that resolves to a `ReadableStream`.
*
* If the promise doesn't resolve immediately, it means the sync object is busy processing another command.
* @returns A `ReadableStream` that reads from the file.
*/
public async read(filename: string): Promise<ReadableStream<ArrayBuffer>> {
await this.sendLock.wait();
const readable = adbSyncPull(this.stream, this.writer, filename);
const lockStream = new TransformStream<ArrayBuffer, ArrayBuffer>();
readable
.pipeTo(lockStream.writable)
.then(() => {
public read(filename: string): ReadableStream<ArrayBuffer> {
return new WrapReadableStream<ArrayBuffer, ReadableStream<ArrayBuffer>, undefined>({
start: async () => {
await this.sendLock.wait();
return {
readable: adbSyncPull(this.stream, this.writer, filename),
state: undefined,
};
},
close: async () => {
this.sendLock.notify();
});
return lockStream.readable;
},
});
}
/**
@ -120,7 +117,7 @@ export class AdbSync extends AutoDisposable {
mode?: number,
mtime?: number,
): WritableStream<ArrayBuffer> {
return new HookWritableStream({
return new WrapWritableStream({
start: async () => {
await this.sendLock.wait();
return {

View file

@ -1,5 +1,5 @@
import Struct from '@yume-chan/struct';
import { TransformStream } from "./utils";
import { TransformStream } from "./stream";
export enum AdbCommand {
Auth = 0x48545541, // 'AUTH'

View file

@ -1,7 +1,7 @@
import { PromiseResolver } from "@yume-chan/async";
import { AutoDisposable } from '@yume-chan/event';
import { AdbCommand } from '../packet';
import { ChunkStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../utils';
import { ChunkStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../stream';
import { AdbPacketDispatcher } from './dispatcher';
export interface AdbSocketInfo {

View file

@ -1,7 +1,8 @@
import { AsyncOperationManager } from '@yume-chan/async';
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
import { AdbCommand, AdbPacket, AdbPacketInit, AdbPacketSerializeStream } from '../packet';
import { AbortController, decodeUtf8, encodeUtf8, ReadableStream, StructDeserializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../utils';
import { AbortController, ReadableStream, StructDeserializeStream, TransformStream, WritableStream, WritableStreamDefaultWriter } from '../stream';
import { decodeUtf8, encodeUtf8 } from '../utils';
import { AdbSocketController } from './controller';
import { AdbLogger } from './logger';
import { AdbSocket } from './socket';

View file

@ -1,4 +1,4 @@
import { ReadableStream, WritableStream } from "../utils";
import { ReadableStream, WritableStream } from "../stream";
import { AdbSocketController, AdbSocketInfo } from './controller';
export class AdbSocket implements AdbSocketInfo {

View file

@ -1,6 +1,6 @@
import { StructAsyncDeserializeStream } from '@yume-chan/struct';
import { AdbSocket, AdbSocketInfo } from '../socket';
import { ReadableStream, ReadableStreamDefaultReader } from '../utils';
import { ReadableStream, ReadableStreamDefaultReader } from './detect';
export class BufferedStreamEndedError extends Error {
public constructor() {

View file

@ -2,6 +2,13 @@
// cspell: ignore backpressure
// cspell: ignore endregion
// Because Node.js exports Web Streams types from `stream/web` package,
// this module uses Top-Level Await to support both Web Browsers and Node.js.
// For Webpack, the `experimental.topLevelAwait` option is required.
// (See: https://webpack.js.org/configuration/experiments/)
// It's also possible to add fallback to some polyfill.
//#region borrowed
// from https://github.com/microsoft/TypeScript/blob/38da7c600c83e7b31193a62495239a0fe478cb67/lib/lib.webworker.d.ts#L633 until moved to separate lib
/** A controller object that allows you to abort one or more DOM requests as and when desired. */

View file

@ -1 +1,3 @@
export * from './buffered-stream';
export * from './buffered';
export * from './detect';
export * from './transform';

View file

@ -1,7 +1,7 @@
import Struct, { decodeUtf8, StructLike, StructValueType } from "@yume-chan/struct";
import { BufferedStream, BufferedStreamEndedError } from "../stream";
import { chunkArrayLike } from "./chunk";
import { TransformStream, WritableStream, WritableStreamDefaultWriter } from "./stream";
import { chunkArrayLike } from "../utils/chunk";
import { BufferedStream, BufferedStreamEndedError } from "./buffered";
import { TransformStream, WritableStream, WritableStreamDefaultWriter, ReadableStream, ReadableStreamDefaultReader } from "./detect";
export class DecodeUtf8Stream extends TransformStream<ArrayBuffer, string>{
public constructor() {
@ -76,42 +76,79 @@ export class StructSerializeStream<T extends Struct<any, any, any, any>>
}
}
export interface WritableStreamHooks<T, W extends WritableStream<T>, S> {
export interface WritableStreamWrapper<T, W extends WritableStream<T>, S> {
start(): Promise<{ writable: W, state: S; }>;
close(state: S): Promise<void>;
}
export class HookWritableStream<T, W extends WritableStream<T>, S> extends WritableStream<T>{
export class WrapWritableStream<T, W extends WritableStream<T>, S> extends WritableStream<T>{
public writable!: W;
private writer!: WritableStreamDefaultWriter<T>;
private state!: S;
public constructor(hooks: WritableStreamHooks<T, W, S>) {
public constructor(wrapper: WritableStreamWrapper<T, W, S>) {
super({
start: async () => {
const { writable, state } = await hooks.start();
const { writable, state } = await wrapper.start();
this.writable = writable;
this.writer = writable.getWriter();
this.state = state;
},
write: async (chunk) => {
// Maintain back pressure
await this.writer.ready;
await this.writer.write(chunk);
},
abort: async (reason) => {
await this.writer.abort(reason);
hooks.close(this.state);
wrapper.close(this.state);
},
close: async () => {
await this.writer.close();
await hooks.close(this.state);
await wrapper.close(this.state);
},
});
}
}
export interface ReadableStreamWrapper<T, R extends ReadableStream<T>, S> {
start(): Promise<{ readable: R, state: S; }>;
close?(state: S): Promise<void>;
}
export class WrapReadableStream<T, R extends ReadableStream<T>, S> extends ReadableStream<T>{
public readable!: R;
private reader!: ReadableStreamDefaultReader<T>;
private state!: S;
public constructor(wrapper: ReadableStreamWrapper<T, R, S>) {
super({
start: async () => {
const { readable, state } = await wrapper.start();
this.readable = readable;
this.reader = readable.getReader();
this.state = state;
},
cancel: async (reason) => {
await this.reader.cancel(reason);
wrapper.close?.(this.state);
},
pull: async (controller) => {
const result = await this.reader.read();
if (result.done) {
controller.close();
} else {
controller.enqueue(result.value);
}
}
});
}
}
export class ChunkStream extends TransformStream<ArrayBuffer, ArrayBuffer>{
public constructor(size: number) {
super({
@ -123,3 +160,31 @@ export class ChunkStream extends TransformStream<ArrayBuffer, ArrayBuffer>{
});
}
}
function* splitLines(text: string): Generator<string, void, void> {
let start = 0;
while (true) {
const index = text.indexOf('\n', start);
if (index === -1) {
return;
}
const line = text.substring(start, index);
yield line;
start = index + 1;
}
}
export class SplitLineStream extends TransformStream<string, string> {
public constructor() {
super({
transform(chunk, controller) {
for (const line of splitLines(chunk)) {
controller.enqueue(line);
}
}
});
}
}

View file

@ -2,5 +2,3 @@ export * from './auto-reset-event';
export * from './base64';
export * from './chunk';
export * from './encoding';
export * from './stream';
export * from './transform';

View file

@ -1,23 +1,7 @@
// cspell: ignore bugreport
// cspell: ignore bugreportz
import { AdbCommandBase, AdbShellSubprocessProtocol, decodeUtf8, TransformStream } from "@yume-chan/adb";
function* splitLines(text: string): Generator<string, void, void> {
let start = 0;
while (true) {
const index = text.indexOf('\n', start);
if (index === -1) {
return;
}
const line = text.substring(start, index);
yield line;
start = index + 1;
}
}
import { AdbCommandBase, AdbShellSubprocessProtocol, DecodeUtf8Stream, ReadableStream, SplitLineStream, WrapReadableStream } from "@yume-chan/adb";
export interface BugReportVersion {
major: number;
@ -66,9 +50,16 @@ export class BugReportZ extends AdbCommandBase {
return version.major > 1 || version.minor >= 2;
}
public async stream(): Promise<ReadableStream<ArrayBuffer>> {
const process = await this.adb.subprocess.spawn(['bugreportz', '-s']);
return process.stdout;
public stream(): ReadableStream<ArrayBuffer> {
return new WrapReadableStream<ArrayBuffer, ReadableStream<ArrayBuffer>, undefined>({
start: async () => {
const process = await this.adb.subprocess.spawn(['bugreportz', '-s']);
return {
readable: process.stdout,
state: undefined,
};
},
});
}
public supportProgress(version: BugReportVersion): boolean {
@ -92,10 +83,11 @@ export class BugReportZ extends AdbCommandBase {
let filename: string | undefined;
let error: string | undefined;
await process.stdout.pipeTo(new WritableStream({
write(chunk) {
const string = decodeUtf8(chunk);
for (const line of splitLines(string)) {
await process.stdout
.pipeThrough(new DecodeUtf8Stream())
.pipeThrough(new SplitLineStream())
.pipeTo(new WritableStream({
write(line) {
// (Not 100% sure) `BEGIN:` and `PROGRESS:` only appear when `-p` is specified.
let match = line.match(BugReportZ.PROGRESS_REGEX);
if (match) {
@ -117,10 +109,10 @@ export class BugReportZ extends AdbCommandBase {
// Don't report error now
// We want to gather all output.
error = match[1];
}
}
}
}));
}));
if (error) {
throw new Error(error);
@ -136,12 +128,16 @@ export class BugReportZ extends AdbCommandBase {
}
export class BugReport extends AdbCommandBase {
public async generate(): Promise<ReadableStream<string>> {
const process = await this.adb.subprocess.spawn(['bugreport']);
return process.stdout.pipeThrough(new TransformStream({
transform(chunk, controller) {
controller.enqueue(decodeUtf8(chunk));
public generate(): ReadableStream<string> {
return new WrapReadableStream<string, ReadableStream<string>, undefined>({
start: async () => {
const process = await this.adb.subprocess.spawn(['bugreport']);
return {
readable: process.stdout
.pipeThrough(new DecodeUtf8Stream()),
state: undefined,
};
}
}));
});
}
}

View file

@ -1,4 +1,4 @@
import { Adb, AdbSync, HookWritableStream, WritableStream } from "@yume-chan/adb";
import { Adb, AdbSync, WrapWritableStream, WritableStream } from "@yume-chan/adb";
import { DEFAULT_SERVER_PATH } from "./options";
export interface PushServerOptions {
@ -11,7 +11,7 @@ export function pushServer(
) {
const { path = DEFAULT_SERVER_PATH } = options;
return new HookWritableStream<ArrayBuffer, WritableStream<ArrayBuffer>, AdbSync>({
return new WrapWritableStream<ArrayBuffer, WritableStream<ArrayBuffer>, AdbSync>({
async start() {
const sync = await device.sync();
return {