@yume-chan/adb 0.0.12 → 0.0.15

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 (145) hide show
  1. package/CHANGELOG.json +54 -0
  2. package/CHANGELOG.md +28 -1
  3. package/README.md +24 -17
  4. package/esm/adb.d.ts +10 -9
  5. package/esm/adb.d.ts.map +1 -1
  6. package/esm/adb.js +88 -67
  7. package/esm/adb.js.map +1 -1
  8. package/esm/auth.d.ts +6 -6
  9. package/esm/auth.d.ts.map +1 -1
  10. package/esm/auth.js +8 -6
  11. package/esm/auth.js.map +1 -1
  12. package/esm/backend.d.ts +2 -2
  13. package/esm/commands/install.d.ts.map +1 -1
  14. package/esm/commands/install.js +5 -7
  15. package/esm/commands/install.js.map +1 -1
  16. package/esm/commands/reverse.d.ts +9 -12
  17. package/esm/commands/reverse.d.ts.map +1 -1
  18. package/esm/commands/reverse.js +44 -36
  19. package/esm/commands/reverse.js.map +1 -1
  20. package/esm/commands/subprocess/index.d.ts +19 -20
  21. package/esm/commands/subprocess/index.d.ts.map +1 -1
  22. package/esm/commands/subprocess/index.js +36 -31
  23. package/esm/commands/subprocess/index.js.map +1 -1
  24. package/esm/commands/subprocess/legacy.d.ts +14 -7
  25. package/esm/commands/subprocess/legacy.d.ts.map +1 -1
  26. package/esm/commands/subprocess/legacy.js +14 -5
  27. package/esm/commands/subprocess/legacy.js.map +1 -1
  28. package/esm/commands/subprocess/protocol.d.ts +3 -2
  29. package/esm/commands/subprocess/protocol.d.ts.map +1 -1
  30. package/esm/commands/subprocess/protocol.js +6 -4
  31. package/esm/commands/subprocess/protocol.js.map +1 -1
  32. package/esm/commands/subprocess/protocols/index.d.ts +4 -0
  33. package/esm/commands/subprocess/protocols/index.d.ts.map +1 -0
  34. package/esm/commands/subprocess/protocols/index.js +4 -0
  35. package/esm/commands/subprocess/protocols/index.js.map +1 -0
  36. package/esm/commands/subprocess/protocols/none.d.ts +35 -0
  37. package/esm/commands/subprocess/protocols/none.d.ts.map +1 -0
  38. package/esm/commands/subprocess/protocols/none.js +53 -0
  39. package/esm/commands/subprocess/protocols/none.js.map +1 -0
  40. package/esm/commands/subprocess/protocols/shell.d.ts +39 -0
  41. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -0
  42. package/esm/commands/subprocess/protocols/shell.js +155 -0
  43. package/esm/commands/subprocess/protocols/shell.js.map +1 -0
  44. package/esm/commands/subprocess/protocols/types.d.ts +50 -0
  45. package/esm/commands/subprocess/protocols/types.d.ts.map +1 -0
  46. package/esm/commands/subprocess/protocols/types.js +2 -0
  47. package/esm/commands/subprocess/protocols/types.js.map +1 -0
  48. package/esm/commands/subprocess/types.d.ts +9 -6
  49. package/esm/commands/subprocess/types.d.ts.map +1 -1
  50. package/esm/commands/sync/list.d.ts +25 -1
  51. package/esm/commands/sync/list.d.ts.map +1 -1
  52. package/esm/commands/sync/list.js +40 -5
  53. package/esm/commands/sync/list.js.map +1 -1
  54. package/esm/commands/sync/pull.d.ts.map +1 -1
  55. package/esm/commands/sync/pull.js +6 -3
  56. package/esm/commands/sync/pull.js.map +1 -1
  57. package/esm/commands/sync/request.d.ts +1 -0
  58. package/esm/commands/sync/request.d.ts.map +1 -1
  59. package/esm/commands/sync/request.js +1 -0
  60. package/esm/commands/sync/request.js.map +1 -1
  61. package/esm/commands/sync/response.d.ts +2 -1
  62. package/esm/commands/sync/response.d.ts.map +1 -1
  63. package/esm/commands/sync/response.js +2 -1
  64. package/esm/commands/sync/response.js.map +1 -1
  65. package/esm/commands/sync/stat.d.ts +13 -1
  66. package/esm/commands/sync/stat.d.ts.map +1 -1
  67. package/esm/commands/sync/stat.js +22 -9
  68. package/esm/commands/sync/stat.js.map +1 -1
  69. package/esm/commands/sync/sync.d.ts +13 -35
  70. package/esm/commands/sync/sync.d.ts.map +1 -1
  71. package/esm/commands/sync/sync.js +40 -10
  72. package/esm/commands/sync/sync.js.map +1 -1
  73. package/esm/crypto.d.ts +23 -10
  74. package/esm/crypto.d.ts.map +1 -1
  75. package/esm/crypto.js +64 -46
  76. package/esm/crypto.js.map +1 -1
  77. package/esm/features.d.ts +4 -2
  78. package/esm/features.d.ts.map +1 -1
  79. package/esm/features.js +6 -2
  80. package/esm/features.js.map +1 -1
  81. package/esm/packet.d.ts +21 -2
  82. package/esm/packet.d.ts.map +1 -1
  83. package/esm/packet.js +2 -2
  84. package/esm/packet.js.map +1 -1
  85. package/esm/socket/dispatcher.d.ts +35 -23
  86. package/esm/socket/dispatcher.d.ts.map +1 -1
  87. package/esm/socket/dispatcher.js +97 -72
  88. package/esm/socket/dispatcher.js.map +1 -1
  89. package/esm/socket/socket.d.ts +29 -8
  90. package/esm/socket/socket.d.ts.map +1 -1
  91. package/esm/socket/socket.js +43 -29
  92. package/esm/socket/socket.js.map +1 -1
  93. package/esm/stream/buffered.d.ts +1 -1
  94. package/esm/stream/buffered.d.ts.map +1 -1
  95. package/esm/stream/buffered.js +2 -2
  96. package/esm/stream/buffered.js.map +1 -1
  97. package/esm/stream/detect.d.ts +1 -17
  98. package/esm/stream/detect.d.ts.map +1 -1
  99. package/esm/stream/detect.js +4 -5
  100. package/esm/stream/detect.js.map +1 -1
  101. package/esm/stream/detect.polyfill.d.ts +1 -1
  102. package/esm/stream/detect.polyfill.d.ts.map +1 -1
  103. package/esm/stream/detect.polyfill.js +4 -1
  104. package/esm/stream/detect.polyfill.js.map +1 -1
  105. package/esm/stream/transform.d.ts +39 -33
  106. package/esm/stream/transform.d.ts.map +1 -1
  107. package/esm/stream/transform.js +121 -100
  108. package/esm/stream/transform.js.map +1 -1
  109. package/esm/utils/base64.js +5 -5
  110. package/esm/utils/base64.js.map +1 -1
  111. package/esm/utils/encoding.d.ts.map +1 -1
  112. package/esm/utils/encoding.js.map +1 -1
  113. package/esm/utils/index.d.ts +1 -1
  114. package/esm/utils/index.d.ts.map +1 -1
  115. package/esm/utils/index.js +1 -1
  116. package/esm/utils/index.js.map +1 -1
  117. package/package.json +7 -7
  118. package/src/adb.ts +112 -86
  119. package/src/auth.ts +21 -18
  120. package/src/backend.ts +2 -2
  121. package/src/commands/install.ts +7 -9
  122. package/src/commands/reverse.ts +48 -44
  123. package/src/commands/subprocess/index.ts +57 -42
  124. package/src/commands/subprocess/protocols/index.ts +3 -0
  125. package/src/commands/subprocess/{legacy.ts → protocols/none.ts} +20 -10
  126. package/src/commands/subprocess/{protocol.ts → protocols/shell.ts} +22 -17
  127. package/src/commands/subprocess/{types.ts → protocols/types.ts} +13 -9
  128. package/src/commands/sync/list.ts +51 -5
  129. package/src/commands/sync/pull.ts +6 -3
  130. package/src/commands/sync/request.ts +1 -0
  131. package/src/commands/sync/response.ts +8 -2
  132. package/src/commands/sync/stat.ts +36 -9
  133. package/src/commands/sync/sync.ts +56 -20
  134. package/src/crypto.ts +72 -56
  135. package/src/features.ts +6 -2
  136. package/src/packet.ts +17 -6
  137. package/src/socket/dispatcher.ts +139 -91
  138. package/src/socket/socket.ts +76 -61
  139. package/src/stream/buffered.ts +2 -2
  140. package/src/stream/detect.polyfill.ts +5 -2
  141. package/src/stream/detect.ts +4 -24
  142. package/src/stream/transform.ts +144 -124
  143. package/src/utils/index.ts +1 -1
  144. package/tsconfig.build.tsbuildinfo +1 -1
  145. package/src/utils/encoding.ts +0 -13
package/src/adb.ts CHANGED
@@ -1,9 +1,11 @@
1
+ // cspell: ignore libusb
2
+
1
3
  import { PromiseResolver } from '@yume-chan/async';
2
- import { AdbAuthenticationHandler, AdbDefaultAuthenticators, type AdbCredentialStore } from './auth.js';
4
+ import { AdbAuthenticationProcessor, ADB_DEFAULT_AUTHENTICATORS, type AdbCredentialStore } from './auth.js';
3
5
  import { AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install, type AdbFrameBuffer } from './commands/index.js';
4
6
  import { AdbFeatures } from './features.js';
5
- import { AdbCommand, AdbPacket, calculateChecksum, type AdbPacketCore, type AdbPacketInit } from './packet.js';
6
- import { AdbPacketDispatcher, AdbSocket } from './socket/index.js';
7
+ import { AdbCommand, calculateChecksum, type AdbPacketData, type AdbPacketInit } from './packet.js';
8
+ import { AdbIncomingSocketHandler, AdbPacketDispatcher, type AdbSocket, type Closeable } from './socket/index.js';
7
9
  import { AbortController, DecodeUtf8Stream, GatherStringStream, WritableStream, type ReadableWritablePair } from "./stream/index.js";
8
10
  import { decodeUtf8, encodeUtf8 } from "./utils/index.js";
9
11
 
@@ -16,46 +18,30 @@ export enum AdbPropKey {
16
18
 
17
19
  export const VERSION_OMIT_CHECKSUM = 0x01000001;
18
20
 
19
- export class Adb {
21
+ export class Adb implements Closeable {
20
22
  /**
21
23
  * It's possible to call `authenticate` multiple times on a single connection,
22
24
  * every time the device receives a `CNXN` packet, it resets its internal state,
23
25
  * and starts a new authentication process.
24
26
  */
25
27
  public static async authenticate(
26
- connection: ReadableWritablePair<AdbPacketCore, AdbPacketCore>,
28
+ connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
27
29
  credentialStore: AdbCredentialStore,
28
- authenticators = AdbDefaultAuthenticators,
30
+ authenticators = ADB_DEFAULT_AUTHENTICATORS,
29
31
  ): Promise<Adb> {
32
+ // Initially, set to highest-supported version and payload size.
30
33
  let version = 0x01000001;
31
34
  let maxPayloadSize = 0x100000;
32
35
 
33
- const features = [
34
- 'shell_v2',
35
- 'cmd',
36
- AdbFeatures.StatV2,
37
- 'ls_v2',
38
- 'fixed_push_mkdir',
39
- 'apex',
40
- 'abb',
41
- 'fixed_push_symlink_timestamp',
42
- 'abb_exec',
43
- 'remount_shell',
44
- 'track_app',
45
- 'sendrecv_v2',
46
- 'sendrecv_v2_brotli',
47
- 'sendrecv_v2_lz4',
48
- 'sendrecv_v2_zstd',
49
- 'sendrecv_v2_dry_run_send',
50
- ].join(',');
51
-
52
36
  const resolver = new PromiseResolver<string>();
53
- const authHandler = new AdbAuthenticationHandler(authenticators, credentialStore);
37
+ const authProcessor = new AdbAuthenticationProcessor(authenticators, credentialStore);
54
38
 
39
+ // Here is similar to `AdbPacketDispatcher`,
40
+ // But the received packet types and send packet processing are different.
55
41
  const abortController = new AbortController();
56
42
  const pipe = connection.readable
57
43
  .pipeTo(new WritableStream({
58
- async write(packet: AdbPacket) {
44
+ async write(packet) {
59
45
  switch (packet.command) {
60
46
  case AdbCommand.Connect:
61
47
  version = Math.min(version, packet.arg0);
@@ -63,64 +49,89 @@ export class Adb {
63
49
  resolver.resolve(decodeUtf8(packet.payload));
64
50
  break;
65
51
  case AdbCommand.Auth:
66
- const response = await authHandler.handle(packet);
52
+ const response = await authProcessor.process(packet);
67
53
  await sendPacket(response);
68
54
  break;
69
- case AdbCommand.Close:
70
- // Last connection was interrupted
71
- // Ignore this packet, device will recover
72
- break;
73
55
  default:
74
- throw new Error('Device not in correct state. Reconnect your device and try again');
56
+ // Maybe the previous ADB client exited without reading all packets,
57
+ // so they are still waiting in OS internal buffer.
58
+ // Just ignore them.
59
+ // Because a `Connect` packet will reset the device,
60
+ // Eventually there will be `Connect` and `Auth` response packets.
61
+ break;
75
62
  }
76
63
  }
77
64
  }), {
78
65
  preventCancel: true,
79
66
  signal: abortController.signal,
80
67
  })
81
- .catch((e) => { resolver.reject(e); });
68
+ .catch((e) => {
69
+ resolver.reject(e);
70
+ });
82
71
 
83
72
  const writer = connection.writable.getWriter();
84
- async function sendPacket(init: AdbPacketCore) {
73
+ async function sendPacket(init: AdbPacketData) {
85
74
  // Always send checksum in auth steps
86
- // Because we don't know if the device will ignore it yet.
75
+ // Because we don't know if the device needs it or not.
87
76
  await writer.write(calculateChecksum(init));
88
77
  }
89
78
 
90
- await sendPacket({
91
- command: AdbCommand.Connect,
92
- arg0: version,
93
- arg1: maxPayloadSize,
94
- // The terminating `;` is required in formal definition
95
- // But ADB daemon (all versions) can still work without it
96
- payload: encodeUtf8(`host::features=${features};`),
97
- });
98
-
79
+ let banner: string;
99
80
  try {
100
- const banner = await resolver.promise;
101
-
102
- // Stop piping before creating Adb object
103
- // Because AdbPacketDispatcher will try to lock the streams when initializing
104
- abortController.abort();
105
- await pipe;
106
-
107
- writer.releaseLock();
108
-
109
- return new Adb(
110
- connection,
111
- version,
112
- maxPayloadSize,
113
- banner,
114
- );
81
+ // https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
82
+ // There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
83
+ const features = [
84
+ AdbFeatures.ShellV2,
85
+ AdbFeatures.Cmd,
86
+ AdbFeatures.StatV2,
87
+ AdbFeatures.ListV2,
88
+ AdbFeatures.FixedPushMkdir,
89
+ 'apex',
90
+ 'abb',
91
+ // only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
92
+ // No special handling required.
93
+ 'fixed_push_symlink_timestamp',
94
+ 'abb_exec',
95
+ 'remount_shell',
96
+ 'track_app',
97
+ 'sendrecv_v2',
98
+ 'sendrecv_v2_brotli',
99
+ 'sendrecv_v2_lz4',
100
+ 'sendrecv_v2_zstd',
101
+ 'sendrecv_v2_dry_run_send',
102
+ ].join(',');
103
+
104
+ await sendPacket({
105
+ command: AdbCommand.Connect,
106
+ arg0: version,
107
+ arg1: maxPayloadSize,
108
+ // The terminating `;` is required in formal definition
109
+ // But ADB daemon (all versions) can still work without it
110
+ payload: encodeUtf8(`host::features=${features};`),
111
+ });
112
+
113
+ banner = await resolver.promise;
115
114
  } finally {
115
+ // When failed, release locks on `connection` so the caller can try again.
116
+ // When success, also release locks so `AdbPacketDispatcher` can use them.
116
117
  abortController.abort();
117
118
  writer.releaseLock();
119
+
120
+ // Wait until pipe stops (`ReadableStream` lock released)
121
+ await pipe;
118
122
  }
123
+
124
+ return new Adb(
125
+ connection,
126
+ version,
127
+ maxPayloadSize,
128
+ banner,
129
+ );
119
130
  }
120
131
 
121
- private readonly packetDispatcher: AdbPacketDispatcher;
132
+ private readonly dispatcher: AdbPacketDispatcher;
122
133
 
123
- public get disconnected() { return this.packetDispatcher.disconnected; }
134
+ public get disconnected() { return this.dispatcher.disconnected; }
124
135
 
125
136
  private _protocolVersion: number | undefined;
126
137
  public get protocolVersion() { return this._protocolVersion; }
@@ -143,26 +154,37 @@ export class Adb {
143
154
  public readonly tcpip: AdbTcpIpCommand;
144
155
 
145
156
  public constructor(
146
- connection: ReadableWritablePair<AdbPacketCore, AdbPacketInit>,
157
+ connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
147
158
  version: number,
148
159
  maxPayloadSize: number,
149
160
  banner: string,
150
161
  ) {
151
162
  this.parseBanner(banner);
152
- this.packetDispatcher = new AdbPacketDispatcher(connection);
153
163
 
154
- this._protocolVersion = version;
164
+ let calculateChecksum: boolean;
165
+ let appendNullToServiceString: boolean;
155
166
  if (version >= VERSION_OMIT_CHECKSUM) {
156
- this.packetDispatcher.calculateChecksum = false;
157
- // Android prior to 9.0.0 uses char* to parse service string
158
- // thus requires an extra null character
159
- this.packetDispatcher.appendNullToServiceString = false;
167
+ calculateChecksum = false;
168
+ appendNullToServiceString = false;
169
+ } else {
170
+ calculateChecksum = true;
171
+ appendNullToServiceString = true;
160
172
  }
161
- this.packetDispatcher.maxPayloadSize = maxPayloadSize;
173
+
174
+ this.dispatcher = new AdbPacketDispatcher(
175
+ connection,
176
+ {
177
+ calculateChecksum,
178
+ appendNullToServiceString,
179
+ maxPayloadSize,
180
+ }
181
+ );
182
+
183
+ this._protocolVersion = version;
162
184
 
163
185
  this.subprocess = new AdbSubprocess(this);
164
186
  this.power = new AdbPower(this);
165
- this.reverse = new AdbReverseCommand(this.packetDispatcher);
187
+ this.reverse = new AdbReverseCommand(this);
166
188
  this.tcpip = new AdbTcpIpCommand(this);
167
189
  }
168
190
 
@@ -201,6 +223,23 @@ export class Adb {
201
223
  }
202
224
  }
203
225
 
226
+ public addIncomingSocketHandler(handler: AdbIncomingSocketHandler) {
227
+ return this.dispatcher.addIncomingSocketHandler(handler);
228
+ }
229
+
230
+ public async createSocket(service: string): Promise<AdbSocket> {
231
+ return this.dispatcher.createSocket(service);
232
+ }
233
+
234
+ public async createSocketAndWait(service: string): Promise<string> {
235
+ const socket = await this.createSocket(service);
236
+ const gatherStream = new GatherStringStream();
237
+ await socket.readable
238
+ .pipeThrough(new DecodeUtf8Stream())
239
+ .pipeTo(gatherStream);
240
+ return gatherStream.result;
241
+ }
242
+
204
243
  public async getProp(key: string): Promise<string> {
205
244
  const stdout = await this.subprocess.spawnAndWaitLegacy(
206
245
  ['getprop', key]
@@ -228,20 +267,7 @@ export class Adb {
228
267
  return framebuffer(this);
229
268
  }
230
269
 
231
- public async createSocket(service: string): Promise<AdbSocket> {
232
- return this.packetDispatcher.createSocket(service);
233
- }
234
-
235
- public async createSocketAndWait(service: string): Promise<string> {
236
- const socket = await this.createSocket(service);
237
- const gatherStream = new GatherStringStream();
238
- await socket.readable
239
- .pipeThrough(new DecodeUtf8Stream())
240
- .pipeTo(gatherStream.writable);
241
- return gatherStream.result;
242
- }
243
-
244
- public async dispose(): Promise<void> {
245
- this.packetDispatcher.dispose();
270
+ public async close(): Promise<void> {
271
+ await this.dispatcher.close();
246
272
  }
247
273
  }
package/src/auth.ts CHANGED
@@ -2,7 +2,7 @@ import { PromiseResolver } from '@yume-chan/async';
2
2
  import type { Disposable } from '@yume-chan/event';
3
3
  import type { ValueOrPromise } from '@yume-chan/struct';
4
4
  import { calculatePublicKey, calculatePublicKeyLength, sign } from './crypto.js';
5
- import { AdbCommand, AdbPacket, type AdbPacketCore } from './packet.js';
5
+ import { AdbCommand, type AdbPacketData } from './packet.js';
6
6
  import { calculateBase64EncodedLength, encodeBase64 } from './utils/index.js';
7
7
 
8
8
  export type AdbKeyIterable = Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
@@ -43,14 +43,14 @@ export interface AdbAuthenticator {
43
43
  */
44
44
  (
45
45
  credentialStore: AdbCredentialStore,
46
- getNextRequest: () => Promise<AdbPacket>
47
- ): AsyncIterable<AdbPacketCore>;
46
+ getNextRequest: () => Promise<AdbPacketData>
47
+ ): AsyncIterable<AdbPacketData>;
48
48
  }
49
49
 
50
50
  export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
51
51
  credentialStore: AdbCredentialStore,
52
- getNextRequest: () => Promise<AdbPacket>,
53
- ): AsyncIterable<AdbPacketCore> {
52
+ getNextRequest: () => Promise<AdbPacketData>,
53
+ ): AsyncIterable<AdbPacketData> {
54
54
  for await (const key of credentialStore.iterateKeys()) {
55
55
  const packet = await getNextRequest();
56
56
 
@@ -70,8 +70,8 @@ export const AdbSignatureAuthenticator: AdbAuthenticator = async function* (
70
70
 
71
71
  export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
72
72
  credentialStore: AdbCredentialStore,
73
- getNextRequest: () => Promise<AdbPacket>,
74
- ): AsyncIterable<AdbPacketCore> {
73
+ getNextRequest: () => Promise<AdbPacketData>,
74
+ ): AsyncIterable<AdbPacketData> {
75
75
  const packet = await getNextRequest();
76
76
 
77
77
  if (packet.arg0 !== AdbAuthType.Token) {
@@ -110,19 +110,19 @@ export const AdbPublicKeyAuthenticator: AdbAuthenticator = async function* (
110
110
  };
111
111
  };
112
112
 
113
- export const AdbDefaultAuthenticators: AdbAuthenticator[] = [
113
+ export const ADB_DEFAULT_AUTHENTICATORS: AdbAuthenticator[] = [
114
114
  AdbSignatureAuthenticator,
115
115
  AdbPublicKeyAuthenticator,
116
116
  ];
117
117
 
118
- export class AdbAuthenticationHandler implements Disposable {
118
+ export class AdbAuthenticationProcessor implements Disposable {
119
119
  public readonly authenticators: readonly AdbAuthenticator[];
120
120
 
121
121
  private readonly credentialStore: AdbCredentialStore;
122
122
 
123
- private pendingRequest = new PromiseResolver<AdbPacket>();
123
+ private pendingRequest = new PromiseResolver<AdbPacketData>();
124
124
 
125
- private iterator: AsyncIterator<AdbPacketCore> | undefined;
125
+ private iterator: AsyncIterator<AdbPacketData, void, void> | undefined;
126
126
 
127
127
  public constructor(
128
128
  authenticators: readonly AdbAuthenticator[],
@@ -132,16 +132,16 @@ export class AdbAuthenticationHandler implements Disposable {
132
132
  this.credentialStore = credentialStore;
133
133
  }
134
134
 
135
- private getNextRequest = (): Promise<AdbPacket> => {
135
+ private getNextRequest = (): Promise<AdbPacketData> => {
136
136
  return this.pendingRequest.promise;
137
137
  };
138
138
 
139
- private async* runAuthenticator(): AsyncGenerator<AdbPacketCore> {
139
+ private async* invokeAuthenticator(): AsyncGenerator<AdbPacketData, void, void> {
140
140
  for (const authenticator of this.authenticators) {
141
141
  for await (const packet of authenticator(this.credentialStore, this.getNextRequest)) {
142
142
  // If the authenticator yielded a response
143
143
  // Prepare `nextRequest` for next authentication request
144
- this.pendingRequest = new PromiseResolver<AdbPacket>();
144
+ this.pendingRequest = new PromiseResolver();
145
145
 
146
146
  // Yield the response to outer layer
147
147
  yield packet;
@@ -150,17 +150,20 @@ export class AdbAuthenticationHandler implements Disposable {
150
150
  // If the authenticator returned,
151
151
  // Next authenticator will be given the same `pendingRequest`
152
152
  }
153
-
154
- throw new Error('Cannot authenticate with device');
155
153
  }
156
154
 
157
- public async handle(packet: AdbPacket): Promise<AdbPacketCore> {
155
+ public async process(packet: AdbPacketData): Promise<AdbPacketData> {
158
156
  if (!this.iterator) {
159
- this.iterator = this.runAuthenticator();
157
+ this.iterator = this.invokeAuthenticator();
160
158
  }
161
159
 
162
160
  this.pendingRequest.resolve(packet);
161
+
163
162
  const result = await this.iterator.next();
163
+ if (result.done) {
164
+ throw new Error('No authenticator can handle the request');
165
+ }
166
+
164
167
  return result.value;
165
168
  }
166
169
 
package/src/backend.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ValueOrPromise } from '@yume-chan/struct';
2
- import type { AdbPacketCore, AdbPacketInit } from "./packet.js";
2
+ import type { AdbPacketData, AdbPacketInit } from "./packet.js";
3
3
  import type { ReadableWritablePair } from "./stream/index.js";
4
4
 
5
5
  export interface AdbBackend {
@@ -7,5 +7,5 @@ export interface AdbBackend {
7
7
 
8
8
  readonly name: string | undefined;
9
9
 
10
- connect(): ValueOrPromise<ReadableWritablePair<AdbPacketCore, AdbPacketInit>>;
10
+ connect(): ValueOrPromise<ReadableWritablePair<AdbPacketData, AdbPacketInit>>;
11
11
  }
@@ -8,18 +8,16 @@ export function install(
8
8
  ): WritableStream<Uint8Array> {
9
9
  const filename = `/data/local/tmp/${Math.random().toString().substring(2)}.apk`;
10
10
 
11
- return new WrapWritableStream<Uint8Array, WritableStream<Uint8Array>, AdbSync>({
11
+ let sync!: AdbSync;
12
+ return new WrapWritableStream<Uint8Array>({
12
13
  async start() {
13
- // Upload apk file to tmp folder
14
- const sync = await adb.sync();
15
- const writable = sync.write(filename, undefined, undefined);
14
+ // TODO: install: support other install apk methods (streaming, etc.)
16
15
 
17
- return {
18
- writable,
19
- state: sync,
20
- };
16
+ // Upload apk file to tmp folder
17
+ sync = await adb.sync();
18
+ return sync.write(filename, undefined, undefined);
21
19
  },
22
- async close(sync) {
20
+ async close() {
23
21
  sync.dispose();
24
22
 
25
23
  // Invoke `pm install` to install it
@@ -2,15 +2,11 @@
2
2
 
3
3
  import { AutoDisposable } from '@yume-chan/event';
4
4
  import Struct from '@yume-chan/struct';
5
- import type { AdbPacketCore } from '../packet.js';
6
- import type { AdbIncomingSocketEventArgs, AdbPacketDispatcher, AdbSocket } from '../socket/index.js';
7
- import { AdbBufferedStream } from '../stream/index.js';
5
+ import type { Adb } from "../adb.js";
6
+ import type { AdbIncomingSocketHandler, AdbSocket } from '../socket/index.js';
7
+ import { AdbBufferedStream, BufferedStreamEndedError } from '../stream/index.js';
8
8
  import { decodeUtf8 } from "../utils/index.js";
9
9
 
10
- export interface AdbReverseHandler {
11
- onSocket(packet: AdbPacketCore, socket: AdbSocket): void;
12
- }
13
-
14
10
  export interface AdbForwardListener {
15
11
  deviceSerial: string;
16
12
 
@@ -20,49 +16,42 @@ export interface AdbForwardListener {
20
16
  }
21
17
 
22
18
  const AdbReverseStringResponse =
23
- new Struct({ littleEndian: true })
19
+ new Struct()
24
20
  .string('length', { length: 4 })
25
- .string('content', { lengthField: 'length', lengthFieldBase: 16 });
21
+ .string('content', { lengthField: 'length', lengthFieldRadix: 16 });
26
22
 
27
23
  const AdbReverseErrorResponse =
28
- new Struct({ littleEndian: true })
24
+ new Struct()
29
25
  .fields(AdbReverseStringResponse)
30
26
  .postDeserialize((value) => {
31
27
  throw new Error(value.content);
32
28
  });
33
29
 
34
30
  export class AdbReverseCommand extends AutoDisposable {
35
- protected localPortToHandler = new Map<number, AdbReverseHandler>();
31
+ protected localAddressToHandler = new Map<string, AdbIncomingSocketHandler>();
36
32
 
37
- protected deviceAddressToLocalPort = new Map<string, number>();
33
+ protected deviceAddressToLocalAddress = new Map<string, string>();
38
34
 
39
- protected dispatcher: AdbPacketDispatcher;
35
+ protected adb: Adb;
40
36
 
41
37
  protected listening = false;
42
38
 
43
- public constructor(dispatcher: AdbPacketDispatcher) {
39
+ public constructor(adb: Adb) {
44
40
  super();
45
41
 
46
- this.dispatcher = dispatcher;
47
- this.addDisposable(this.dispatcher.onIncomingSocket(this.handleIncomingSocket, this));
42
+ this.adb = adb;
43
+ this.addDisposable(this.adb.addIncomingSocketHandler(this.handleIncomingSocket));
48
44
  }
49
45
 
50
- protected handleIncomingSocket(e: AdbIncomingSocketEventArgs): void {
51
- if (e.handled) {
52
- return;
53
- }
54
-
55
- const address = decodeUtf8(e.packet.payload);
56
- // Address format: `tcp:12345\0`
57
- const port = Number.parseInt(address.substring(4));
58
- if (this.localPortToHandler.has(port)) {
59
- this.localPortToHandler.get(port)!.onSocket(e.packet, e.socket);
60
- e.handled = true;
61
- }
62
- }
46
+ protected handleIncomingSocket = async (socket: AdbSocket) => {
47
+ let address = socket.serviceString;
48
+ // ADB daemon appends `\0` to the service string
49
+ address = address.replace(/\0/g, '');
50
+ return !!(await this.localAddressToHandler.get(address)?.(socket));
51
+ };
63
52
 
64
53
  private async createBufferedStream(service: string) {
65
- const socket = await this.dispatcher.createSocket(service);
54
+ const socket = await this.adb.createSocket(service);
66
55
  return new AdbBufferedStream(socket);
67
56
  }
68
57
 
@@ -89,26 +78,41 @@ export class AdbReverseCommand extends AutoDisposable {
89
78
 
90
79
  /**
91
80
  * @param deviceAddress The address adbd on device is listening on. Can be `tcp:0` to let adbd choose an available TCP port by itself.
92
- * @param localPort Native ADB will open a connection to localPort when reverse connection starts. In webadb, it's only used to uniquely identify a reverse registry, `handler` will be called on connection.
81
+ * @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.
93
82
  * @param handler A callback to handle incoming connections
94
83
  * @returns If `deviceAddress` is `tcp:0`, return `tcp:{ACTUAL_LISTENING_PORT}`; otherwise, return `deviceAddress`.
95
84
  */
96
85
  public async add(
97
86
  deviceAddress: string,
98
- localPort: number,
99
- handler: AdbReverseHandler,
87
+ localAddress: string,
88
+ handler: AdbIncomingSocketHandler,
100
89
  ): Promise<string> {
101
- const stream = await this.sendRequest(`reverse:forward:${deviceAddress};tcp:${localPort}`);
90
+ const stream = await this.sendRequest(`reverse:forward:${deviceAddress};${localAddress}`);
102
91
 
103
92
  // `tcp:0` tells the device to pick an available port.
104
- // However, device will response with the selected port for all `tcp:` requests.
93
+ // Begin with Android 8, device will respond with the selected port for all `tcp:` requests.
105
94
  if (deviceAddress.startsWith('tcp:')) {
106
- const response = await AdbReverseStringResponse.deserialize(stream);
107
- deviceAddress = `tcp:${Number.parseInt(response.content!, 10)}`;
95
+ let length: number | undefined;
96
+ try {
97
+ length = Number.parseInt(decodeUtf8(await stream.read(4)), 16);
98
+ } catch (e) {
99
+ if (!(e instanceof BufferedStreamEndedError)) {
100
+ throw e;
101
+ }
102
+
103
+ // Device before Android 8 doesn't have this response.
104
+ // (the stream is closed now)
105
+ // Can be safely ignored.
106
+ }
107
+
108
+ if (length !== undefined) {
109
+ const port = decodeUtf8(await stream.read(length!));
110
+ deviceAddress = `tcp:${Number.parseInt(port, 10)}`;
111
+ }
108
112
  }
109
113
 
110
- this.localPortToHandler.set(localPort, handler);
111
- this.deviceAddressToLocalPort.set(deviceAddress, localPort);
114
+ this.localAddressToHandler.set(localAddress, handler);
115
+ this.deviceAddressToLocalAddress.set(deviceAddress, localAddress);
112
116
  return deviceAddress;
113
117
 
114
118
  // No need to close the stream, device will close it
@@ -117,9 +121,9 @@ export class AdbReverseCommand extends AutoDisposable {
117
121
  public async remove(deviceAddress: string): Promise<void> {
118
122
  await this.sendRequest(`reverse:killforward:${deviceAddress}`);
119
123
 
120
- if (this.deviceAddressToLocalPort.has(deviceAddress)) {
121
- this.localPortToHandler.delete(this.deviceAddressToLocalPort.get(deviceAddress)!);
122
- this.deviceAddressToLocalPort.delete(deviceAddress);
124
+ if (this.deviceAddressToLocalAddress.has(deviceAddress)) {
125
+ this.localAddressToHandler.delete(this.deviceAddressToLocalAddress.get(deviceAddress)!);
126
+ this.deviceAddressToLocalAddress.delete(deviceAddress);
123
127
  }
124
128
 
125
129
  // No need to close the stream, device will close it
@@ -128,8 +132,8 @@ export class AdbReverseCommand extends AutoDisposable {
128
132
  public async removeAll(): Promise<void> {
129
133
  await this.sendRequest(`reverse:killforward-all`);
130
134
 
131
- this.deviceAddressToLocalPort.clear();
132
- this.localPortToHandler.clear();
135
+ this.deviceAddressToLocalAddress.clear();
136
+ this.localAddressToHandler.clear();
133
137
 
134
138
  // No need to close the stream, device will close it
135
139
  }