@yume-chan/adb 0.0.18 → 0.0.20

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 (241) hide show
  1. package/CHANGELOG.json +54 -0
  2. package/CHANGELOG.md +25 -1
  3. package/README.md +220 -103
  4. package/esm/adb.d.ts +43 -60
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +68 -249
  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 +6 -6
  13. package/esm/commands/base.d.ts.map +1 -1
  14. package/esm/commands/base.js +8 -8
  15. package/esm/commands/framebuffer.d.ts +53 -53
  16. package/esm/commands/framebuffer.d.ts.map +1 -1
  17. package/esm/commands/framebuffer.js +46 -46
  18. package/esm/commands/framebuffer.js.map +1 -1
  19. package/esm/commands/index.d.ts +7 -8
  20. package/esm/commands/index.d.ts.map +1 -1
  21. package/esm/commands/index.js +7 -8
  22. package/esm/commands/index.js.map +1 -1
  23. package/esm/commands/power.d.ts +22 -22
  24. package/esm/commands/power.js +48 -48
  25. package/esm/commands/reverse.d.ts +40 -28
  26. package/esm/commands/reverse.d.ts.map +1 -1
  27. package/esm/commands/reverse.js +130 -100
  28. package/esm/commands/reverse.js.map +1 -1
  29. package/esm/commands/subprocess/command.d.ts +57 -51
  30. package/esm/commands/subprocess/command.d.ts.map +1 -1
  31. package/esm/commands/subprocess/command.js +86 -80
  32. package/esm/commands/subprocess/command.js.map +1 -1
  33. package/esm/commands/subprocess/index.d.ts +3 -3
  34. package/esm/commands/subprocess/index.js +3 -3
  35. package/esm/commands/subprocess/protocols/index.d.ts +3 -3
  36. package/esm/commands/subprocess/protocols/index.js +3 -3
  37. package/esm/commands/subprocess/protocols/none.d.ts +30 -35
  38. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  39. package/esm/commands/subprocess/protocols/none.js +65 -55
  40. package/esm/commands/subprocess/protocols/none.js.map +1 -1
  41. package/esm/commands/subprocess/protocols/shell.d.ts +33 -38
  42. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  43. package/esm/commands/subprocess/protocols/shell.js +172 -162
  44. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  45. package/esm/commands/subprocess/protocols/types.d.ts +48 -49
  46. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  47. package/esm/commands/subprocess/protocols/types.js +1 -1
  48. package/esm/commands/subprocess/utils.d.ts +1 -1
  49. package/esm/commands/subprocess/utils.js +18 -18
  50. package/esm/commands/sync/index.d.ts +8 -7
  51. package/esm/commands/sync/index.d.ts.map +1 -1
  52. package/esm/commands/sync/index.js +8 -7
  53. package/esm/commands/sync/index.js.map +1 -1
  54. package/esm/commands/sync/list.d.ts +42 -40
  55. package/esm/commands/sync/list.d.ts.map +1 -1
  56. package/esm/commands/sync/list.js +67 -47
  57. package/esm/commands/sync/list.js.map +1 -1
  58. package/esm/commands/sync/pull.d.ts +13 -11
  59. package/esm/commands/sync/pull.d.ts.map +1 -1
  60. package/esm/commands/sync/pull.js +35 -37
  61. package/esm/commands/sync/pull.js.map +1 -1
  62. package/esm/commands/sync/push.d.ts +57 -7
  63. package/esm/commands/sync/push.d.ts.map +1 -1
  64. package/esm/commands/sync/push.js +89 -22
  65. package/esm/commands/sync/push.js.map +1 -1
  66. package/esm/commands/sync/request.d.ts +26 -23
  67. package/esm/commands/sync/request.d.ts.map +1 -1
  68. package/esm/commands/sync/request.js +40 -42
  69. package/esm/commands/sync/request.js.map +1 -1
  70. package/esm/commands/sync/response.d.ts +19 -19
  71. package/esm/commands/sync/response.d.ts.map +1 -1
  72. package/esm/commands/sync/response.js +54 -54
  73. package/esm/commands/sync/response.js.map +1 -1
  74. package/esm/commands/sync/socket.d.ts +22 -0
  75. package/esm/commands/sync/socket.d.ts.map +1 -0
  76. package/esm/commands/sync/socket.js +69 -0
  77. package/esm/commands/sync/socket.js.map +1 -0
  78. package/esm/commands/sync/stat.d.ts +72 -72
  79. package/esm/commands/sync/stat.d.ts.map +1 -1
  80. package/esm/commands/sync/stat.js +116 -104
  81. package/esm/commands/sync/stat.js.map +1 -1
  82. package/esm/commands/sync/sync.d.ts +65 -60
  83. package/esm/commands/sync/sync.d.ts.map +1 -1
  84. package/esm/commands/sync/sync.js +123 -149
  85. package/esm/commands/sync/sync.js.map +1 -1
  86. package/esm/commands/tcpip.d.ts +5 -5
  87. package/esm/commands/tcpip.d.ts.map +1 -1
  88. package/esm/commands/tcpip.js +20 -18
  89. package/esm/commands/tcpip.js.map +1 -1
  90. package/esm/{auth.d.ts → daemon/auth.d.ts} +50 -51
  91. package/esm/daemon/auth.d.ts.map +1 -0
  92. package/esm/{auth.js → daemon/auth.js} +95 -96
  93. package/esm/daemon/auth.js.map +1 -0
  94. package/esm/daemon/connection.d.ts +11 -0
  95. package/esm/daemon/connection.d.ts.map +1 -0
  96. package/esm/daemon/connection.js +2 -0
  97. package/esm/daemon/connection.js.map +1 -0
  98. package/esm/{crypto.d.ts → daemon/crypto.d.ts} +36 -36
  99. package/esm/daemon/crypto.d.ts.map +1 -0
  100. package/esm/{crypto.js → daemon/crypto.js} +244 -248
  101. package/esm/daemon/crypto.js.map +1 -0
  102. package/esm/daemon/device.d.ts +9 -0
  103. package/esm/daemon/device.d.ts.map +1 -0
  104. package/esm/daemon/device.js +2 -0
  105. package/esm/daemon/device.js.map +1 -0
  106. package/esm/daemon/dispatcher.d.ts +42 -0
  107. package/esm/daemon/dispatcher.d.ts.map +1 -0
  108. package/esm/{socket → daemon}/dispatcher.js +225 -234
  109. package/esm/daemon/dispatcher.js.map +1 -0
  110. package/esm/daemon/index.d.ts +8 -0
  111. package/esm/daemon/index.d.ts.map +1 -0
  112. package/esm/daemon/index.js +8 -0
  113. package/esm/daemon/index.js.map +1 -0
  114. package/esm/{packet.d.ts → daemon/packet.d.ts} +45 -46
  115. package/esm/daemon/packet.d.ts.map +1 -0
  116. package/esm/{packet.js → daemon/packet.js} +43 -48
  117. package/esm/daemon/packet.js.map +1 -0
  118. package/esm/daemon/socket.d.ts +58 -0
  119. package/esm/daemon/socket.d.ts.map +1 -0
  120. package/esm/{socket → daemon}/socket.js +121 -118
  121. package/esm/daemon/socket.js.map +1 -0
  122. package/esm/daemon/transport.d.ts +45 -0
  123. package/esm/daemon/transport.d.ts.map +1 -0
  124. package/esm/daemon/transport.js +184 -0
  125. package/esm/daemon/transport.js.map +1 -0
  126. package/esm/features.d.ts +10 -7
  127. package/esm/features.d.ts.map +1 -1
  128. package/esm/features.js +13 -10
  129. package/esm/features.js.map +1 -1
  130. package/esm/index.d.ts +7 -9
  131. package/esm/index.d.ts.map +1 -1
  132. package/esm/index.js +7 -9
  133. package/esm/index.js.map +1 -1
  134. package/esm/server/client.d.ts +78 -0
  135. package/esm/server/client.d.ts.map +1 -0
  136. package/esm/server/client.js +329 -0
  137. package/esm/server/client.js.map +1 -0
  138. package/esm/server/index.d.ts +3 -0
  139. package/esm/server/index.d.ts.map +1 -0
  140. package/esm/server/index.js +3 -0
  141. package/esm/server/index.js.map +1 -0
  142. package/esm/server/transport.d.ts +19 -0
  143. package/esm/server/transport.d.ts.map +1 -0
  144. package/esm/server/transport.js +44 -0
  145. package/esm/server/transport.js.map +1 -0
  146. package/esm/utils/auto-reset-event.d.ts +8 -9
  147. package/esm/utils/auto-reset-event.d.ts.map +1 -1
  148. package/esm/utils/auto-reset-event.js +33 -33
  149. package/esm/utils/auto-reset-event.js.map +1 -1
  150. package/esm/utils/base64.d.ts +27 -27
  151. package/esm/utils/base64.js +246 -246
  152. package/esm/utils/base64.js.map +1 -1
  153. package/esm/utils/conditional-variable.d.ts +8 -0
  154. package/esm/utils/conditional-variable.d.ts.map +1 -0
  155. package/esm/utils/conditional-variable.js +34 -0
  156. package/esm/utils/conditional-variable.js.map +1 -0
  157. package/esm/utils/hex.d.ts +3 -0
  158. package/esm/utils/hex.d.ts.map +1 -0
  159. package/esm/utils/hex.js +55 -0
  160. package/esm/utils/hex.js.map +1 -0
  161. package/esm/utils/index.d.ts +6 -3
  162. package/esm/utils/index.d.ts.map +1 -1
  163. package/esm/utils/index.js +6 -3
  164. package/esm/utils/index.js.map +1 -1
  165. package/esm/utils/no-op.d.ts +7 -0
  166. package/esm/utils/no-op.d.ts.map +1 -0
  167. package/esm/utils/no-op.js +12 -0
  168. package/esm/utils/no-op.js.map +1 -0
  169. package/package.json +14 -12
  170. package/src/adb.ts +47 -248
  171. package/src/banner.ts +82 -0
  172. package/src/commands/base.ts +1 -1
  173. package/src/commands/framebuffer.ts +4 -4
  174. package/src/commands/index.ts +7 -8
  175. package/src/commands/power.ts +2 -2
  176. package/src/commands/reverse.ts +90 -70
  177. package/src/commands/subprocess/command.ts +12 -4
  178. package/src/commands/subprocess/index.ts +3 -3
  179. package/src/commands/subprocess/protocols/index.ts +3 -3
  180. package/src/commands/subprocess/protocols/none.ts +36 -23
  181. package/src/commands/subprocess/protocols/shell.ts +94 -74
  182. package/src/commands/subprocess/protocols/types.ts +7 -7
  183. package/src/commands/subprocess/utils.ts +1 -1
  184. package/src/commands/sync/index.ts +8 -7
  185. package/src/commands/sync/list.ts +44 -25
  186. package/src/commands/sync/pull.ts +40 -45
  187. package/src/commands/sync/push.ts +158 -39
  188. package/src/commands/sync/request.ts +21 -16
  189. package/src/commands/sync/response.ts +10 -9
  190. package/src/commands/sync/socket.ts +99 -0
  191. package/src/commands/sync/stat.ts +47 -42
  192. package/src/commands/sync/sync.ts +73 -116
  193. package/src/commands/tcpip.ts +9 -7
  194. package/src/{auth.ts → daemon/auth.ts} +24 -23
  195. package/src/{crypto.ts → daemon/crypto.ts} +25 -33
  196. package/src/daemon/device.ts +14 -0
  197. package/src/{socket → daemon}/dispatcher.ts +95 -117
  198. package/src/daemon/index.ts +7 -0
  199. package/src/daemon/packet.ts +75 -0
  200. package/src/{socket → daemon}/socket.ts +71 -56
  201. package/src/daemon/transport.ts +269 -0
  202. package/src/features.ts +9 -6
  203. package/src/index.ts +3 -5
  204. package/src/server/client.ts +476 -0
  205. package/src/server/index.ts +2 -0
  206. package/src/server/transport.ts +77 -0
  207. package/src/utils/auto-reset-event.ts +14 -15
  208. package/src/utils/conditional-variable.ts +45 -0
  209. package/src/utils/hex.ts +58 -0
  210. package/src/utils/index.ts +6 -3
  211. package/src/utils/no-op.ts +12 -0
  212. package/tsconfig.build.json +12 -1
  213. package/tsconfig.build.tsbuildinfo +1 -1
  214. package/esm/auth.d.ts.map +0 -1
  215. package/esm/auth.js.map +0 -1
  216. package/esm/backend.d.ts +0 -9
  217. package/esm/backend.d.ts.map +0 -1
  218. package/esm/backend.js +0 -2
  219. package/esm/backend.js.map +0 -1
  220. package/esm/commands/install.d.ts +0 -4
  221. package/esm/commands/install.d.ts.map +0 -1
  222. package/esm/commands/install.js +0 -29
  223. package/esm/commands/install.js.map +0 -1
  224. package/esm/crypto.d.ts.map +0 -1
  225. package/esm/crypto.js.map +0 -1
  226. package/esm/packet.d.ts.map +0 -1
  227. package/esm/packet.js.map +0 -1
  228. package/esm/socket/dispatcher.d.ts +0 -60
  229. package/esm/socket/dispatcher.d.ts.map +0 -1
  230. package/esm/socket/dispatcher.js.map +0 -1
  231. package/esm/socket/index.d.ts +0 -3
  232. package/esm/socket/index.d.ts.map +0 -1
  233. package/esm/socket/index.js +0 -3
  234. package/esm/socket/index.js.map +0 -1
  235. package/esm/socket/socket.d.ts +0 -60
  236. package/esm/socket/socket.d.ts.map +0 -1
  237. package/esm/socket/socket.js.map +0 -1
  238. package/src/backend.ts +0 -14
  239. package/src/commands/install.ts +0 -40
  240. package/src/packet.ts +0 -90
  241. 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
+ public 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
+ public get product() {
53
+ return this.#product;
54
+ }
55
+
56
+ #model: string | undefined;
57
+ public get model() {
58
+ return this.#model;
59
+ }
60
+
61
+ #device: string | undefined;
62
+ public get device() {
63
+ return this.#device;
64
+ }
65
+
66
+ #features: AdbFeature[] = [];
67
+ public get features() {
68
+ return this.#features;
69
+ }
70
+
71
+ public 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
+ }
@@ -1,6 +1,6 @@
1
1
  import { AutoDisposable } from "@yume-chan/event";
2
2
 
3
- import { type Adb } from "../adb.js";
3
+ import type { Adb } from "../adb.js";
4
4
 
5
5
  export class AdbCommandBase extends AutoDisposable {
6
6
  protected adb: Adb;
@@ -1,7 +1,7 @@
1
1
  import { BufferedReadableStream } from "@yume-chan/stream-extra";
2
2
  import Struct from "@yume-chan/struct";
3
3
 
4
- import { type Adb } from "../adb.js";
4
+ import type { Adb } from "../adb.js";
5
5
 
6
6
  const Version = new Struct({ littleEndian: true }).uint32("version");
7
7
 
@@ -20,7 +20,7 @@ export const AdbFrameBufferV1 = new Struct({ littleEndian: true })
20
20
  .uint32("alpha_length")
21
21
  .uint8Array("data", { lengthField: "size" });
22
22
 
23
- export type AdbFrameBufferV1 = typeof AdbFrameBufferV1["TDeserializeResult"];
23
+ export type AdbFrameBufferV1 = (typeof AdbFrameBufferV1)["TDeserializeResult"];
24
24
 
25
25
  export const AdbFrameBufferV2 = new Struct({ littleEndian: true })
26
26
  .uint32("bpp")
@@ -38,7 +38,7 @@ export const AdbFrameBufferV2 = new Struct({ littleEndian: true })
38
38
  .uint32("alpha_length")
39
39
  .uint8Array("data", { lengthField: "size" });
40
40
 
41
- export type AdbFrameBufferV2 = typeof AdbFrameBufferV2["TDeserializeResult"];
41
+ export type AdbFrameBufferV2 = (typeof AdbFrameBufferV2)["TDeserializeResult"];
42
42
 
43
43
  /**
44
44
  * ADB uses 8 int32 fields to describe bit depths
@@ -69,6 +69,6 @@ export async function framebuffer(adb: Adb): Promise<AdbFrameBuffer> {
69
69
  case 2:
70
70
  return AdbFrameBufferV2.deserialize(stream);
71
71
  default:
72
- throw new Error("Unknown FrameBuffer version");
72
+ throw new Error("Unsupported FrameBuffer version");
73
73
  }
74
74
  }
@@ -1,8 +1,7 @@
1
- export * from './base.js';
2
- export * from './framebuffer.js';
3
- export * from './install.js';
4
- export * from './power.js';
5
- export * from './reverse.js';
6
- export * from './subprocess/index.js';
7
- export * from './sync/index.js';
8
- export * from './tcpip.js';
1
+ export * from "./base.js";
2
+ export * from "./framebuffer.js";
3
+ export * from "./power.js";
4
+ export * from "./reverse.js";
5
+ export * from "./subprocess/index.js";
6
+ export * from "./sync/index.js";
7
+ export * from "./tcpip.js";
@@ -6,8 +6,8 @@
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
+ public reboot(mode = "") {
10
+ return this.adb.createSocketAndWait(`reboot:${mode}`);
11
11
  }
12
12
 
13
13
  public bootloader() {
@@ -1,18 +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";
9
-
10
- import { type Adb } from "../adb.js";
11
- import {
12
- type AdbIncomingSocketHandler,
13
- type AdbSocket,
14
- } from "../socket/index.js";
15
- import { decodeUtf8 } from "../utils/index.js";
4
+ import { BufferedReadableStream } from "@yume-chan/stream-extra";
5
+ import Struct, { ExactReadableEndedError } from "@yume-chan/struct";
6
+
7
+ import type { Adb, AdbIncomingSocketHandler } from "../adb.js";
8
+ import { decodeUtf8, hexToNumber } from "../utils/index.js";
16
9
 
17
10
  export interface AdbForwardListener {
18
11
  deviceSerial: string;
@@ -26,48 +19,58 @@ const AdbReverseStringResponse = new Struct()
26
19
  .string("length", { length: 4 })
27
20
  .string("content", { lengthField: "length", lengthFieldRadix: 16 });
28
21
 
22
+ export class AdbReverseError extends Error {
23
+ public constructor(message: string) {
24
+ super(message);
25
+ Object.setPrototypeOf(this, new.target.prototype);
26
+ }
27
+ }
28
+
29
+ export class AdbReverseNotSupportedError extends AdbReverseError {
30
+ public constructor() {
31
+ super(
32
+ "ADB reverse tunnel is not supported on this device when connected wirelessly."
33
+ );
34
+ }
35
+ }
36
+
29
37
  const AdbReverseErrorResponse = new Struct()
30
- .fields(AdbReverseStringResponse)
38
+ .concat(AdbReverseStringResponse)
31
39
  .postDeserialize((value) => {
32
- throw new Error(value.content);
40
+ // https://issuetracker.google.com/issues/37066218
41
+ // ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over WiFi),
42
+ // and returns this confusing "more than one device/emulator" error.
43
+ if (value.content === "more than one device/emulator") {
44
+ throw new AdbReverseNotSupportedError();
45
+ } else {
46
+ throw new AdbReverseError(value.content);
47
+ }
33
48
  });
34
49
 
35
- export class AdbReverseCommand extends AutoDisposable {
36
- protected localAddressToHandler = new Map<
37
- string,
38
- AdbIncomingSocketHandler
39
- >();
40
-
41
- 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
+ }
42
54
 
55
+ export class AdbReverseCommand extends AutoDisposable {
43
56
  protected adb: Adb;
44
57
 
45
- protected listening = false;
58
+ readonly #deviceAddressToLocalAddress = new Map<string, string>();
46
59
 
47
60
  public constructor(adb: Adb) {
48
61
  super();
49
62
 
50
63
  this.adb = adb;
51
- this.addDisposable(
52
- this.adb.onIncomingSocket(this.handleIncomingSocket)
53
- );
54
64
  }
55
65
 
56
- protected handleIncomingSocket = async (socket: AdbSocket) => {
57
- let address = socket.serviceString;
58
- // ADB daemon appends `\0` to the service string
59
- address = address.replace(/\0/g, "");
60
- return !!(await this.localAddressToHandler.get(address)?.(socket));
61
- };
62
-
63
- private async createBufferedStream(service: string) {
66
+ protected async createBufferedStream(service: string) {
64
67
  const socket = await this.adb.createSocket(service);
65
68
  return new BufferedReadableStream(socket.readable);
66
69
  }
67
70
 
68
- private async sendRequest(service: string) {
71
+ protected async sendRequest(service: string) {
69
72
  const stream = await this.createBufferedStream(service);
70
- const success = decodeUtf8(await stream.read(4)) === "OKAY";
73
+ const success = (await readString(stream, 4)) === "OKAY";
71
74
  if (!success) {
72
75
  await AdbReverseErrorResponse.deserialize(stream);
73
76
  }
@@ -91,18 +94,12 @@ export class AdbReverseCommand extends AutoDisposable {
91
94
  }
92
95
 
93
96
  /**
94
- * @param deviceAddress The address adbd on device is listening on. Can be `tcp:0` to let adbd choose an available TCP port by itself.
95
- * @param localAddress Native ADB client will open a connection to this address when reverse connection received. In WebADB, it's only used to uniquely identify a reverse tunnel registry, `handler` will be called to handle the connection.
96
- * @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.
97
100
  * @returns `tcp:{ACTUAL_LISTENING_PORT}`, If `deviceAddress` is `tcp:0`; otherwise, `deviceAddress`.
98
101
  */
99
- public async add(
100
- deviceAddress: string,
101
- localAddress: string,
102
- handler: AdbIncomingSocketHandler
103
- ): Promise<string> {
104
- // TODO(reverse): handle ADB bug
105
- // https://issuetracker.google.com/issues/37066218
102
+ public async addExternal(deviceAddress: string, localAddress: string) {
106
103
  const stream = await this.sendRequest(
107
104
  `reverse:forward:${deviceAddress};${localAddress}`
108
105
  );
@@ -110,50 +107,73 @@ export class AdbReverseCommand extends AutoDisposable {
110
107
  // `tcp:0` tells the device to pick an available port.
111
108
  // On Android >=8, device will respond with the selected port for all `tcp:` requests.
112
109
  if (deviceAddress.startsWith("tcp:")) {
113
- let length: number | undefined;
110
+ const position = stream.position;
114
111
  try {
115
- 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)}`;
116
115
  } catch (e) {
117
- 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 {
118
124
  throw e;
119
125
  }
120
-
121
- // Android <8 doesn't have this response.
122
- // (the stream is closed now)
123
- // Can be safely ignored.
124
- }
125
-
126
- if (length !== undefined) {
127
- const port = decodeUtf8(await stream.read(length));
128
- deviceAddress = `tcp:${Number.parseInt(port, 10)}`;
129
126
  }
130
127
  }
131
128
 
132
- this.localAddressToHandler.set(localAddress, handler);
133
- this.deviceAddressToLocalAddress.set(deviceAddress, localAddress);
134
129
  return deviceAddress;
130
+ }
135
131
 
136
- // No need to close the stream, device will close it
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
+ public 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
+ }
137
158
  }
138
159
 
139
160
  public async remove(deviceAddress: string): Promise<void> {
140
- await this.sendRequest(`reverse:killforward:${deviceAddress}`);
141
-
142
- if (this.deviceAddressToLocalAddress.has(deviceAddress)) {
143
- this.localAddressToHandler.delete(
144
- this.deviceAddressToLocalAddress.get(deviceAddress)!
145
- );
146
- this.deviceAddressToLocalAddress.delete(deviceAddress);
161
+ const localAddress =
162
+ this.#deviceAddressToLocalAddress.get(deviceAddress);
163
+ if (localAddress) {
164
+ await this.adb.transport.removeReverseTunnel(localAddress);
147
165
  }
148
166
 
167
+ await this.sendRequest(`reverse:killforward:${deviceAddress}`);
168
+
149
169
  // No need to close the stream, device will close it
150
170
  }
151
171
 
152
172
  public async removeAll(): Promise<void> {
153
- await this.sendRequest(`reverse:killforward-all`);
173
+ await this.adb.transport.clearReverseTunnels();
174
+ this.#deviceAddressToLocalAddress.clear();
154
175
 
155
- this.deviceAddressToLocalAddress.clear();
156
- this.localAddressToHandler.clear();
176
+ await this.sendRequest(`reverse:killforward-all`);
157
177
 
158
178
  // No need to close the stream, device will close it
159
179
  }
@@ -2,11 +2,13 @@ import { DecodeUtf8Stream, GatherStringStream } from "@yume-chan/stream-extra";
2
2
 
3
3
  import { AdbCommandBase } from "../base.js";
4
4
 
5
+ import type {
6
+ AdbSubprocessProtocol,
7
+ AdbSubprocessProtocolConstructor,
8
+ } from "./protocols/index.js";
5
9
  import {
6
10
  AdbSubprocessNoneProtocol,
7
11
  AdbSubprocessShellProtocol,
8
- type AdbSubprocessProtocol,
9
- type AdbSubprocessProtocolConstructor,
10
12
  } from "./protocols/index.js";
11
13
 
12
14
  export interface AdbSubprocessOptions {
@@ -65,7 +67,10 @@ export class AdbSubprocess extends AdbCommandBase {
65
67
  }
66
68
 
67
69
  /**
68
- * Spawns an executable in PTY (interactive) mode.
70
+ * Spawns an executable in PTY mode.
71
+ *
72
+ * Redirection mode is enough for most simple commands, but PTY mode is required for
73
+ * commands that manipulate the terminal, such as `vi` and `less`.
69
74
  * @param command The command to run. If omitted, the default shell will be spawned.
70
75
  * @param options The options for creating the `AdbSubprocessProtocol`
71
76
  * @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
@@ -78,7 +83,10 @@ export class AdbSubprocess extends AdbCommandBase {
78
83
  }
79
84
 
80
85
  /**
81
- * Spawns an executable and pipe the output.
86
+ * Spawns an executable and redirect the standard input/output stream.
87
+ *
88
+ * Redirection mode is enough for most simple commands, but PTY mode is required for
89
+ * commands that manipulate the terminal, such as `vi` and `less`.
82
90
  * @param command The command to run, or an array of strings containing both command and args.
83
91
  * @param options The options for creating the `AdbSubprocessProtocol`
84
92
  * @returns A new `AdbSubprocessProtocol` instance connecting to the spawned process.
@@ -1,3 +1,3 @@
1
- export * from './command.js';
2
- export * from './protocols/index.js';
3
- export * from './utils.js';
1
+ export * from "./command.js";
2
+ export * from "./protocols/index.js";
3
+ export * from "./utils.js";
@@ -1,3 +1,3 @@
1
- export * from './none.js';
2
- export * from './shell.js';
3
- export * from './types.js';
1
+ export * from "./none.js";
2
+ export * from "./shell.js";
3
+ export * from "./types.js";
@@ -1,9 +1,8 @@
1
- import { DuplexStreamFactory, ReadableStream } from '@yume-chan/stream-extra';
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
- import { type AdbSubprocessProtocol } from "./types.js";
5
+ import type { AdbSubprocessProtocol } from "./types.js";
7
6
 
8
7
  /**
9
8
  * The legacy shell
@@ -14,54 +13,68 @@ import { type AdbSubprocessProtocol } from "./types.js";
14
13
  * * `resize`: No
15
14
  */
16
15
  export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
17
- public static isSupported() { return true; }
16
+ public static isSupported() {
17
+ return true;
18
+ }
18
19
 
19
20
  public static async pty(adb: Adb, command: string) {
20
- return new AdbSubprocessNoneProtocol(await adb.createSocket(`shell:${command}`));
21
+ return new AdbSubprocessNoneProtocol(
22
+ await adb.createSocket(`shell:${command}`)
23
+ );
21
24
  }
22
25
 
23
26
  public static async raw(adb: Adb, command: string) {
24
27
  // `shell,raw:${command}` also triggers raw mode,
25
28
  // But is not supported on Android version <7.
26
- return new AdbSubprocessNoneProtocol(await adb.createSocket(`exec:${command}`));
29
+ return new AdbSubprocessNoneProtocol(
30
+ await adb.createSocket(`exec:${command}`)
31
+ );
27
32
  }
28
33
 
29
- private readonly socket: AdbSocket;
34
+ readonly #socket: AdbSocket;
30
35
 
31
- private readonly duplex: DuplexStreamFactory<Uint8Array, Uint8Array>;
36
+ readonly #duplex: DuplexStreamFactory<Uint8Array, Uint8Array>;
32
37
 
33
38
  // Legacy shell forwards all data to stdin.
34
- public get stdin() { return this.socket.writable; }
39
+ public get stdin() {
40
+ return this.#socket.writable;
41
+ }
35
42
 
36
- private _stdout: ReadableStream<Uint8Array>;
43
+ #stdout: ReadableStream<Uint8Array>;
37
44
  /**
38
45
  * Legacy shell mixes stdout and stderr.
39
46
  */
40
- public get stdout() { return this._stdout; }
47
+ public get stdout() {
48
+ return this.#stdout;
49
+ }
41
50
 
42
- private _stderr: ReadableStream<Uint8Array>;
51
+ #stderr: ReadableStream<Uint8Array>;
43
52
  /**
44
53
  * `stderr` will always be empty.
45
54
  */
46
- public get stderr() { return this._stderr; }
55
+ public get stderr() {
56
+ return this.#stderr;
57
+ }
47
58
 
48
- private _exit: Promise<number>;
49
- public get exit() { return this._exit; }
59
+ #exit: Promise<number>;
60
+ public get exit() {
61
+ return this.#exit;
62
+ }
50
63
 
51
64
  public constructor(socket: AdbSocket) {
52
- this.socket = socket;
65
+ this.#socket = socket;
53
66
 
54
67
  // Link `stdout`, `stderr` and `stdin` together,
55
68
  // so closing any of them will close the others.
56
- this.duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
69
+ this.#duplex = new DuplexStreamFactory<Uint8Array, Uint8Array>({
57
70
  close: async () => {
58
- await this.socket.close();
71
+ await this.#socket.close();
59
72
  },
60
73
  });
61
74
 
62
- this._stdout = this.duplex.wrapReadable(this.socket.readable);
63
- this._stderr = this.duplex.wrapReadable(new ReadableStream());
64
- 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);
65
78
  }
66
79
 
67
80
  public resize() {
@@ -69,6 +82,6 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
69
82
  }
70
83
 
71
84
  public kill() {
72
- return this.duplex.close();
85
+ return this.#duplex.close();
73
86
  }
74
87
  }