@yume-chan/adb 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ import { BufferedReadableStream, tryCancel, tryClose, } from "@yume-chan/stream-extra";
2
+ import { bipedal, decodeUtf8, encodeUtf8, TextDecoder, } from "@yume-chan/struct";
3
+ import { hexToNumber, sequenceEqual, write4HexDigits } from "../utils/index.js";
4
+ const OKAY = encodeUtf8("OKAY");
5
+ export const FAIL = encodeUtf8("FAIL");
6
+ export class AdbServerStream {
7
+ #connection;
8
+ #buffered;
9
+ #writer;
10
+ constructor(connection) {
11
+ this.#connection = connection;
12
+ this.#buffered = new BufferedReadableStream(connection.readable);
13
+ this.#writer = connection.writable.getWriter();
14
+ }
15
+ readExactly(length) {
16
+ return this.#buffered.readExactly(length);
17
+ }
18
+ readString = bipedal(function* (then) {
19
+ const data = yield* then(this.readExactly(4));
20
+ const length = hexToNumber(data);
21
+ if (length === 0) {
22
+ return "";
23
+ }
24
+ else {
25
+ const decoder = new TextDecoder();
26
+ let result = "";
27
+ const iterator = this.#buffered.iterateExactly(length);
28
+ while (true) {
29
+ const { done, value } = iterator.next();
30
+ if (done) {
31
+ break;
32
+ }
33
+ result += decoder.decode(yield* then(value), { stream: true });
34
+ }
35
+ result += decoder.decode();
36
+ return result;
37
+ }
38
+ });
39
+ async readOkay() {
40
+ const response = await this.readExactly(4);
41
+ if (sequenceEqual(response, OKAY)) {
42
+ // `OKAY` is followed by data length and data
43
+ // But different services want to parse the data differently
44
+ // So don't read the data here
45
+ return;
46
+ }
47
+ if (sequenceEqual(response, FAIL)) {
48
+ const reason = await this.readString();
49
+ throw new Error(reason);
50
+ }
51
+ throw new Error(`Unexpected response: ${decodeUtf8(response)}`);
52
+ }
53
+ async writeString(value) {
54
+ // TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
55
+ // That way allocates a new string (hopefully only a rope) instead of a new buffer
56
+ const encoded = encodeUtf8(value);
57
+ const buffer = new Uint8Array(4 + encoded.length);
58
+ write4HexDigits(buffer, 0, encoded.length);
59
+ buffer.set(encoded, 4);
60
+ await this.#writer.write(buffer);
61
+ }
62
+ release() {
63
+ this.#writer.releaseLock();
64
+ return {
65
+ readable: this.#buffered.release(),
66
+ writable: this.#connection.writable,
67
+ closed: this.#connection.closed,
68
+ close: () => this.#connection.close(),
69
+ };
70
+ }
71
+ async dispose() {
72
+ void tryCancel(this.#buffered);
73
+ void tryClose(this.#writer);
74
+ await this.#connection.close();
75
+ }
76
+ }
77
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/server/stream.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,3 @@
1
- import type { MaybePromiseLike } from "@yume-chan/async";
2
1
  import type { AdbIncomingSocketHandler, AdbSocket, AdbTransport } from "../adb.js";
3
2
  import type { AdbBanner } from "../banner.js";
4
3
  import { AdbFeature } from "../features.js";
@@ -10,13 +9,13 @@ export declare class AdbServerTransport implements AdbTransport {
10
9
  readonly transportId: bigint;
11
10
  readonly maxPayloadSize: number;
12
11
  readonly banner: AdbBanner;
13
- readonly disconnected: Promise<void>;
12
+ get disconnected(): Promise<void>;
14
13
  get clientFeatures(): AdbFeature[];
15
- constructor(client: AdbServerClient, serial: string, banner: AdbBanner, transportId: bigint);
14
+ constructor(client: AdbServerClient, serial: string, banner: AdbBanner, transportId: bigint, disconnected: Promise<void>);
16
15
  connect(service: string): Promise<AdbSocket>;
17
16
  addReverseTunnel(handler: AdbIncomingSocketHandler, address?: string): Promise<string>;
18
17
  removeReverseTunnel(address: string): Promise<void>;
19
18
  clearReverseTunnels(): Promise<void>;
20
- close(): MaybePromiseLike<void>;
19
+ close(): Promise<void>;
21
20
  }
22
21
  //# sourceMappingURL=transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIzD,OAAO,KAAK,EACR,wBAAwB,EACxB,SAAS,EACT,YAAY,EACf,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,eAAO,MAAM,2BAA2B,EAmBnC,UAAU,EAAE,CAAC;AAElB,qBAAa,kBAAmB,YAAW,YAAY;;IAGnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAmB;IAElD,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAI3B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAErC,IAAI,cAAc,iBAIjB;gBAGG,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM;IAgBjB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAO5C,gBAAgB,CAClB,OAAO,EAAE,wBAAwB,EACjC,OAAO,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IAIZ,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1C,KAAK,IAAI,gBAAgB,CAAC,IAAI,CAAC;CAIlC"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACR,wBAAwB,EACxB,SAAS,EACT,YAAY,EACf,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,eAAO,MAAM,2BAA2B,EAmBnC,UAAU,EAAE,CAAC;AAElB,qBAAa,kBAAmB,YAAW,YAAY;;IAGnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAmB;IAElD,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAM3B,IAAI,YAAY,kBAEf;IAED,IAAI,cAAc,iBAIjB;gBAIG,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC;IAUzB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAS5C,gBAAgB,CAClB,OAAO,EAAE,wBAAwB,EACjC,OAAO,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IAIZ,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAO/B"}
@@ -1,5 +1,4 @@
1
1
  import { PromiseResolver } from "@yume-chan/async";
2
- import { AbortController } from "@yume-chan/stream-extra";
3
2
  import { AdbFeature } from "../features.js";
4
3
  export const ADB_SERVER_DEFAULT_FEATURES = [
5
4
  AdbFeature.ShellV2,
@@ -27,29 +26,29 @@ export class AdbServerTransport {
27
26
  transportId;
28
27
  maxPayloadSize = 1 * 1024 * 1024;
29
28
  banner;
29
+ #sockets = [];
30
30
  #closed = new PromiseResolver();
31
- #waitAbortController = new AbortController();
32
- disconnected;
31
+ #disconnected;
32
+ get disconnected() {
33
+ return this.#disconnected;
34
+ }
33
35
  get clientFeatures() {
34
36
  // No need to get host features (features supported by ADB server)
35
37
  // Because we create all ADB packets ourselves
36
38
  return ADB_SERVER_DEFAULT_FEATURES;
37
39
  }
38
- constructor(client, serial, banner, transportId) {
40
+ // eslint-disable-next-line @typescript-eslint/max-params
41
+ constructor(client, serial, banner, transportId, disconnected) {
39
42
  this.#client = client;
40
43
  this.serial = serial;
41
44
  this.banner = banner;
42
45
  this.transportId = transportId;
43
- this.disconnected = Promise.race([
44
- this.#closed.promise,
45
- client.waitFor({ transportId }, "disconnect", {
46
- signal: this.#waitAbortController.signal,
47
- unref: true,
48
- }),
49
- ]);
46
+ this.#disconnected = Promise.race([this.#closed.promise, disconnected]);
50
47
  }
51
48
  async connect(service) {
52
- return await this.#client.createDeviceConnection({ transportId: this.transportId }, service);
49
+ const socket = await this.#client.createDeviceConnection({ transportId: this.transportId }, service);
50
+ this.#sockets.push(socket);
51
+ return socket;
53
52
  }
54
53
  async addReverseTunnel(handler, address) {
55
54
  return await this.#client.connector.addReverseTunnel(handler, address);
@@ -60,9 +59,12 @@ export class AdbServerTransport {
60
59
  async clearReverseTunnels() {
61
60
  await this.#client.connector.clearReverseTunnels();
62
61
  }
63
- close() {
62
+ async close() {
63
+ for (const socket of this.#sockets) {
64
+ await socket.close();
65
+ }
66
+ this.#sockets.length = 0;
64
67
  this.#closed.resolve();
65
- this.#waitAbortController.abort();
66
68
  }
67
69
  }
68
70
  //# sourceMappingURL=transport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAQ1D,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAI5C,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACvC,UAAU,CAAC,OAAO;IAClB,UAAU,CAAC,GAAG;IACd,UAAU,CAAC,MAAM;IACjB,UAAU,CAAC,MAAM;IACjB,UAAU,CAAC,cAAc;IACzB,MAAM;IACN,UAAU,CAAC,GAAG;IACd,yFAAyF;IACzF,gCAAgC;IAChC,8BAA8B;IAC9B,UAAU,CAAC,OAAO;IAClB,eAAe;IACf,WAAW;IACX,UAAU,CAAC,aAAa;IACxB,oBAAoB;IACpB,iBAAiB;IACjB,kBAAkB;IAClB,0BAA0B;CACb,CAAC;AAElB,MAAM,OAAO,kBAAkB;IAC3B,OAAO,CAAkB;IAEhB,MAAM,CAAS;IAEf,WAAW,CAAS;IAEpB,cAAc,GAAW,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAEzC,MAAM,CAAY;IAE3B,OAAO,GAAG,IAAI,eAAe,EAAQ,CAAC;IACtC,oBAAoB,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,YAAY,CAAgB;IAErC,IAAI,cAAc;QACd,kEAAkE;QAClE,8CAA8C;QAC9C,OAAO,2BAA2B,CAAC;IACvC,CAAC;IAED,YACI,MAAuB,EACvB,MAAc,EACd,MAAiB,EACjB,WAAmB;QAEnB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAE/B,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,OAAO;YACpB,MAAM,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,EAAE,YAAY,EAAE;gBAC1C,MAAM,EAAE,IAAI,CAAC,oBAAoB,CAAC,MAAM;gBACxC,KAAK,EAAE,IAAI;aACd,CAAC;SACL,CAAC,CAAC;IACP,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAe;QACzB,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAC5C,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,EACjC,OAAO,CACV,CAAC;IACN,CAAC;IAED,KAAK,CAAC,gBAAgB,CAClB,OAAiC,EACjC,OAAgB;QAEhB,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,OAAe;QACrC,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,mBAAmB;QACrB,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,mBAAmB,EAAE,CAAC;IACvD,CAAC;IAED,KAAK;QACD,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACvB,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,CAAC;IACtC,CAAC;CACJ"}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAQnD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAI5C,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACvC,UAAU,CAAC,OAAO;IAClB,UAAU,CAAC,GAAG;IACd,UAAU,CAAC,MAAM;IACjB,UAAU,CAAC,MAAM;IACjB,UAAU,CAAC,cAAc;IACzB,MAAM;IACN,UAAU,CAAC,GAAG;IACd,yFAAyF;IACzF,gCAAgC;IAChC,8BAA8B;IAC9B,UAAU,CAAC,OAAO;IAClB,eAAe;IACf,WAAW;IACX,UAAU,CAAC,aAAa;IACxB,oBAAoB;IACpB,iBAAiB;IACjB,kBAAkB;IAClB,0BAA0B;CACb,CAAC;AAElB,MAAM,OAAO,kBAAkB;IAC3B,OAAO,CAAkB;IAEhB,MAAM,CAAS;IAEf,WAAW,CAAS;IAEpB,cAAc,GAAW,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAEzC,MAAM,CAAY;IAE3B,QAAQ,GAAgB,EAAE,CAAC;IAE3B,OAAO,GAAG,IAAI,eAAe,EAAQ,CAAC;IACtC,aAAa,CAAgB;IAC7B,IAAI,YAAY;QACZ,OAAO,IAAI,CAAC,aAAa,CAAC;IAC9B,CAAC;IAED,IAAI,cAAc;QACd,kEAAkE;QAClE,8CAA8C;QAC9C,OAAO,2BAA2B,CAAC;IACvC,CAAC;IAED,yDAAyD;IACzD,YACI,MAAuB,EACvB,MAAc,EACd,MAAiB,EACjB,WAAmB,EACnB,YAA2B;QAE3B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAE/B,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAe;QACzB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,sBAAsB,CACpD,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,EACjC,OAAO,CACV,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3B,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,gBAAgB,CAClB,OAAiC,EACjC,OAAgB;QAEhB,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,OAAe;QACrC,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,mBAAmB;QACrB,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,mBAAmB,EAAE,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,KAAK;QACP,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjC,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;CACJ"}
@@ -3,5 +3,6 @@ export * from "./auto-reset-event.js";
3
3
  export * from "./base64.js";
4
4
  export * from "./hex.js";
5
5
  export * from "./no-op.js";
6
+ export * from "./ref.js";
6
7
  export * from "./sequence-equal.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,qBAAqB,CAAC"}
@@ -3,5 +3,6 @@ export * from "./auto-reset-event.js";
3
3
  export * from "./base64.js";
4
4
  export * from "./hex.js";
5
5
  export * from "./no-op.js";
6
+ export * from "./ref.js";
6
7
  export * from "./sequence-equal.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * An object to keep current Node.js process alive even when no code is running.
3
+ *
4
+ * Does nothing in Web environments.
5
+ *
6
+ * Note that it does't have reference counting. Calling `unref` will
7
+ * remove the ref no matter how many times `ref` has been previously called, and vice versa.
8
+ * This is the same as how Node.js works.
9
+ */
10
+ export declare class Ref {
11
+ #private;
12
+ constructor(options?: {
13
+ unref?: boolean | undefined;
14
+ });
15
+ ref(): void;
16
+ unref(): void;
17
+ }
18
+ //# sourceMappingURL=ref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ref.d.ts","sourceRoot":"","sources":["../../src/utils/ref.ts"],"names":[],"mappings":"AAOA;;;;;;;;GAQG;AACH,qBAAa,GAAG;;gBAGA,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE;IAMrD,GAAG;IAKH,KAAK;CAMR"}
@@ -0,0 +1,29 @@
1
+ const { setInterval, clearInterval } = globalThis;
2
+ /**
3
+ * An object to keep current Node.js process alive even when no code is running.
4
+ *
5
+ * Does nothing in Web environments.
6
+ *
7
+ * Note that it does't have reference counting. Calling `unref` will
8
+ * remove the ref no matter how many times `ref` has been previously called, and vice versa.
9
+ * This is the same as how Node.js works.
10
+ */
11
+ export class Ref {
12
+ #intervalId;
13
+ constructor(options) {
14
+ if (!options?.unref) {
15
+ this.ref();
16
+ }
17
+ }
18
+ ref() {
19
+ // `setInterval` can keep current Node.js alive, the delay value doesn't matter
20
+ this.#intervalId = setInterval(() => { }, 60 * 1000);
21
+ }
22
+ unref() {
23
+ if (this.#intervalId) {
24
+ clearInterval(this.#intervalId);
25
+ this.#intervalId = undefined;
26
+ }
27
+ }
28
+ }
29
+ //# sourceMappingURL=ref.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ref.js","sourceRoot":"","sources":["../../src/utils/ref.ts"],"names":[],"mappings":"AAKA,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,UAAwC,CAAC;AAEhF;;;;;;;;GAQG;AACH,MAAM,OAAO,GAAG;IACZ,WAAW,CAAqB;IAEhC,YAAY,OAAyC;QACjD,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;YAClB,IAAI,CAAC,GAAG,EAAE,CAAC;QACf,CAAC;IACL,CAAC;IAED,GAAG;QACC,+EAA+E;QAC/E,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE,GAAE,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC;IACxD,CAAC;IAED,KAAK;QACD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAChC,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QACjC,CAAC;IACL,CAAC;CACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yume-chan/adb",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "TypeScript implementation of Android Debug Bridge (ADB) protocol.",
5
5
  "keywords": [
6
6
  "adb",
@@ -28,17 +28,17 @@
28
28
  "dependencies": {
29
29
  "@yume-chan/async": "^4.0.2",
30
30
  "@yume-chan/event": "^1.0.0",
31
+ "@yume-chan/no-data-view": "^1.0.0",
31
32
  "@yume-chan/stream-extra": "^1.0.0",
32
- "@yume-chan/struct": "^1.0.0",
33
- "@yume-chan/no-data-view": "^1.0.0"
33
+ "@yume-chan/struct": "^1.0.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@types/node": "^22.10.0",
37
- "prettier": "^3.4.1",
36
+ "@types/node": "^22.10.2",
37
+ "prettier": "^3.4.2",
38
38
  "typescript": "^5.7.2",
39
39
  "@yume-chan/eslint-config": "^1.0.0",
40
- "@yume-chan/test-runner": "^1.0.0",
41
- "@yume-chan/tsconfig": "^1.0.0"
40
+ "@yume-chan/tsconfig": "^1.0.0",
41
+ "@yume-chan/test-runner": "^1.0.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsc -b tsconfig.build.json",
@@ -3,123 +3,28 @@
3
3
  import type { MaybePromiseLike } from "@yume-chan/async";
4
4
  import { PromiseResolver } from "@yume-chan/async";
5
5
  import type { Event } from "@yume-chan/event";
6
- import { EventEmitter } from "@yume-chan/event";
7
6
  import { getUint64LittleEndian } from "@yume-chan/no-data-view";
8
7
  import type {
9
8
  AbortSignal,
10
9
  MaybeConsumable,
11
10
  ReadableWritablePair,
12
- WritableStreamDefaultWriter,
13
11
  } from "@yume-chan/stream-extra";
14
- import {
15
- BufferedReadableStream,
16
- tryCancel,
17
- tryClose,
18
- } from "@yume-chan/stream-extra";
19
- import {
20
- bipedal,
21
- decodeUtf8,
22
- encodeUtf8,
23
- TextDecoder,
24
- } from "@yume-chan/struct";
12
+ import { AbortController } from "@yume-chan/stream-extra";
25
13
 
26
14
  import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
27
15
  import { AdbBanner } from "../banner.js";
28
16
  import type { DeviceObserver as DeviceObserverBase } from "../device-observer.js";
29
17
  import type { AdbFeature } from "../features.js";
30
- import { hexToNumber, sequenceEqual, write4HexDigits } from "../utils/index.js";
18
+ import { hexToNumber, sequenceEqual } from "../utils/index.js";
31
19
 
20
+ import { AdbServerDeviceObserverOwner } from "./observer.js";
21
+ import { AdbServerStream, FAIL } from "./stream.js";
32
22
  import { AdbServerTransport } from "./transport.js";
33
23
 
34
- const OKAY = encodeUtf8("OKAY");
35
- const FAIL = encodeUtf8("FAIL");
36
-
37
- class AdbServerStream {
38
- #connection: AdbServerClient.ServerConnection;
39
- #buffered: BufferedReadableStream;
40
- #writer: WritableStreamDefaultWriter<Uint8Array>;
41
-
42
- constructor(connection: AdbServerClient.ServerConnection) {
43
- this.#connection = connection;
44
- this.#buffered = new BufferedReadableStream(connection.readable);
45
- this.#writer = connection.writable.getWriter();
46
- }
47
-
48
- readExactly(length: number): MaybePromiseLike<Uint8Array> {
49
- return this.#buffered.readExactly(length);
50
- }
51
-
52
- readString = bipedal(function* (this: AdbServerStream, then) {
53
- const data = yield* then(this.readExactly(4));
54
- const length = hexToNumber(data);
55
- if (length === 0) {
56
- return "";
57
- } else {
58
- const decoder = new TextDecoder();
59
- let result = "";
60
- const iterator = this.#buffered.iterateExactly(length);
61
- while (true) {
62
- const { done, value } = iterator.next();
63
- if (done) {
64
- break;
65
- }
66
- result += decoder.decode(yield* then(value), { stream: true });
67
- }
68
- result += decoder.decode();
69
- return result;
70
- }
71
- });
72
-
73
- async writeString(value: string): Promise<void> {
74
- // TODO: investigate using `encodeUtf8("0000" + value)` then modifying the length
75
- // That way allocates a new string (hopefully only a rope) instead of a new buffer
76
- const encoded = encodeUtf8(value);
77
- const buffer = new Uint8Array(4 + encoded.length);
78
- write4HexDigits(buffer, 0, encoded.length);
79
- buffer.set(encoded, 4);
80
- await this.#writer.write(buffer);
81
- }
82
-
83
- async readOkay(): Promise<void> {
84
- const response = await this.readExactly(4);
85
- if (sequenceEqual(response, OKAY)) {
86
- // `OKAY` is followed by data length and data
87
- // But different services want to parse the data differently
88
- // So don't read the data here
89
- return;
90
- }
91
-
92
- if (sequenceEqual(response, FAIL)) {
93
- const reason = await this.readString();
94
- throw new Error(reason);
95
- }
96
-
97
- throw new Error(`Unexpected response: ${decodeUtf8(response)}`);
98
- }
99
-
100
- release() {
101
- this.#writer.releaseLock();
102
- return {
103
- readable: this.#buffered.release(),
104
- writable: this.#connection.writable,
105
- closed: this.#connection.closed,
106
- close: () => this.#connection.close(),
107
- };
108
- }
109
-
110
- async dispose() {
111
- void tryCancel(this.#buffered);
112
- void tryClose(this.#writer);
113
- await this.#connection.close();
114
- }
115
- }
116
-
117
24
  /**
118
25
  * Client for the ADB Server.
119
26
  */
120
27
  export class AdbServerClient {
121
- static readonly VERSION = 41;
122
-
123
28
  static parseDeviceList(value: string): AdbServerClient.Device[] {
124
29
  const devices: AdbServerClient.Device[] = [];
125
30
  for (const line of value.split("\n")) {
@@ -196,6 +101,7 @@ export class AdbServerClient {
196
101
 
197
102
  readonly wireless = new AdbServerClient.WirelessCommands(this);
198
103
  readonly mDns = new AdbServerClient.MDnsCommands(this);
104
+ #observerOwner = new AdbServerDeviceObserverOwner(this);
199
105
 
200
106
  constructor(connector: AdbServerClient.ServerConnector) {
201
107
  this.connector = connector;
@@ -240,11 +146,11 @@ export class AdbServerClient {
240
146
  }
241
147
  }
242
148
 
243
- async validateVersion() {
149
+ async validateVersion(minimalVersion: number) {
244
150
  const version = await this.getVersion();
245
- if (version !== AdbServerClient.VERSION) {
151
+ if (version < minimalVersion) {
246
152
  throw new Error(
247
- `adb server version (${version}) doesn't match this client (${AdbServerClient.VERSION})`,
153
+ `adb server version (${version}) doesn't match this client (${minimalVersion})`,
248
154
  );
249
155
  }
250
156
  }
@@ -288,61 +194,10 @@ export class AdbServerClient {
288
194
  /**
289
195
  * Monitors device list changes.
290
196
  */
291
- async trackDevices(): Promise<AdbServerClient.DeviceObserver> {
292
- const connection = await this.createConnection("host:track-devices-l");
293
-
294
- let current: AdbServerClient.Device[] = [];
295
- const onError = new EventEmitter<Error>();
296
- const onDeviceAdd = new EventEmitter<AdbServerClient.Device[]>();
297
- const onDeviceRemove = new EventEmitter<AdbServerClient.Device[]>();
298
- const onListChange = new EventEmitter<AdbServerClient.Device[]>();
299
-
300
- void (async () => {
301
- try {
302
- while (true) {
303
- const response = await connection.readString();
304
- const next = AdbServerClient.parseDeviceList(response);
305
-
306
- const added: AdbServerClient.Device[] = [];
307
- for (const nextDevice of next) {
308
- const index = current.findIndex(
309
- (device) =>
310
- device.transportId === nextDevice.transportId,
311
- );
312
- if (index === -1) {
313
- added.push(nextDevice);
314
- continue;
315
- }
316
-
317
- current[index] = current[current.length - 1]!;
318
- current.length -= 1;
319
- }
320
-
321
- if (added.length) {
322
- onDeviceAdd.fire(added);
323
- }
324
- if (current.length) {
325
- onDeviceRemove.fire(current);
326
- }
327
-
328
- current = next;
329
- onListChange.fire(current);
330
- }
331
- } catch (e) {
332
- onError.fire(e as Error);
333
- }
334
- })();
335
-
336
- return {
337
- onError: onError.event,
338
- onDeviceAdd: onDeviceAdd.event,
339
- onDeviceRemove: onDeviceRemove.event,
340
- onListChange: onListChange.event,
341
- get current() {
342
- return current;
343
- },
344
- stop: () => connection.dispose(),
345
- };
197
+ async trackDevices(
198
+ options?: AdbServerClient.ServerConnectionOptions,
199
+ ): Promise<AdbServerClient.DeviceObserver> {
200
+ return this.#observerOwner.createObserver(options);
346
201
  }
347
202
 
348
203
  /**
@@ -412,20 +267,22 @@ export class AdbServerClient {
412
267
  device: AdbServerClient.DeviceSelector,
413
268
  service: string,
414
269
  ): Promise<AdbServerClient.Socket> {
415
- await this.validateVersion();
416
-
417
270
  let switchService: string;
418
271
  let transportId: bigint | undefined;
419
272
  if (!device) {
273
+ await this.validateVersion(41);
420
274
  switchService = `host:tport:any`;
421
275
  } else if ("transportId" in device) {
422
276
  switchService = `host:transport-id:${device.transportId}`;
423
277
  transportId = device.transportId;
424
278
  } else if ("serial" in device) {
279
+ await this.validateVersion(41);
425
280
  switchService = `host:tport:serial:${device.serial}`;
426
281
  } else if ("usb" in device) {
282
+ await this.validateVersion(41);
427
283
  switchService = `host:tport:usb`;
428
284
  } else if ("tcp" in device) {
285
+ await this.validateVersion(41);
429
286
  switchService = `host:tport:local`;
430
287
  } else {
431
288
  throw new TypeError("Invalid device selector");
@@ -467,18 +324,7 @@ export class AdbServerClient {
467
324
  throw e;
468
325
  }
469
326
  }
470
-
471
- /**
472
- * Wait for a device to be connected or disconnected.
473
- *
474
- * `adb wait-for-<state>`
475
- *
476
- * @param device The device selector
477
- * @param state The state to wait for
478
- * @param options The options
479
- * @returns A promise that resolves when the condition is met.
480
- */
481
- async waitFor(
327
+ async #waitForUnchecked(
482
328
  device: AdbServerClient.DeviceSelector,
483
329
  state: "device" | "disconnect",
484
330
  options?: AdbServerClient.ServerConnectionOptions,
@@ -513,6 +359,60 @@ export class AdbServerClient {
513
359
  }
514
360
  }
515
361
 
362
+ /**
363
+ * Wait for a device to be connected or disconnected.
364
+ *
365
+ * `adb wait-for-<state>`
366
+ *
367
+ * @param device The device selector
368
+ * @param state The state to wait for
369
+ * @param options The options
370
+ * @returns A promise that resolves when the condition is met.
371
+ */
372
+ async waitFor(
373
+ device: AdbServerClient.DeviceSelector,
374
+ state: "device" | "disconnect",
375
+ options?: AdbServerClient.ServerConnectionOptions,
376
+ ): Promise<void> {
377
+ if (state === "disconnect") {
378
+ await this.validateVersion(41);
379
+ }
380
+
381
+ return this.#waitForUnchecked(device, state, options);
382
+ }
383
+
384
+ async waitForDisconnect(
385
+ transportId: bigint,
386
+ options?: AdbServerClient.ServerConnectionOptions,
387
+ ): Promise<void> {
388
+ const serverVersion = await this.getVersion();
389
+ if (serverVersion >= 41) {
390
+ return this.#waitForUnchecked(
391
+ { transportId },
392
+ "disconnect",
393
+ options,
394
+ );
395
+ } else {
396
+ const observer = await this.trackDevices(options);
397
+ return new Promise<void>((resolve, reject) => {
398
+ observer.onDeviceRemove((devices) => {
399
+ if (
400
+ devices.some(
401
+ (device) => device.transportId === transportId,
402
+ )
403
+ ) {
404
+ observer.stop();
405
+ resolve();
406
+ }
407
+ });
408
+ observer.onError((e) => {
409
+ observer.stop();
410
+ reject(e);
411
+ });
412
+ });
413
+ }
414
+ }
415
+
516
416
  /**
517
417
  * Creates an ADB Transport for the specified device.
518
418
  */
@@ -533,12 +433,26 @@ export class AdbServerClient {
533
433
  features,
534
434
  );
535
435
 
536
- return new AdbServerTransport(
436
+ const waitAbortController = new AbortController();
437
+ const disconnected = this.waitForDisconnect(transportId, {
438
+ unref: true,
439
+ signal: waitAbortController.signal,
440
+ });
441
+
442
+ const transport = new AdbServerTransport(
537
443
  this,
538
444
  info?.serial ?? "",
539
445
  banner,
540
446
  transportId,
447
+ disconnected,
541
448
  );
449
+
450
+ transport.disconnected.then(
451
+ () => waitAbortController.abort(),
452
+ () => waitAbortController.abort(),
453
+ );
454
+
455
+ return transport;
542
456
  }
543
457
  }
544
458
 
@@ -1,2 +1,4 @@
1
1
  export * from "./client.js";
2
+ export * from "./observer.js";
3
+ export * from "./stream.js";
2
4
  export * from "./transport.js";