@yume-chan/adb 0.0.19 → 0.0.21

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.
Files changed (197) hide show
  1. package/CHANGELOG.json +57 -0
  2. package/CHANGELOG.md +26 -1
  3. package/README.md +222 -103
  4. package/esm/adb.d.ts +28 -45
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +34 -213
  7. package/esm/adb.js.map +1 -1
  8. package/esm/banner.d.ts +17 -0
  9. package/esm/banner.d.ts.map +1 -0
  10. package/esm/banner.js +67 -0
  11. package/esm/banner.js.map +1 -0
  12. package/esm/commands/base.d.ts.map +1 -1
  13. package/esm/commands/base.js.map +1 -1
  14. package/esm/commands/framebuffer.d.ts +9 -0
  15. package/esm/commands/framebuffer.d.ts.map +1 -1
  16. package/esm/commands/framebuffer.js +27 -3
  17. package/esm/commands/framebuffer.js.map +1 -1
  18. package/esm/commands/power.d.ts +1 -1
  19. package/esm/commands/power.d.ts.map +1 -1
  20. package/esm/commands/power.js +2 -2
  21. package/esm/commands/power.js.map +1 -1
  22. package/esm/commands/reverse.d.ts +17 -18
  23. package/esm/commands/reverse.d.ts.map +1 -1
  24. package/esm/commands/reverse.js +53 -48
  25. package/esm/commands/reverse.js.map +1 -1
  26. package/esm/commands/subprocess/command.d.ts +1 -1
  27. package/esm/commands/subprocess/command.d.ts.map +1 -1
  28. package/esm/commands/subprocess/command.js +15 -13
  29. package/esm/commands/subprocess/command.js.map +1 -1
  30. package/esm/commands/subprocess/protocols/none.d.ts +3 -8
  31. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  32. package/esm/commands/subprocess/protocols/none.js +16 -16
  33. package/esm/commands/subprocess/protocols/none.js.map +1 -1
  34. package/esm/commands/subprocess/protocols/shell.d.ts +3 -9
  35. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  36. package/esm/commands/subprocess/protocols/shell.js +63 -57
  37. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  38. package/esm/commands/subprocess/protocols/types.d.ts +1 -2
  39. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  40. package/esm/commands/sync/list.js +2 -2
  41. package/esm/commands/sync/push.d.ts +14 -3
  42. package/esm/commands/sync/push.d.ts.map +1 -1
  43. package/esm/commands/sync/push.js +9 -9
  44. package/esm/commands/sync/push.js.map +1 -1
  45. package/esm/commands/sync/request.js +2 -2
  46. package/esm/commands/sync/request.js.map +1 -1
  47. package/esm/commands/sync/response.d.ts +5 -3
  48. package/esm/commands/sync/response.d.ts.map +1 -1
  49. package/esm/commands/sync/response.js +7 -5
  50. package/esm/commands/sync/response.js.map +1 -1
  51. package/esm/commands/sync/socket.d.ts +8 -14
  52. package/esm/commands/sync/socket.d.ts.map +1 -1
  53. package/esm/commands/sync/socket.js +35 -32
  54. package/esm/commands/sync/socket.js.map +1 -1
  55. package/esm/commands/sync/stat.js +4 -4
  56. package/esm/commands/sync/stat.js.map +1 -1
  57. package/esm/commands/sync/sync.d.ts +26 -18
  58. package/esm/commands/sync/sync.d.ts.map +1 -1
  59. package/esm/commands/sync/sync.js +36 -25
  60. package/esm/commands/sync/sync.js.map +1 -1
  61. package/esm/commands/tcpip.d.ts +2 -2
  62. package/esm/commands/tcpip.d.ts.map +1 -1
  63. package/esm/commands/tcpip.js +4 -2
  64. package/esm/commands/tcpip.js.map +1 -1
  65. package/esm/{auth.d.ts → daemon/auth.d.ts} +12 -11
  66. package/esm/daemon/auth.d.ts.map +1 -0
  67. package/esm/{auth.js → daemon/auth.js} +31 -22
  68. package/esm/daemon/auth.js.map +1 -0
  69. package/esm/{crypto.d.ts → daemon/crypto.d.ts} +5 -5
  70. package/esm/daemon/crypto.d.ts.map +1 -0
  71. package/esm/{crypto.js → daemon/crypto.js} +24 -28
  72. package/esm/daemon/crypto.js.map +1 -0
  73. package/esm/{backend.d.ts → daemon/device.d.ts} +2 -2
  74. package/esm/daemon/device.d.ts.map +1 -0
  75. package/esm/daemon/device.js +2 -0
  76. package/esm/daemon/device.js.map +1 -0
  77. package/esm/{socket → daemon}/dispatcher.d.ts +12 -30
  78. package/esm/daemon/dispatcher.d.ts.map +1 -0
  79. package/esm/{socket → daemon}/dispatcher.js +80 -78
  80. package/esm/daemon/dispatcher.js.map +1 -0
  81. package/esm/daemon/index.d.ts +8 -0
  82. package/esm/daemon/index.d.ts.map +1 -0
  83. package/esm/daemon/index.js +8 -0
  84. package/esm/daemon/index.js.map +1 -0
  85. package/esm/{packet.d.ts → daemon/packet.d.ts} +4 -4
  86. package/esm/daemon/packet.d.ts.map +1 -0
  87. package/esm/{packet.js → daemon/packet.js} +2 -2
  88. package/esm/{packet.js.map → daemon/packet.js.map} +1 -1
  89. package/esm/{socket → daemon}/socket.d.ts +15 -19
  90. package/esm/daemon/socket.d.ts.map +1 -0
  91. package/esm/{socket → daemon}/socket.js +49 -42
  92. package/esm/daemon/socket.js.map +1 -0
  93. package/esm/daemon/transport.d.ts +53 -0
  94. package/esm/daemon/transport.d.ts.map +1 -0
  95. package/esm/daemon/transport.js +186 -0
  96. package/esm/daemon/transport.js.map +1 -0
  97. package/esm/features.js +1 -1
  98. package/esm/features.js.map +1 -1
  99. package/esm/index.d.ts +3 -5
  100. package/esm/index.d.ts.map +1 -1
  101. package/esm/index.js +3 -5
  102. package/esm/index.js.map +1 -1
  103. package/esm/server/client.d.ts +78 -0
  104. package/esm/server/client.d.ts.map +1 -0
  105. package/esm/server/client.js +328 -0
  106. package/esm/server/client.js.map +1 -0
  107. package/esm/server/index.d.ts +3 -0
  108. package/esm/server/index.d.ts.map +1 -0
  109. package/esm/server/index.js +3 -0
  110. package/esm/server/index.js.map +1 -0
  111. package/esm/server/transport.d.ts +19 -0
  112. package/esm/server/transport.d.ts.map +1 -0
  113. package/esm/server/transport.js +44 -0
  114. package/esm/server/transport.js.map +1 -0
  115. package/esm/utils/auto-reset-event.d.ts +1 -2
  116. package/esm/utils/auto-reset-event.d.ts.map +1 -1
  117. package/esm/utils/auto-reset-event.js +12 -12
  118. package/esm/utils/auto-reset-event.js.map +1 -1
  119. package/esm/utils/base64.js +5 -5
  120. package/esm/utils/base64.js.map +1 -1
  121. package/esm/utils/conditional-variable.d.ts +1 -2
  122. package/esm/utils/conditional-variable.d.ts.map +1 -1
  123. package/esm/utils/conditional-variable.js +10 -10
  124. package/esm/utils/conditional-variable.js.map +1 -1
  125. package/esm/utils/hex.d.ts +3 -0
  126. package/esm/utils/hex.d.ts.map +1 -0
  127. package/esm/utils/hex.js +55 -0
  128. package/esm/utils/hex.js.map +1 -0
  129. package/esm/utils/index.d.ts +2 -0
  130. package/esm/utils/index.d.ts.map +1 -1
  131. package/esm/utils/index.js +2 -0
  132. package/esm/utils/index.js.map +1 -1
  133. package/esm/utils/no-op.d.ts +7 -0
  134. package/esm/utils/no-op.d.ts.map +1 -0
  135. package/esm/utils/no-op.js +12 -0
  136. package/esm/utils/no-op.js.map +1 -0
  137. package/package.json +12 -12
  138. package/src/adb.ts +75 -274
  139. package/src/banner.ts +82 -0
  140. package/src/commands/base.ts +1 -1
  141. package/src/commands/framebuffer.ts +31 -3
  142. package/src/commands/power.ts +10 -10
  143. package/src/commands/reverse.ts +77 -82
  144. package/src/commands/subprocess/command.ts +24 -25
  145. package/src/commands/subprocess/protocols/none.ts +29 -30
  146. package/src/commands/subprocess/protocols/shell.ts +87 -89
  147. package/src/commands/subprocess/protocols/types.ts +1 -2
  148. package/src/commands/sync/list.ts +7 -7
  149. package/src/commands/sync/pull.ts +4 -4
  150. package/src/commands/sync/push.ts +28 -16
  151. package/src/commands/sync/request.ts +4 -4
  152. package/src/commands/sync/response.ts +13 -11
  153. package/src/commands/sync/socket.ts +48 -46
  154. package/src/commands/sync/stat.ts +5 -5
  155. package/src/commands/sync/sync.ts +55 -43
  156. package/src/commands/tcpip.ts +6 -4
  157. package/src/{auth.ts → daemon/auth.ts} +62 -43
  158. package/src/{crypto.ts → daemon/crypto.ts} +33 -41
  159. package/src/{backend.ts → daemon/device.ts} +1 -1
  160. package/src/{socket → daemon}/dispatcher.ts +103 -109
  161. package/src/daemon/index.ts +7 -0
  162. package/src/{packet.ts → daemon/packet.ts} +6 -6
  163. package/src/{socket → daemon}/socket.ts +77 -67
  164. package/src/daemon/transport.ts +281 -0
  165. package/src/index.ts +3 -5
  166. package/src/server/client.ts +473 -0
  167. package/src/server/index.ts +2 -0
  168. package/src/server/transport.ts +77 -0
  169. package/src/utils/auto-reset-event.ts +16 -16
  170. package/src/utils/base64.ts +5 -5
  171. package/src/utils/conditional-variable.ts +14 -14
  172. package/src/utils/hex.ts +58 -0
  173. package/src/utils/index.ts +2 -0
  174. package/src/utils/no-op.ts +12 -0
  175. package/tsconfig.build.json +12 -1
  176. package/tsconfig.build.tsbuildinfo +1 -1
  177. package/esm/auth.d.ts.map +0 -1
  178. package/esm/auth.js.map +0 -1
  179. package/esm/backend.d.ts.map +0 -1
  180. package/esm/backend.js +0 -2
  181. package/esm/backend.js.map +0 -1
  182. package/esm/commands/install.d.ts +0 -10
  183. package/esm/commands/install.d.ts.map +0 -1
  184. package/esm/commands/install.js +0 -29
  185. package/esm/commands/install.js.map +0 -1
  186. package/esm/crypto.d.ts.map +0 -1
  187. package/esm/crypto.js.map +0 -1
  188. package/esm/packet.d.ts.map +0 -1
  189. package/esm/socket/dispatcher.d.ts.map +0 -1
  190. package/esm/socket/dispatcher.js.map +0 -1
  191. package/esm/socket/index.d.ts +0 -3
  192. package/esm/socket/index.d.ts.map +0 -1
  193. package/esm/socket/index.js +0 -3
  194. package/esm/socket/index.js.map +0 -1
  195. package/esm/socket/socket.d.ts.map +0 -1
  196. package/esm/socket/socket.js.map +0 -1
  197. package/src/socket/index.ts +0 -2
package/src/banner.ts ADDED
@@ -0,0 +1,82 @@
1
+ import type { AdbFeature } from "./features.js";
2
+
3
+ export enum AdbBannerKey {
4
+ Product = "ro.product.name",
5
+ Model = "ro.product.model",
6
+ Device = "ro.product.device",
7
+ Features = "features",
8
+ }
9
+
10
+ export class AdbBanner {
11
+ static parse(banner: string) {
12
+ let product: string | undefined;
13
+ let model: string | undefined;
14
+ let device: string | undefined;
15
+ let features: AdbFeature[] = [];
16
+
17
+ const pieces = banner.split("::");
18
+ if (pieces.length > 1) {
19
+ const props = pieces[1]!;
20
+ for (const prop of props.split(";")) {
21
+ if (!prop) {
22
+ continue;
23
+ }
24
+
25
+ const keyValue = prop.split("=");
26
+ if (keyValue.length !== 2) {
27
+ continue;
28
+ }
29
+
30
+ const [key, value] = keyValue;
31
+ switch (key) {
32
+ case AdbBannerKey.Product:
33
+ product = value;
34
+ break;
35
+ case AdbBannerKey.Model:
36
+ model = value;
37
+ break;
38
+ case AdbBannerKey.Device:
39
+ device = value;
40
+ break;
41
+ case AdbBannerKey.Features:
42
+ features = value!.split(",") as AdbFeature[];
43
+ break;
44
+ }
45
+ }
46
+ }
47
+
48
+ return new AdbBanner(product, model, device, features);
49
+ }
50
+
51
+ #product: string | undefined;
52
+ get product() {
53
+ return this.#product;
54
+ }
55
+
56
+ #model: string | undefined;
57
+ get model() {
58
+ return this.#model;
59
+ }
60
+
61
+ #device: string | undefined;
62
+ get device() {
63
+ return this.#device;
64
+ }
65
+
66
+ #features: AdbFeature[] = [];
67
+ get features() {
68
+ return this.#features;
69
+ }
70
+
71
+ constructor(
72
+ product: string | undefined,
73
+ model: string | undefined,
74
+ device: string | undefined,
75
+ features: AdbFeature[],
76
+ ) {
77
+ this.#product = product;
78
+ this.#model = model;
79
+ this.#device = device;
80
+ this.#features = features;
81
+ }
82
+ }
@@ -5,7 +5,7 @@ import type { Adb } from "../adb.js";
5
5
  export class AdbCommandBase extends AutoDisposable {
6
6
  protected adb: Adb;
7
7
 
8
- public constructor(adb: Adb) {
8
+ constructor(adb: Adb) {
9
9
  super();
10
10
  this.adb = adb;
11
11
  }
@@ -1,5 +1,5 @@
1
1
  import { BufferedReadableStream } from "@yume-chan/stream-extra";
2
- import Struct from "@yume-chan/struct";
2
+ import Struct, { StructEmptyError } from "@yume-chan/struct";
3
3
 
4
4
  import type { Adb } from "../adb.js";
5
5
 
@@ -58,10 +58,38 @@ export type AdbFrameBufferV2 = (typeof AdbFrameBufferV2)["TDeserializeResult"];
58
58
  */
59
59
  export type AdbFrameBuffer = AdbFrameBufferV1 | AdbFrameBufferV2;
60
60
 
61
+ export class AdbFrameBufferError extends Error {
62
+ constructor(message: string, options?: ErrorOptions) {
63
+ super(message, options);
64
+ }
65
+ }
66
+
67
+ export class AdbFrameBufferUnsupportedVersionError extends AdbFrameBufferError {
68
+ constructor(version: number) {
69
+ super(`Unsupported FrameBuffer version ${version}`);
70
+ }
71
+ }
72
+
73
+ export class AdbFrameBufferForbiddenError extends AdbFrameBufferError {
74
+ constructor() {
75
+ super("FrameBuffer is disabled by current app");
76
+ }
77
+ }
78
+
61
79
  export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
62
80
  const socket = await adb.createSocket("framebuffer:");
63
81
  const stream = new BufferedReadableStream(socket.readable);
64
- const { version } = await Version.deserialize(stream);
82
+
83
+ let version: number;
84
+ try {
85
+ ({ version } = await Version.deserialize(stream));
86
+ } catch (e) {
87
+ if (e instanceof StructEmptyError) {
88
+ throw new AdbFrameBufferForbiddenError();
89
+ }
90
+ throw e;
91
+ }
92
+
65
93
  switch (version) {
66
94
  case 1:
67
95
  // TODO: AdbFrameBuffer: does all v1 responses uses the same color space? Add it so the command returns same format for all versions.
@@ -69,6 +97,6 @@ export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
69
97
  case 2:
70
98
  return AdbFrameBufferV2.deserialize(stream);
71
99
  default:
72
- throw new Error("Unsupported FrameBuffer version");
100
+ throw new AdbFrameBufferUnsupportedVersionError(version);
73
101
  }
74
102
  }
@@ -6,23 +6,23 @@
6
6
  import { AdbCommandBase } from "./base.js";
7
7
 
8
8
  export class AdbPower extends AdbCommandBase {
9
- public reboot(name = "") {
10
- return this.adb.createSocketAndWait(`reboot:${name}`);
9
+ reboot(mode = "") {
10
+ return this.adb.createSocketAndWait(`reboot:${mode}`);
11
11
  }
12
12
 
13
- public bootloader() {
13
+ bootloader() {
14
14
  return this.reboot("bootloader");
15
15
  }
16
16
 
17
- public fastboot() {
17
+ fastboot() {
18
18
  return this.reboot("fastboot");
19
19
  }
20
20
 
21
- public recovery() {
21
+ recovery() {
22
22
  return this.reboot("recovery");
23
23
  }
24
24
 
25
- public sideload() {
25
+ sideload() {
26
26
  return this.reboot("sideload");
27
27
  }
28
28
 
@@ -31,15 +31,15 @@ export class AdbPower extends AdbCommandBase {
31
31
  *
32
32
  * Only works on some Qualcomm devices.
33
33
  */
34
- public qualcommEdlMode() {
34
+ qualcommEdlMode() {
35
35
  return this.reboot("edl");
36
36
  }
37
37
 
38
- public powerOff() {
38
+ powerOff() {
39
39
  return this.adb.subprocess.spawnAndWaitLegacy(["reboot", "-p"]);
40
40
  }
41
41
 
42
- public powerButton(longPress = false) {
42
+ powerButton(longPress = false) {
43
43
  return this.adb.subprocess.spawnAndWaitLegacy([
44
44
  "input",
45
45
  "keyevent",
@@ -52,7 +52,7 @@ export class AdbPower extends AdbCommandBase {
52
52
  *
53
53
  * Only works on Samsung devices.
54
54
  */
55
- public samsungOdin() {
55
+ samsungOdin() {
56
56
  return this.reboot("download");
57
57
  }
58
58
  }
@@ -1,15 +1,11 @@
1
1
  // cspell: ignore killforward
2
2
 
3
3
  import { AutoDisposable } from "@yume-chan/event";
4
- import {
5
- BufferedReadableStream,
6
- BufferedReadableStreamEndedError,
7
- } from "@yume-chan/stream-extra";
8
- import Struct from "@yume-chan/struct";
4
+ import { BufferedReadableStream } from "@yume-chan/stream-extra";
5
+ import Struct, { ExactReadableEndedError } from "@yume-chan/struct";
9
6
 
10
- import type { Adb } from "../adb.js";
11
- import type { AdbIncomingSocketHandler, AdbSocket } from "../socket/index.js";
12
- import { decodeUtf8 } from "../utils/index.js";
7
+ import type { Adb, AdbIncomingSocketHandler } from "../adb.js";
8
+ import { decodeUtf8, hexToNumber } from "../utils/index.js";
13
9
 
14
10
  export interface AdbForwardListener {
15
11
  deviceSerial: string;
@@ -24,23 +20,22 @@ const AdbReverseStringResponse = new Struct()
24
20
  .string("content", { lengthField: "length", lengthFieldRadix: 16 });
25
21
 
26
22
  export class AdbReverseError extends Error {
27
- public constructor(message: string) {
23
+ constructor(message: string) {
28
24
  super(message);
29
- Object.setPrototypeOf(this, AdbReverseError.prototype);
25
+ Object.setPrototypeOf(this, new.target.prototype);
30
26
  }
31
27
  }
32
28
 
33
- export class AdbReverseNotSupportedError extends Error {
34
- public constructor() {
29
+ export class AdbReverseNotSupportedError extends AdbReverseError {
30
+ constructor() {
35
31
  super(
36
- "ADB reverse tunnel is not supported on this device when connected wirelessly."
32
+ "ADB reverse tunnel is not supported on this device when connected wirelessly.",
37
33
  );
38
- Object.setPrototypeOf(this, AdbReverseNotSupportedError.prototype);
39
34
  }
40
35
  }
41
36
 
42
37
  const AdbReverseErrorResponse = new Struct()
43
- .fields(AdbReverseStringResponse)
38
+ .concat(AdbReverseStringResponse)
44
39
  .postDeserialize((value) => {
45
40
  // https://issuetracker.google.com/issues/37066218
46
41
  // ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over WiFi),
@@ -52,49 +47,37 @@ const AdbReverseErrorResponse = new Struct()
52
47
  }
53
48
  });
54
49
 
55
- export class AdbReverseCommand extends AutoDisposable {
56
- protected localAddressToHandler = new Map<
57
- string,
58
- AdbIncomingSocketHandler
59
- >();
60
-
61
- protected deviceAddressToLocalAddress = new Map<string, string>();
50
+ async function readString(stream: BufferedReadableStream, length: number) {
51
+ const buffer = await stream.readExactly(length);
52
+ return decodeUtf8(buffer);
53
+ }
62
54
 
55
+ export class AdbReverseCommand extends AutoDisposable {
63
56
  protected adb: Adb;
64
57
 
65
- protected listening = false;
58
+ readonly #deviceAddressToLocalAddress = new Map<string, string>();
66
59
 
67
- public constructor(adb: Adb) {
60
+ constructor(adb: Adb) {
68
61
  super();
69
62
 
70
63
  this.adb = adb;
71
- this.addDisposable(
72
- this.adb.onIncomingSocket(this.handleIncomingSocket)
73
- );
74
64
  }
75
65
 
76
- protected handleIncomingSocket = async (socket: AdbSocket) => {
77
- let address = socket.serviceString;
78
- // ADB daemon appends `\0` to the service string
79
- address = address.replace(/\0/g, "");
80
- return !!(await this.localAddressToHandler.get(address)?.(socket));
81
- };
82
-
83
- private async createBufferedStream(service: string) {
66
+ protected async createBufferedStream(service: string) {
84
67
  const socket = await this.adb.createSocket(service);
85
68
  return new BufferedReadableStream(socket.readable);
86
69
  }
87
70
 
88
- private async sendRequest(service: string) {
71
+ protected async sendRequest(service: string) {
89
72
  const stream = await this.createBufferedStream(service);
90
- const success = decodeUtf8(await stream.read(4)) === "OKAY";
73
+ const success = (await readString(stream, 4)) === "OKAY";
91
74
  if (!success) {
92
75
  await AdbReverseErrorResponse.deserialize(stream);
93
76
  }
94
77
  return stream;
95
78
  }
96
79
 
97
- public async list(): Promise<AdbForwardListener[]> {
80
+ async list(): Promise<AdbForwardListener[]> {
98
81
  const stream = await this.createBufferedStream("reverse:list-forward");
99
82
 
100
83
  const response = await AdbReverseStringResponse.deserialize(stream);
@@ -102,7 +85,7 @@ export class AdbReverseCommand extends AutoDisposable {
102
85
  const [deviceSerial, localName, remoteName] = line.split(" ") as [
103
86
  string,
104
87
  string,
105
- string
88
+ string,
106
89
  ];
107
90
  return { deviceSerial, localName, remoteName };
108
91
  });
@@ -111,74 +94,86 @@ export class AdbReverseCommand extends AutoDisposable {
111
94
  }
112
95
 
113
96
  /**
114
- * @param deviceAddress
115
- * The address to be listened on device by ADB daemon. Or `tcp:0` to choose an available TCP port.
116
- * @param localAddress
117
- * An identifier for the reverse tunnel.
118
- *
119
- * When a socket wants to connect to {@link deviceAddress}, native ADB client will forward that connection to {@link localAddress}.
120
- * However in this library, the {@link handler} is invoked instead. So this parameter is only used to identify the reverse tunnel.
121
- * @param handler A callback to handle incoming connections. It must return `true` if it accepts the connection.
97
+ * Add an already existing reverse tunnel. Depends on the transport type, this may not do anything.
98
+ * @param deviceAddress The address to be listened on device by ADB daemon. Or `tcp:0` to choose an available TCP port.
99
+ * @param localAddress The address that listens on the local machine.
122
100
  * @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`.
123
- * @throws {AdbReverseNotSupportedError} If ADB reverse tunnel is not supported on this device when connected wirelessly.
124
- * @throws {AdbReverseError} If ADB daemon returns an error.
125
101
  */
126
- public async add(
127
- deviceAddress: string,
128
- localAddress: string,
129
- handler: AdbIncomingSocketHandler
130
- ): Promise<string> {
102
+ async addExternal(deviceAddress: string, localAddress: string) {
131
103
  const stream = await this.sendRequest(
132
- `reverse:forward:${deviceAddress};${localAddress}`
104
+ `reverse:forward:${deviceAddress};${localAddress}`,
133
105
  );
134
106
 
135
107
  // `tcp:0` tells the device to pick an available port.
136
108
  // On Android >=8, device will respond with the selected port for all `tcp:` requests.
137
109
  if (deviceAddress.startsWith("tcp:")) {
138
- let length: number | undefined;
110
+ const position = stream.position;
139
111
  try {
140
- length = Number.parseInt(decodeUtf8(await stream.read(4)), 16);
112
+ const length = hexToNumber(await stream.readExactly(4));
113
+ const port = await readString(stream, length);
114
+ deviceAddress = `tcp:${Number.parseInt(port, 10)}`;
141
115
  } catch (e) {
142
- if (!(e instanceof BufferedReadableStreamEndedError)) {
116
+ if (
117
+ e instanceof ExactReadableEndedError &&
118
+ stream.position === position
119
+ ) {
120
+ // Android <8 doesn't have this response.
121
+ // (the stream is closed now)
122
+ // Can be safely ignored.
123
+ } else {
143
124
  throw e;
144
125
  }
145
-
146
- // Android <8 doesn't have this response.
147
- // (the stream is closed now)
148
- // Can be safely ignored.
149
- }
150
-
151
- if (length !== undefined) {
152
- const port = decodeUtf8(await stream.read(length));
153
- deviceAddress = `tcp:${Number.parseInt(port, 10)}`;
154
126
  }
155
127
  }
156
128
 
157
- this.localAddressToHandler.set(localAddress, handler);
158
- this.deviceAddressToLocalAddress.set(deviceAddress, localAddress);
159
129
  return deviceAddress;
160
-
161
- // No need to close the stream, device will close it
162
130
  }
163
131
 
164
- public async remove(deviceAddress: string): Promise<void> {
165
- await this.sendRequest(`reverse:killforward:${deviceAddress}`);
132
+ /**
133
+ * @param deviceAddress The address to be listened on device by ADB daemon. Or `tcp:0` to choose an available TCP port.
134
+ * @param handler A callback to handle incoming connections.
135
+ * @param localAddressThe The address that listens on the local machine. May be `undefined` to let the transport choose an appropriate one.
136
+ * @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`.
137
+ * @throws {AdbReverseNotSupportedError} If ADB reverse tunnel is not supported on this device when connected wirelessly.
138
+ * @throws {AdbReverseError} If ADB daemon returns an error.
139
+ */
140
+ async add(
141
+ deviceAddress: string,
142
+ handler: AdbIncomingSocketHandler,
143
+ localAddress?: string,
144
+ ): Promise<string> {
145
+ localAddress = await this.adb.transport.addReverseTunnel(
146
+ handler,
147
+ localAddress,
148
+ );
149
+
150
+ try {
151
+ deviceAddress = await this.addExternal(deviceAddress, localAddress);
152
+ this.#deviceAddressToLocalAddress.set(deviceAddress, localAddress);
153
+ return deviceAddress;
154
+ } catch (e) {
155
+ await this.adb.transport.removeReverseTunnel(localAddress);
156
+ throw e;
157
+ }
158
+ }
166
159
 
167
- if (this.deviceAddressToLocalAddress.has(deviceAddress)) {
168
- this.localAddressToHandler.delete(
169
- this.deviceAddressToLocalAddress.get(deviceAddress)!
170
- );
171
- this.deviceAddressToLocalAddress.delete(deviceAddress);
160
+ async remove(deviceAddress: string): Promise<void> {
161
+ const localAddress =
162
+ this.#deviceAddressToLocalAddress.get(deviceAddress);
163
+ if (localAddress) {
164
+ await this.adb.transport.removeReverseTunnel(localAddress);
172
165
  }
173
166
 
167
+ await this.sendRequest(`reverse:killforward:${deviceAddress}`);
168
+
174
169
  // No need to close the stream, device will close it
175
170
  }
176
171
 
177
- public async removeAll(): Promise<void> {
178
- await this.sendRequest(`reverse:killforward-all`);
172
+ async removeAll(): Promise<void> {
173
+ await this.adb.transport.clearReverseTunnels();
174
+ this.#deviceAddressToLocalAddress.clear();
179
175
 
180
- this.deviceAddressToLocalAddress.clear();
181
- this.localAddressToHandler.clear();
176
+ await this.sendRequest(`reverse:killforward-all`);
182
177
 
183
178
  // No need to close the stream, device will close it
184
179
  }
@@ -1,4 +1,4 @@
1
- import { DecodeUtf8Stream, GatherStringStream } from "@yume-chan/stream-extra";
1
+ import { ConcatStringStream, DecodeUtf8Stream } from "@yume-chan/stream-extra";
2
2
 
3
3
  import { AdbCommandBase } from "../base.js";
4
4
 
@@ -37,10 +37,10 @@ export interface AdbSubprocessWaitResult {
37
37
  }
38
38
 
39
39
  export class AdbSubprocess extends AdbCommandBase {
40
- private async createProtocol(
40
+ async #createProtocol(
41
41
  mode: "pty" | "raw",
42
42
  command?: string | string[],
43
- options?: Partial<AdbSubprocessOptions>
43
+ options?: Partial<AdbSubprocessOptions>,
44
44
  ): Promise<AdbSubprocessProtocol> {
45
45
  const { protocols } = { ...DEFAULT_OPTIONS, ...options };
46
46
 
@@ -75,11 +75,11 @@ export class AdbSubprocess extends AdbCommandBase {
75
75
  * @param options The options for creating the `AdbSubprocessProtocol`
76
76
  * @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
77
77
  */
78
- public shell(
78
+ shell(
79
79
  command?: string | string[],
80
- options?: Partial<AdbSubprocessOptions>
80
+ options?: Partial<AdbSubprocessOptions>,
81
81
  ): Promise<AdbSubprocessProtocol> {
82
- return this.createProtocol("pty", command, options);
82
+ return this.#createProtocol("pty", command, options);
83
83
  }
84
84
 
85
85
  /**
@@ -91,11 +91,11 @@ export class AdbSubprocess extends AdbCommandBase {
91
91
  * @param options The options for creating the `AdbSubprocessProtocol`
92
92
  * @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
93
93
  */
94
- public spawn(
94
+ spawn(
95
95
  command: string | string[],
96
- options?: Partial<AdbSubprocessOptions>
96
+ options?: Partial<AdbSubprocessOptions>,
97
97
  ): Promise<AdbSubprocessProtocol> {
98
- return this.createProtocol("raw", command, options);
98
+ return this.#createProtocol("raw", command, options);
99
99
  }
100
100
 
101
101
  /**
@@ -104,24 +104,25 @@ export class AdbSubprocess extends AdbCommandBase {
104
104
  * @param options The options for creating the `AdbSubprocessProtocol`
105
105
  * @returns The entire output of the command
106
106
  */
107
- public async spawnAndWait(
107
+ async spawnAndWait(
108
108
  command: string | string[],
109
- options?: Partial<AdbSubprocessOptions>
109
+ options?: Partial<AdbSubprocessOptions>,
110
110
  ): Promise<AdbSubprocessWaitResult> {
111
- const shell = await this.spawn(command, options);
112
-
113
- const stdout = new GatherStringStream();
114
- const stderr = new GatherStringStream();
115
-
116
- const [, , exitCode] = await Promise.all([
117
- shell.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(stdout),
118
- shell.stderr.pipeThrough(new DecodeUtf8Stream()).pipeTo(stderr),
119
- shell.exit,
111
+ const process = await this.spawn(command, options);
112
+
113
+ const [stdout, stderr, exitCode] = await Promise.all([
114
+ process.stdout
115
+ .pipeThrough(new DecodeUtf8Stream())
116
+ .pipeThrough(new ConcatStringStream()),
117
+ process.stderr
118
+ .pipeThrough(new DecodeUtf8Stream())
119
+ .pipeThrough(new ConcatStringStream()),
120
+ process.exit,
120
121
  ]);
121
122
 
122
123
  return {
123
- stdout: stdout.result,
124
- stderr: stderr.result,
124
+ stdout,
125
+ stderr,
125
126
  exitCode,
126
127
  };
127
128
  }
@@ -131,9 +132,7 @@ export class AdbSubprocess extends AdbCommandBase {
131
132
  * @param command The command to run
132
133
  * @returns The entire output of the command
133
134
  */
134
- public async spawnAndWaitLegacy(
135
- command: string | string[]
136
- ): Promise<string> {
135
+ async spawnAndWaitLegacy(command: string | string[]): Promise<string> {
137
136
  const { stdout } = await this.spawnAndWait(command, {
138
137
  protocols: [AdbSubprocessNoneProtocol],
139
138
  });
@@ -1,7 +1,6 @@
1
1
  import { DuplexStreamFactory, ReadableStream } from "@yume-chan/stream-extra";
2
2
 
3
- import type { Adb } from "../../../adb.js";
4
- import type { AdbSocket } from "../../../socket/index.js";
3
+ import type { Adb, AdbSocket } from "../../../adb.js";
5
4
 
6
5
  import type { AdbSubprocessProtocol } from "./types.js";
7
6
 
@@ -14,75 +13,75 @@ import type { AdbSubprocessProtocol } from "./types.js";
14
13
  * * `resize`: No
15
14
  */
16
15
  export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
17
- public static isSupported() {
16
+ static isSupported() {
18
17
  return true;
19
18
  }
20
19
 
21
- public static async pty(adb: Adb, command: string) {
20
+ static async pty(adb: Adb, command: string) {
22
21
  return new AdbSubprocessNoneProtocol(
23
- await adb.createSocket(`shell:${command}`)
22
+ await adb.createSocket(`shell:${command}`),
24
23
  );
25
24
  }
26
25
 
27
- public static async raw(adb: Adb, command: string) {
26
+ static async raw(adb: Adb, command: string) {
28
27
  // `shell,raw:${command}` also triggers raw mode,
29
28
  // But is not supported on Android version <7.
30
29
  return new AdbSubprocessNoneProtocol(
31
- await adb.createSocket(`exec:${command}`)
30
+ await adb.createSocket(`exec:${command}`),
32
31
  );
33
32
  }
34
33
 
35
- private readonly socket: AdbSocket;
34
+ readonly #socket: AdbSocket;
36
35
 
37
- private readonly duplex: DuplexStreamFactory<Uint8Array, Uint8Array>;
36
+ readonly #duplex: DuplexStreamFactory<Uint8Array, Uint8Array>;
38
37
 
39
38
  // Legacy shell forwards all data to stdin.
40
- public get stdin() {
41
- return this.socket.writable;
39
+ get stdin() {
40
+ return this.#socket.writable;
42
41
  }
43
42
 
44
- private _stdout: ReadableStream<Uint8Array>;
43
+ #stdout: ReadableStream<Uint8Array>;
45
44
  /**
46
45
  * Legacy shell mixes stdout and stderr.
47
46
  */
48
- public get stdout() {
49
- return this._stdout;
47
+ get stdout() {
48
+ return this.#stdout;
50
49
  }
51
50
 
52
- private _stderr: ReadableStream<Uint8Array>;
51
+ #stderr: ReadableStream<Uint8Array>;
53
52
  /**
54
53
  * `stderr` will always be empty.
55
54
  */
56
- public get stderr() {
57
- return this._stderr;
55
+ get stderr() {
56
+ return this.#stderr;
58
57
  }
59
58
 
60
- private _exit: Promise<number>;
61
- public get exit() {
62
- return this._exit;
59
+ #exit: Promise<number>;
60
+ get exit() {
61
+ return this.#exit;
63
62
  }
64
63
 
65
- public constructor(socket: AdbSocket) {
66
- this.socket = socket;
64
+ constructor(socket: AdbSocket) {
65
+ this.#socket = socket;
67
66
 
68
67
  // Link `stdout`, `stderr` and `stdin` together,
69
68
  // so closing any of them will close the others.
70
- this.duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
69
+ this.#duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
71
70
  close: async () => {
72
- await this.socket.close();
71
+ await this.#socket.close();
73
72
  },
74
73
  });
75
74
 
76
- this._stdout = this.duplex.wrapReadable(this.socket.readable);
77
- this._stderr = this.duplex.wrapReadable(new ReadableStream());
78
- this._exit = this.duplex.closed.then(() => 0);
75
+ this.#stdout = this.#duplex.wrapReadable(this.#socket.readable);
76
+ this.#stderr = this.#duplex.wrapReadable(new ReadableStream());
77
+ this.#exit = this.#duplex.closed.then(() => 0);
79
78
  }
80
79
 
81
- public resize() {
80
+ resize() {
82
81
  // Not supported, but don't throw.
83
82
  }
84
83
 
85
- public kill() {
86
- return this.duplex.close();
84
+ kill() {
85
+ return this.#duplex.close();
87
86
  }
88
87
  }