@yume-chan/adb 0.0.19 → 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 (181) hide show
  1. package/CHANGELOG.json +33 -0
  2. package/CHANGELOG.md +15 -1
  3. package/README.md +220 -103
  4. package/esm/adb.d.ts +24 -44
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +14 -203
  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/power.d.ts +1 -1
  13. package/esm/commands/power.js +2 -2
  14. package/esm/commands/reverse.d.ts +17 -18
  15. package/esm/commands/reverse.d.ts.map +1 -1
  16. package/esm/commands/reverse.js +53 -48
  17. package/esm/commands/reverse.js.map +1 -1
  18. package/esm/commands/subprocess/protocols/none.d.ts +3 -8
  19. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -1
  20. package/esm/commands/subprocess/protocols/none.js +16 -16
  21. package/esm/commands/subprocess/protocols/none.js.map +1 -1
  22. package/esm/commands/subprocess/protocols/shell.d.ts +3 -9
  23. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  24. package/esm/commands/subprocess/protocols/shell.js +48 -42
  25. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  26. package/esm/commands/subprocess/protocols/types.d.ts +1 -2
  27. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -1
  28. package/esm/commands/sync/list.js +2 -2
  29. package/esm/commands/sync/push.d.ts +14 -3
  30. package/esm/commands/sync/push.d.ts.map +1 -1
  31. package/esm/commands/sync/push.js +8 -8
  32. package/esm/commands/sync/push.js.map +1 -1
  33. package/esm/commands/sync/request.js +1 -1
  34. package/esm/commands/sync/response.d.ts +3 -3
  35. package/esm/commands/sync/response.d.ts.map +1 -1
  36. package/esm/commands/sync/response.js +3 -3
  37. package/esm/commands/sync/response.js.map +1 -1
  38. package/esm/commands/sync/socket.d.ts +8 -13
  39. package/esm/commands/sync/socket.d.ts.map +1 -1
  40. package/esm/commands/sync/socket.js +32 -29
  41. package/esm/commands/sync/socket.js.map +1 -1
  42. package/esm/commands/sync/stat.js +2 -2
  43. package/esm/commands/sync/stat.js.map +1 -1
  44. package/esm/commands/sync/sync.d.ts +11 -18
  45. package/esm/commands/sync/sync.d.ts.map +1 -1
  46. package/esm/commands/sync/sync.js +19 -23
  47. package/esm/commands/sync/sync.js.map +1 -1
  48. package/esm/commands/tcpip.d.ts +2 -2
  49. package/esm/commands/tcpip.d.ts.map +1 -1
  50. package/esm/commands/tcpip.js +4 -2
  51. package/esm/commands/tcpip.js.map +1 -1
  52. package/esm/{auth.d.ts → daemon/auth.d.ts} +1 -2
  53. package/esm/daemon/auth.d.ts.map +1 -0
  54. package/esm/{auth.js → daemon/auth.js} +14 -14
  55. package/esm/daemon/auth.js.map +1 -0
  56. package/esm/daemon/connection.d.ts +11 -0
  57. package/esm/daemon/connection.d.ts.map +1 -0
  58. package/esm/daemon/connection.js +2 -0
  59. package/esm/daemon/connection.js.map +1 -0
  60. package/esm/{crypto.d.ts → daemon/crypto.d.ts} +5 -5
  61. package/esm/daemon/crypto.d.ts.map +1 -0
  62. package/esm/{crypto.js → daemon/crypto.js} +23 -27
  63. package/esm/daemon/crypto.js.map +1 -0
  64. package/esm/{backend.d.ts → daemon/device.d.ts} +2 -2
  65. package/esm/daemon/device.d.ts.map +1 -0
  66. package/esm/daemon/device.js +2 -0
  67. package/esm/daemon/device.js.map +1 -0
  68. package/esm/{socket → daemon}/dispatcher.d.ts +8 -26
  69. package/esm/daemon/dispatcher.d.ts.map +1 -0
  70. package/esm/{socket → daemon}/dispatcher.js +65 -63
  71. package/esm/daemon/dispatcher.js.map +1 -0
  72. package/esm/daemon/index.d.ts +8 -0
  73. package/esm/daemon/index.d.ts.map +1 -0
  74. package/esm/daemon/index.js +8 -0
  75. package/esm/daemon/index.js.map +1 -0
  76. package/esm/{packet.d.ts → daemon/packet.d.ts} +4 -4
  77. package/esm/daemon/packet.d.ts.map +1 -0
  78. package/esm/{packet.js → daemon/packet.js} +1 -1
  79. package/esm/daemon/packet.js.map +1 -0
  80. package/esm/{socket → daemon}/socket.d.ts +15 -17
  81. package/esm/daemon/socket.d.ts.map +1 -0
  82. package/esm/{socket → daemon}/socket.js +38 -35
  83. package/esm/daemon/socket.js.map +1 -0
  84. package/esm/daemon/transport.d.ts +45 -0
  85. package/esm/daemon/transport.d.ts.map +1 -0
  86. package/esm/daemon/transport.js +184 -0
  87. package/esm/daemon/transport.js.map +1 -0
  88. package/esm/index.d.ts +3 -5
  89. package/esm/index.d.ts.map +1 -1
  90. package/esm/index.js +3 -5
  91. package/esm/index.js.map +1 -1
  92. package/esm/server/client.d.ts +78 -0
  93. package/esm/server/client.d.ts.map +1 -0
  94. package/esm/server/client.js +329 -0
  95. package/esm/server/client.js.map +1 -0
  96. package/esm/server/index.d.ts +3 -0
  97. package/esm/server/index.d.ts.map +1 -0
  98. package/esm/server/index.js +3 -0
  99. package/esm/server/index.js.map +1 -0
  100. package/esm/server/transport.d.ts +19 -0
  101. package/esm/server/transport.d.ts.map +1 -0
  102. package/esm/server/transport.js +44 -0
  103. package/esm/server/transport.js.map +1 -0
  104. package/esm/utils/auto-reset-event.d.ts +1 -2
  105. package/esm/utils/auto-reset-event.d.ts.map +1 -1
  106. package/esm/utils/auto-reset-event.js +12 -12
  107. package/esm/utils/auto-reset-event.js.map +1 -1
  108. package/esm/utils/base64.js +5 -5
  109. package/esm/utils/base64.js.map +1 -1
  110. package/esm/utils/conditional-variable.d.ts +1 -2
  111. package/esm/utils/conditional-variable.d.ts.map +1 -1
  112. package/esm/utils/conditional-variable.js +10 -10
  113. package/esm/utils/conditional-variable.js.map +1 -1
  114. package/esm/utils/hex.d.ts +3 -0
  115. package/esm/utils/hex.d.ts.map +1 -0
  116. package/esm/utils/hex.js +55 -0
  117. package/esm/utils/hex.js.map +1 -0
  118. package/esm/utils/index.d.ts +2 -0
  119. package/esm/utils/index.d.ts.map +1 -1
  120. package/esm/utils/index.js +2 -0
  121. package/esm/utils/index.js.map +1 -1
  122. package/esm/utils/no-op.d.ts +7 -0
  123. package/esm/utils/no-op.d.ts.map +1 -0
  124. package/esm/utils/no-op.js +12 -0
  125. package/esm/utils/no-op.js.map +1 -0
  126. package/package.json +11 -10
  127. package/src/adb.ts +41 -252
  128. package/src/banner.ts +82 -0
  129. package/src/commands/power.ts +2 -2
  130. package/src/commands/reverse.ts +68 -73
  131. package/src/commands/subprocess/protocols/none.ts +17 -18
  132. package/src/commands/subprocess/protocols/shell.ts +54 -56
  133. package/src/commands/subprocess/protocols/types.ts +1 -2
  134. package/src/commands/sync/list.ts +2 -2
  135. package/src/commands/sync/push.ts +22 -10
  136. package/src/commands/sync/request.ts +1 -1
  137. package/src/commands/sync/response.ts +6 -6
  138. package/src/commands/sync/socket.ts +37 -35
  139. package/src/commands/sync/sync.ts +25 -28
  140. package/src/commands/tcpip.ts +6 -4
  141. package/src/{auth.ts → daemon/auth.ts} +17 -17
  142. package/src/{crypto.ts → daemon/crypto.ts} +25 -33
  143. package/src/{backend.ts → daemon/device.ts} +1 -1
  144. package/src/{socket → daemon}/dispatcher.ts +71 -78
  145. package/src/daemon/index.ts +7 -0
  146. package/src/{packet.ts → daemon/packet.ts} +5 -5
  147. package/src/{socket → daemon}/socket.ts +50 -44
  148. package/src/daemon/transport.ts +269 -0
  149. package/src/index.ts +3 -5
  150. package/src/server/client.ts +476 -0
  151. package/src/server/index.ts +2 -0
  152. package/src/server/transport.ts +77 -0
  153. package/src/utils/auto-reset-event.ts +12 -12
  154. package/src/utils/conditional-variable.ts +10 -10
  155. package/src/utils/hex.ts +58 -0
  156. package/src/utils/index.ts +2 -0
  157. package/src/utils/no-op.ts +12 -0
  158. package/tsconfig.build.json +12 -1
  159. package/tsconfig.build.tsbuildinfo +1 -1
  160. package/esm/auth.d.ts.map +0 -1
  161. package/esm/auth.js.map +0 -1
  162. package/esm/backend.d.ts.map +0 -1
  163. package/esm/backend.js +0 -2
  164. package/esm/backend.js.map +0 -1
  165. package/esm/commands/install.d.ts +0 -10
  166. package/esm/commands/install.d.ts.map +0 -1
  167. package/esm/commands/install.js +0 -29
  168. package/esm/commands/install.js.map +0 -1
  169. package/esm/crypto.d.ts.map +0 -1
  170. package/esm/crypto.js.map +0 -1
  171. package/esm/packet.d.ts.map +0 -1
  172. package/esm/packet.js.map +0 -1
  173. package/esm/socket/dispatcher.d.ts.map +0 -1
  174. package/esm/socket/dispatcher.js.map +0 -1
  175. package/esm/socket/index.d.ts +0 -3
  176. package/esm/socket/index.d.ts.map +0 -1
  177. package/esm/socket/index.js +0 -3
  178. package/esm/socket/index.js.map +0 -1
  179. package/esm/socket/socket.d.ts.map +0 -1
  180. package/esm/socket/socket.js.map +0 -1
  181. package/src/socket/index.ts +0 -2
@@ -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;
@@ -26,21 +22,20 @@ const AdbReverseStringResponse = new Struct()
26
22
  export class AdbReverseError extends Error {
27
23
  public 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 {
29
+ export class AdbReverseNotSupportedError extends AdbReverseError {
34
30
  public constructor() {
35
31
  super(
36
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,42 +47,30 @@ 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
60
  public 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
  }
@@ -111,23 +94,12 @@ 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
+ public async addExternal(deviceAddress: string, localAddress: string) {
131
103
  const stream = await this.sendRequest(
132
104
  `reverse:forward:${deviceAddress};${localAddress}`
133
105
  );
@@ -135,50 +107,73 @@ export class AdbReverseCommand extends AutoDisposable {
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;
130
+ }
160
131
 
161
- // 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
+ }
162
158
  }
163
159
 
164
160
  public async remove(deviceAddress: string): Promise<void> {
165
- await this.sendRequest(`reverse:killforward:${deviceAddress}`);
166
-
167
- if (this.deviceAddressToLocalAddress.has(deviceAddress)) {
168
- this.localAddressToHandler.delete(
169
- this.deviceAddressToLocalAddress.get(deviceAddress)!
170
- );
171
- this.deviceAddressToLocalAddress.delete(deviceAddress);
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
172
  public async removeAll(): Promise<void> {
178
- await this.sendRequest(`reverse:killforward-all`);
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,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
 
@@ -32,50 +31,50 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
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
39
  public get stdin() {
41
- return this.socket.writable;
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
47
  public get stdout() {
49
- return this._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
55
  public get stderr() {
57
- return this._stderr;
56
+ return this.#stderr;
58
57
  }
59
58
 
60
- private _exit: Promise<number>;
59
+ #exit: Promise<number>;
61
60
  public get exit() {
62
- return this._exit;
61
+ return this.#exit;
63
62
  }
64
63
 
65
64
  public constructor(socket: AdbSocket) {
66
- this.socket = socket;
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
80
  public resize() {
@@ -83,6 +82,6 @@ export class AdbSubprocessNoneProtocol implements AdbSubprocessProtocol {
83
82
  }
84
83
 
85
84
  public kill() {
86
- return this.duplex.close();
85
+ return this.#duplex.close();
87
86
  }
88
87
  }
@@ -10,16 +10,14 @@ import {
10
10
  ConsumableWritableStream,
11
11
  PushReadableStream,
12
12
  StructDeserializeStream,
13
- TransformStream,
14
13
  WritableStream,
15
14
  pipeFrom,
16
15
  } from "@yume-chan/stream-extra";
17
16
  import type { StructValueType } from "@yume-chan/struct";
18
17
  import Struct, { placeholder } from "@yume-chan/struct";
19
18
 
20
- import type { Adb } from "../../../adb.js";
19
+ import type { Adb, AdbSocket } from "../../../adb.js";
21
20
  import { AdbFeature } from "../../../features.js";
22
- import type { AdbSocket } from "../../../socket/index.js";
23
21
  import { encodeUtf8 } from "../../../utils/index.js";
24
22
 
25
23
  import type { AdbSubprocessProtocol } from "./types.js";
@@ -62,21 +60,6 @@ class StdinSerializeStream extends ConsumableTransformStream<
62
60
  }
63
61
  }
64
62
 
65
- class StdoutDeserializeStream extends TransformStream<
66
- AdbShellProtocolPacket,
67
- Uint8Array
68
- > {
69
- constructor(type: AdbShellProtocolId.Stdout | AdbShellProtocolId.Stderr) {
70
- super({
71
- transform(chunk, controller) {
72
- if (chunk.id === type) {
73
- controller.enqueue(chunk.data);
74
- }
75
- },
76
- });
77
- }
78
- }
79
-
80
63
  class MultiplexStream<T> {
81
64
  private _readable: PushReadableStream<T>;
82
65
  private _readableController!: PushReadableStreamController<T>;
@@ -142,67 +125,82 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
142
125
  );
143
126
  }
144
127
 
145
- private readonly _socket: AdbSocket;
146
- private _socketWriter: WritableStreamDefaultWriter<
128
+ readonly #socket: AdbSocket;
129
+ #socketWriter: WritableStreamDefaultWriter<
147
130
  Consumable<AdbShellProtocolPacketInit>
148
131
  >;
149
132
 
150
- private _stdin: WritableStream<Consumable<Uint8Array>>;
133
+ #stdin: WritableStream<Consumable<Uint8Array>>;
151
134
  public get stdin() {
152
- return this._stdin;
135
+ return this.#stdin;
153
136
  }
154
137
 
155
- private _stdout: ReadableStream<Uint8Array>;
138
+ #stdout: ReadableStream<Uint8Array>;
156
139
  public get stdout() {
157
- return this._stdout;
140
+ return this.#stdout;
158
141
  }
159
142
 
160
- private _stderr: ReadableStream<Uint8Array>;
143
+ #stderr: ReadableStream<Uint8Array>;
161
144
  public get stderr() {
162
- return this._stderr;
145
+ return this.#stderr;
163
146
  }
164
147
 
165
- private readonly _exit = new PromiseResolver<number>();
148
+ readonly #exit = new PromiseResolver<number>();
166
149
  public get exit() {
167
- return this._exit.promise;
150
+ return this.#exit.promise;
168
151
  }
169
152
 
170
153
  public constructor(socket: AdbSocket) {
171
- this._socket = socket;
154
+ this.#socket = socket;
172
155
 
173
156
  // Check this image to help you understand the stream graph
174
157
  // cspell: disable-next-line
175
158
  // https://www.plantuml.com/plantuml/png/bL91QiCm4Bpx5SAdv90lb1JISmiw5XzaQKf5PIkiLZIqzEyLSg8ks13gYtOykpFhiOw93N6UGjVDqK7rZsxKqNw0U_NTgVAy4empOy2mm4_olC0VEVEE47GUpnGjKdgXoD76q4GIEpyFhOwP_m28hW0NNzxNUig1_JdW0bA7muFIJDco1daJ_1SAX9bgvoPJPyIkSekhNYctvIGXrCH6tIsPL5fs-s6J5yc9BpWXhKtNdF2LgVYPGM_6GlMwfhWUsIt4lbScANrwlgVVUifPSVi__t44qStnwPvZwobdSmHHlL57p2vFuHS0
176
159
 
177
- // TODO: AdbShellSubprocessProtocol: Optimize stream graph
160
+ let stdoutController!: PushReadableStreamController<Uint8Array>;
161
+ let stderrController!: PushReadableStreamController<Uint8Array>;
162
+ this.#stdout = new PushReadableStream<Uint8Array>((controller) => {
163
+ stdoutController = controller;
164
+ });
165
+ this.#stderr = new PushReadableStream<Uint8Array>((controller) => {
166
+ stderrController = controller;
167
+ });
178
168
 
179
- const [stdout, stderr] = socket.readable
169
+ socket.readable
180
170
  .pipeThrough(new StructDeserializeStream(AdbShellProtocolPacket))
181
- .pipeThrough(
182
- new TransformStream<
183
- AdbShellProtocolPacket,
184
- AdbShellProtocolPacket
185
- >({
186
- transform: (chunk, controller) => {
187
- if (chunk.id === AdbShellProtocolId.Exit) {
188
- this._exit.resolve(new Uint8Array(chunk.data)[0]!);
189
- // We can let `StdoutDeserializeStream` to process `AdbShellProtocolId.Exit`,
190
- // but since we need this `TransformStream` to capture the exit code anyway,
191
- // terminating child streams here is killing two birds with one stone.
192
- controller.terminate();
193
- return;
171
+ .pipeTo(
172
+ new WritableStream<AdbShellProtocolPacket>({
173
+ write: async (chunk) => {
174
+ switch (chunk.id) {
175
+ case AdbShellProtocolId.Exit:
176
+ this.#exit.resolve(chunk.data[0]!);
177
+ break;
178
+ case AdbShellProtocolId.Stdout:
179
+ await stdoutController.enqueue(chunk.data);
180
+ break;
181
+ case AdbShellProtocolId.Stderr:
182
+ await stderrController.enqueue(chunk.data);
183
+ break;
194
184
  }
195
- controller.enqueue(chunk);
196
185
  },
197
186
  })
198
187
  )
199
- .tee();
200
- this._stdout = stdout.pipeThrough(
201
- new StdoutDeserializeStream(AdbShellProtocolId.Stdout)
202
- );
203
- this._stderr = stderr.pipeThrough(
204
- new StdoutDeserializeStream(AdbShellProtocolId.Stderr)
205
- );
188
+ .then(
189
+ () => {
190
+ stdoutController.close();
191
+ stderrController.close();
192
+ // If `#exit` has already resolved, this will be a no-op
193
+ this.#exit.reject(
194
+ new Error("Socket ended without exit message")
195
+ );
196
+ },
197
+ (e) => {
198
+ stdoutController.error(e);
199
+ stderrController.error(e);
200
+ // If `#exit` has already resolved, this will be a no-op
201
+ this.#exit.reject(e);
202
+ }
203
+ );
206
204
 
207
205
  const multiplexer = new MultiplexStream<
208
206
  Consumable<AdbShellProtocolPacketInit>
@@ -219,16 +217,16 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
219
217
  )
220
218
  .pipeTo(socket.writable);
221
219
 
222
- this._stdin = pipeFrom(
220
+ this.#stdin = pipeFrom(
223
221
  multiplexer.createWriteable(),
224
222
  new StdinSerializeStream()
225
223
  );
226
224
 
227
- this._socketWriter = multiplexer.createWriteable().getWriter();
225
+ this.#socketWriter = multiplexer.createWriteable().getWriter();
228
226
  }
229
227
 
230
228
  public async resize(rows: number, cols: number) {
231
- await ConsumableWritableStream.write(this._socketWriter, {
229
+ await ConsumableWritableStream.write(this.#socketWriter, {
232
230
  id: AdbShellProtocolId.WindowSizeChange,
233
231
  data: encodeUtf8(
234
232
  // The "correct" format is `${rows}x${cols},${x_pixels}x${y_pixels}`
@@ -240,6 +238,6 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
240
238
  }
241
239
 
242
240
  public kill() {
243
- return this._socket.close();
241
+ return this.#socket.close();
244
242
  }
245
243
  }
@@ -5,8 +5,7 @@ import type {
5
5
  } from "@yume-chan/stream-extra";
6
6
  import type { ValueOrPromise } from "@yume-chan/struct";
7
7
 
8
- import type { Adb } from "../../../adb.js";
9
- import type { AdbSocket } from "../../../socket/index.js";
8
+ import type { Adb, AdbSocket } from "../../../adb.js";
10
9
 
11
10
  export interface AdbSubprocessProtocol {
12
11
  /**
@@ -11,7 +11,7 @@ export interface AdbSyncEntry extends AdbSyncStat {
11
11
  }
12
12
 
13
13
  export const AdbSyncEntryResponse = new Struct({ littleEndian: true })
14
- .fields(AdbSyncLstatResponse)
14
+ .concat(AdbSyncLstatResponse)
15
15
  .uint32("nameLength")
16
16
  .string("name", { lengthField: "nameLength" })
17
17
  .extra({ id: AdbSyncResponseId.Entry as const });
@@ -20,7 +20,7 @@ export type AdbSyncEntryResponse =
20
20
  (typeof AdbSyncEntryResponse)["TDeserializeResult"];
21
21
 
22
22
  export const AdbSyncEntry2Response = new Struct({ littleEndian: true })
23
- .fields(AdbSyncStatResponse)
23
+ .concat(AdbSyncStatResponse)
24
24
  .uint32("nameLength")
25
25
  .string("name", { lengthField: "nameLength" })
26
26
  .extra({ id: AdbSyncResponseId.Entry2 as const });
@@ -6,22 +6,21 @@ import {
6
6
  } from "@yume-chan/stream-extra";
7
7
  import Struct, { placeholder } from "@yume-chan/struct";
8
8
 
9
+ import { NOOP } from "../../utils/index.js";
10
+
9
11
  import { AdbSyncRequestId, adbSyncWriteRequest } from "./request.js";
10
12
  import { AdbSyncResponseId, adbSyncReadResponse } from "./response.js";
11
13
  import type { AdbSyncSocket, AdbSyncSocketLocked } from "./socket.js";
12
14
  import { LinuxFileType } from "./stat.js";
13
15
 
14
- const NOOP = () => {
15
- // no-op
16
- };
17
-
18
16
  export const ADB_SYNC_MAX_PACKET_SIZE = 64 * 1024;
19
17
 
20
18
  export interface AdbSyncPushV1Options {
21
19
  socket: AdbSyncSocket;
22
20
  filename: string;
23
21
  file: ReadableStream<Consumable<Uint8Array>>;
24
- mode?: number;
22
+ type?: LinuxFileType;
23
+ permission?: number;
25
24
  mtime?: number;
26
25
  packetSize?: number;
27
26
  }
@@ -30,7 +29,7 @@ export const AdbSyncOkResponse = new Struct({ littleEndian: true }).uint32(
30
29
  "unused"
31
30
  );
32
31
 
33
- async function pipeFile(
32
+ async function pipeFileData(
34
33
  locked: AdbSyncSocketLocked,
35
34
  file: ReadableStream<Consumable<Uint8Array>>,
36
35
  packetSize: number,
@@ -71,15 +70,17 @@ export async function adbSyncPushV1({
71
70
  socket,
72
71
  filename,
73
72
  file,
74
- mode = (LinuxFileType.File << 12) | 0o666,
73
+ type = LinuxFileType.File,
74
+ permission = 0o666,
75
75
  mtime = (Date.now() / 1000) | 0,
76
76
  packetSize = ADB_SYNC_MAX_PACKET_SIZE,
77
77
  }: AdbSyncPushV1Options) {
78
78
  const locked = await socket.lock();
79
79
  try {
80
+ const mode = (type << 12) | permission;
80
81
  const pathAndMode = `${filename},${mode.toString()}`;
81
82
  await adbSyncWriteRequest(locked, AdbSyncRequestId.Send, pathAndMode);
82
- await pipeFile(locked, file, packetSize, mtime);
83
+ await pipeFileData(locked, file, packetSize, mtime);
83
84
  } finally {
84
85
  locked.release();
85
86
  }
@@ -103,6 +104,12 @@ export enum AdbSyncSendV2Flags {
103
104
  }
104
105
 
105
106
  export interface AdbSyncPushV2Options extends AdbSyncPushV1Options {
107
+ /**
108
+ * Don't write the file to disk. Requires the `sendrecv_v2` feature.
109
+ *
110
+ * It was used during ADB development to benchmark the performance of
111
+ * compression algorithms.
112
+ */
106
113
  dryRun?: boolean;
107
114
  }
108
115
 
@@ -115,7 +122,8 @@ export async function adbSyncPushV2({
115
122
  socket,
116
123
  filename,
117
124
  file,
118
- mode = (LinuxFileType.File << 12) | 0o666,
125
+ type = LinuxFileType.File,
126
+ permission = 0o666,
119
127
  mtime = (Date.now() / 1000) | 0,
120
128
  packetSize = ADB_SYNC_MAX_PACKET_SIZE,
121
129
  dryRun = false,
@@ -124,6 +132,7 @@ export async function adbSyncPushV2({
124
132
  try {
125
133
  await adbSyncWriteRequest(locked, AdbSyncRequestId.SendV2, filename);
126
134
 
135
+ const mode = (type << 12) | permission;
127
136
  let flags: AdbSyncSendV2Flags = AdbSyncSendV2Flags.None;
128
137
  if (dryRun) {
129
138
  flags |= AdbSyncSendV2Flags.DryRun;
@@ -136,13 +145,16 @@ export async function adbSyncPushV2({
136
145
  })
137
146
  );
138
147
 
139
- await pipeFile(locked, file, packetSize, mtime);
148
+ await pipeFileData(locked, file, packetSize, mtime);
140
149
  } finally {
141
150
  locked.release();
142
151
  }
143
152
  }
144
153
 
145
154
  export interface AdbSyncPushOptions extends AdbSyncPushV2Options {
155
+ /**
156
+ * Whether to use the v2 protocol. Requires the `sendrecv_v2` feature.
157
+ */
146
158
  v2: boolean;
147
159
  }
148
160
 
@@ -20,7 +20,7 @@ export const AdbSyncNumberRequest = new Struct({ littleEndian: true })
20
20
  .uint32("arg");
21
21
 
22
22
  export const AdbSyncDataRequest = new Struct({ littleEndian: true })
23
- .fields(AdbSyncNumberRequest)
23
+ .concat(AdbSyncNumberRequest)
24
24
  .uint8Array("data", { lengthField: "arg" });
25
25
 
26
26
  export interface AdbSyncWritable {