adb.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // cspell: ignore libusb
  2. import { PromiseResolver } from '@yume-chan/async';
  3. import { AbortController, DecodeUtf8Stream, GatherStringStream, WritableStream, type ReadableWritablePair } from '@yume-chan/stream-extra';
  4. import { AdbAuthenticationProcessor, ADB_DEFAULT_AUTHENTICATORS, type AdbCredentialStore } from './auth.js';
  5. import { AdbPower, AdbReverseCommand, AdbSubprocess, AdbSync, AdbTcpIpCommand, escapeArg, framebuffer, install, type AdbFrameBuffer } from './commands/index.js';
  6. import { AdbFeatures } from './features.js';
  7. import { AdbCommand, calculateChecksum, type AdbPacketData, type AdbPacketInit } from './packet.js';
  8. import { AdbIncomingSocketHandler, AdbPacketDispatcher, type AdbSocket, type Closeable } from './socket/index.js';
  9. import { decodeUtf8, encodeUtf8 } from './utils/index.js';
  10. export enum AdbPropKey {
  11. Product = 'ro.product.name',
  12. Model = 'ro.product.model',
  13. Device = 'ro.product.device',
  14. Features = 'features',
  15. }
  16. export const VERSION_OMIT_CHECKSUM = 0x01000001;
  17. export class Adb implements Closeable {
  18. /**
  19. * It's possible to call `authenticate` multiple times on a single connection,
  20. * every time the device receives a `CNXN` packet, it resets its internal state,
  21. * and starts a new authentication process.
  22. */
  23. public static async authenticate(
  24. connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
  25. credentialStore: AdbCredentialStore,
  26. authenticators = ADB_DEFAULT_AUTHENTICATORS,
  27. ): Promise<Adb> {
  28. // Initially, set to highest-supported version and payload size.
  29. let version = 0x01000001;
  30. let maxPayloadSize = 0x100000;
  31. const resolver = new PromiseResolver<string>();
  32. const authProcessor = new AdbAuthenticationProcessor(authenticators, credentialStore);
  33. // Here is similar to `AdbPacketDispatcher`,
  34. // But the received packet types and send packet processing are different.
  35. const abortController = new AbortController();
  36. const pipe = connection.readable
  37. .pipeTo(new WritableStream({
  38. async write(packet) {
  39. switch (packet.command) {
  40. case AdbCommand.Connect:
  41. version = Math.min(version, packet.arg0);
  42. maxPayloadSize = Math.min(maxPayloadSize, packet.arg1);
  43. resolver.resolve(decodeUtf8(packet.payload));
  44. break;
  45. case AdbCommand.Auth:
  46. const response = await authProcessor.process(packet);
  47. await sendPacket(response);
  48. break;
  49. default:
  50. // Maybe the previous ADB client exited without reading all packets,
  51. // so they are still waiting in OS internal buffer.
  52. // Just ignore them.
  53. // Because a `Connect` packet will reset the device,
  54. // Eventually there will be `Connect` and `Auth` response packets.
  55. break;
  56. }
  57. }
  58. }), {
  59. // Don't cancel the source ReadableStream on AbortSignal abort.
  60. preventCancel: true,
  61. signal: abortController.signal,
  62. })
  63. .catch((e) => {
  64. resolver.reject(e);
  65. });
  66. const writer = connection.writable.getWriter();
  67. async function sendPacket(init: AdbPacketData) {
  68. // Always send checksum in auth steps
  69. // Because we don't know if the device needs it or not.
  70. await writer.write(calculateChecksum(init));
  71. }
  72. let banner: string;
  73. try {
  74. // https://android.googlesource.com/platform/packages/modules/adb/+/79010dc6d5ca7490c493df800d4421730f5466ca/transport.cpp#1252
  75. // There are some other feature constants, but some of them are only used by ADB server, not devices (daemons).
  76. const features = [
  77. AdbFeatures.ShellV2,
  78. AdbFeatures.Cmd,
  79. AdbFeatures.StatV2,
  80. AdbFeatures.ListV2,
  81. AdbFeatures.FixedPushMkdir,
  82. 'apex',
  83. 'abb',
  84. // only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
  85. // No special handling required.
  86. 'fixed_push_symlink_timestamp',
  87. 'abb_exec',
  88. 'remount_shell',
  89. 'track_app',
  90. 'sendrecv_v2',
  91. 'sendrecv_v2_brotli',
  92. 'sendrecv_v2_lz4',
  93. 'sendrecv_v2_zstd',
  94. 'sendrecv_v2_dry_run_send',
  95. ].join(',');
  96. await sendPacket({
  97. command: AdbCommand.Connect,
  98. arg0: version,
  99. arg1: maxPayloadSize,
  100. // The terminating `;` is required in formal definition
  101. // But ADB daemon (all versions) can still work without it
  102. payload: encodeUtf8(`host::features=${features};`),
  103. });
  104. banner = await resolver.promise;
  105. } finally {
  106. // When failed, release locks on `connection` so the caller can try again.
  107. // When success, also release locks so `AdbPacketDispatcher` can use them.
  108. abortController.abort();
  109. writer.releaseLock();
  110. // Wait until pipe stops (`ReadableStream` lock released)
  111. await pipe;
  112. }
  113. return new Adb(
  114. connection,
  115. version,
  116. maxPayloadSize,
  117. banner,
  118. );
  119. }
  120. private readonly dispatcher: AdbPacketDispatcher;
  121. public get disconnected() { return this.dispatcher.disconnected; }
  122. private _protocolVersion: number | undefined;
  123. public get protocolVersion() { return this._protocolVersion; }
  124. private _product: string | undefined;
  125. public get product() { return this._product; }
  126. private _model: string | undefined;
  127. public get model() { return this._model; }
  128. private _device: string | undefined;
  129. public get device() { return this._device; }
  130. private _features: AdbFeatures[] = [];
  131. public get features() { return this._features; }
  132. public readonly subprocess: AdbSubprocess;
  133. public readonly power: AdbPower;
  134. public readonly reverse: AdbReverseCommand;
  135. public readonly tcpip: AdbTcpIpCommand;
  136. public constructor(
  137. connection: ReadableWritablePair<AdbPacketData, AdbPacketInit>,
  138. version: number,
  139. maxPayloadSize: number,
  140. banner: string,
  141. ) {
  142. this.parseBanner(banner);
  143. let calculateChecksum: boolean;
  144. let appendNullToServiceString: boolean;
  145. if (version >= VERSION_OMIT_CHECKSUM) {
  146. calculateChecksum = false;
  147. appendNullToServiceString = false;
  148. } else {
  149. calculateChecksum = true;
  150. appendNullToServiceString = true;
  151. }
  152. this.dispatcher = new AdbPacketDispatcher(
  153. connection,
  154. {
  155. calculateChecksum,
  156. appendNullToServiceString,
  157. maxPayloadSize,
  158. }
  159. );
  160. this._protocolVersion = version;
  161. this.subprocess = new AdbSubprocess(this);
  162. this.power = new AdbPower(this);
  163. this.reverse = new AdbReverseCommand(this);
  164. this.tcpip = new AdbTcpIpCommand(this);
  165. }
  166. private parseBanner(banner: string): void {
  167. const pieces = banner.split('::');
  168. if (pieces.length > 1) {
  169. const props = pieces[1]!;
  170. for (const prop of props.split(';')) {
  171. if (!prop) {
  172. continue;
  173. }
  174. const keyValue = prop.split('=');
  175. if (keyValue.length !== 2) {
  176. continue;
  177. }
  178. const [key, value] = keyValue;
  179. switch (key) {
  180. case AdbPropKey.Product:
  181. this._product = value;
  182. break;
  183. case AdbPropKey.Model:
  184. this._model = value;
  185. break;
  186. case AdbPropKey.Device:
  187. this._device = value;
  188. break;
  189. case AdbPropKey.Features:
  190. this._features = value!.split(',') as AdbFeatures[];
  191. break;
  192. }
  193. }
  194. }
  195. }
  196. /**
  197. * Add a handler for incoming socket.
  198. * @param handler A function to call with new incoming sockets. It must return `true` if it accepts the socket.
  199. * @returns A function to remove the handler.
  200. */
  201. public onIncomingSocket(handler: AdbIncomingSocketHandler) {
  202. return this.dispatcher.onIncomingSocket(handler);
  203. }
  204. public async createSocket(service: string): Promise<AdbSocket> {
  205. return this.dispatcher.createSocket(service);
  206. }
  207. public async createSocketAndWait(service: string): Promise<string> {
  208. const socket = await this.createSocket(service);
  209. const gatherStream = new GatherStringStream();
  210. await socket.readable
  211. .pipeThrough(new DecodeUtf8Stream())
  212. .pipeTo(gatherStream);
  213. return gatherStream.result;
  214. }
  215. public async getProp(key: string): Promise<string> {
  216. const stdout = await this.subprocess.spawnAndWaitLegacy(
  217. ['getprop', key]
  218. );
  219. return stdout.trim();
  220. }
  221. public async rm(...filenames: string[]): Promise<string> {//: Promise<AdbSubprocessProtocol>
  222. // https://android.googlesource.com/platform/packages/modules/adb/+/1a0fb8846d4e6b671c8aa7f137a8c21d7b248716/client/adb_install.cpp#984
  223. await this.subprocess.shell([
  224. "rm",
  225. ...filenames.map((arg) => escapeArg(arg)),
  226. "</dev/null",
  227. ]);
  228. return "";
  229. }
  230. public install() {
  231. return install(this);
  232. }
  233. public async sync(): Promise<AdbSync> {
  234. const socket = await this.createSocket('sync:');
  235. return new AdbSync(this, socket);
  236. }
  237. public async framebuffer(): Promise<AdbFrameBuffer> {
  238. return framebuffer(this);
  239. }
  240. /**
  241. * Close the ADB connection.
  242. *
  243. * Note that it won't close the streams from backends.
  244. * The streams are both physically and logically intact,
  245. * and can be reused.
  246. */
  247. public async close(): Promise<void> {
  248. await this.dispatcher.close();
  249. }
  250. }