@yume-chan/adb 0.0.22 → 0.0.23

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 (57) hide show
  1. package/CHANGELOG.json +18 -0
  2. package/CHANGELOG.md +10 -1
  3. package/esm/adb.d.ts +4 -1
  4. package/esm/adb.d.ts.map +1 -1
  5. package/esm/adb.js +9 -2
  6. package/esm/adb.js.map +1 -1
  7. package/esm/banner.d.ts.map +1 -1
  8. package/esm/banner.js +1 -0
  9. package/esm/banner.js.map +1 -1
  10. package/esm/commands/reverse.js +1 -1
  11. package/esm/commands/reverse.js.map +1 -1
  12. package/esm/commands/subprocess/protocols/shell.d.ts.map +1 -1
  13. package/esm/commands/subprocess/protocols/shell.js +15 -69
  14. package/esm/commands/subprocess/protocols/shell.js.map +1 -1
  15. package/esm/commands/subprocess/utils.js +1 -1
  16. package/esm/commands/subprocess/utils.js.map +1 -1
  17. package/esm/commands/sync/sync.d.ts.map +1 -1
  18. package/esm/commands/sync/sync.js +5 -6
  19. package/esm/commands/sync/sync.js.map +1 -1
  20. package/esm/daemon/dispatcher.d.ts +7 -0
  21. package/esm/daemon/dispatcher.d.ts.map +1 -1
  22. package/esm/daemon/dispatcher.js +57 -12
  23. package/esm/daemon/dispatcher.js.map +1 -1
  24. package/esm/daemon/socket.d.ts +1 -1
  25. package/esm/daemon/socket.d.ts.map +1 -1
  26. package/esm/daemon/socket.js +36 -8
  27. package/esm/daemon/socket.js.map +1 -1
  28. package/esm/daemon/transport.d.ts +26 -2
  29. package/esm/daemon/transport.d.ts.map +1 -1
  30. package/esm/daemon/transport.js +53 -25
  31. package/esm/daemon/transport.js.map +1 -1
  32. package/esm/features.d.ts +2 -1
  33. package/esm/features.d.ts.map +1 -1
  34. package/esm/features.js +2 -1
  35. package/esm/features.js.map +1 -1
  36. package/esm/server/client.d.ts +2 -0
  37. package/esm/server/client.d.ts.map +1 -1
  38. package/esm/server/client.js +82 -50
  39. package/esm/server/client.js.map +1 -1
  40. package/esm/server/transport.d.ts +3 -0
  41. package/esm/server/transport.d.ts.map +1 -1
  42. package/esm/server/transport.js +26 -0
  43. package/esm/server/transport.js.map +1 -1
  44. package/package.json +13 -13
  45. package/src/adb.ts +15 -2
  46. package/src/banner.ts +1 -0
  47. package/src/commands/reverse.ts +2 -2
  48. package/src/commands/subprocess/protocols/shell.ts +22 -97
  49. package/src/commands/subprocess/utils.ts +1 -1
  50. package/src/commands/sync/sync.ts +5 -6
  51. package/src/daemon/dispatcher.ts +90 -14
  52. package/src/daemon/socket.ts +47 -16
  53. package/src/daemon/transport.ts +82 -24
  54. package/src/features.ts +2 -1
  55. package/src/server/client.ts +87 -51
  56. package/src/server/transport.ts +28 -0
  57. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,7 +1,9 @@
1
1
  import type { ValueOrPromise } from "@yume-chan/struct";
2
2
  import type { AdbIncomingSocketHandler, AdbSocket, AdbTransport } from "../adb.js";
3
3
  import type { AdbBanner } from "../banner.js";
4
+ import { AdbFeature } from "../features.js";
4
5
  import type { AdbServerClient } from "./client.js";
6
+ export declare const ADB_SERVER_DEFAULT_FEATURES: AdbFeature[];
5
7
  export declare class AdbServerTransport implements AdbTransport {
6
8
  #private;
7
9
  readonly serial: string;
@@ -9,6 +11,7 @@ export declare class AdbServerTransport implements AdbTransport {
9
11
  readonly maxPayloadSize: number;
10
12
  readonly banner: AdbBanner;
11
13
  readonly disconnected: Promise<void>;
14
+ get clientFeatures(): AdbFeature[];
12
15
  constructor(client: AdbServerClient, serial: string, banner: AdbBanner, transportId: bigint);
13
16
  connect(service: string): Promise<AdbSocket>;
14
17
  addReverseTunnel(handler: AdbIncomingSocketHandler, address?: string): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,KAAK,EACR,wBAAwB,EACxB,SAAS,EACT,YAAY,EACf,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,qBAAa,kBAAmB,YAAW,YAAY;;IAGnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAmB;IAElD,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAI3B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;gBAGjC,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM;IAgBjB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAS5C,gBAAgB,CAClB,OAAO,EAAE,wBAAwB,EACjC,OAAO,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IAIZ,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1C,KAAK,IAAI,cAAc,CAAC,IAAI,CAAC;CAIhC"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,KAAK,EACR,wBAAwB,EACxB,SAAS,EACT,YAAY,EACf,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,eAAO,MAAM,2BAA2B,cAmBvB,CAAC;AAElB,qBAAa,kBAAmB,YAAW,YAAY;;IAGnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAmB;IAElD,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAI3B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAErC,IAAI,cAAc,iBAIjB;gBAGG,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM;IAgBjB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAS5C,gBAAgB,CAClB,OAAO,EAAE,wBAAwB,EACjC,OAAO,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IAIZ,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1C,KAAK,IAAI,cAAc,CAAC,IAAI,CAAC;CAIhC"}
@@ -1,5 +1,26 @@
1
1
  import { PromiseResolver } from "@yume-chan/async";
2
2
  import { AbortController } from "@yume-chan/stream-extra";
3
+ import { AdbFeature } from "../features.js";
4
+ export const ADB_SERVER_DEFAULT_FEATURES = [
5
+ AdbFeature.ShellV2,
6
+ AdbFeature.Cmd,
7
+ AdbFeature.StatV2,
8
+ AdbFeature.ListV2,
9
+ AdbFeature.FixedPushMkdir,
10
+ "apex",
11
+ AdbFeature.Abb,
12
+ // only tells the client the symlink timestamp issue in `adb push --sync` has been fixed.
13
+ // No special handling required.
14
+ "fixed_push_symlink_timestamp",
15
+ AdbFeature.AbbExec,
16
+ "remount_shell",
17
+ "track_app",
18
+ AdbFeature.SendReceiveV2,
19
+ "sendrecv_v2_brotli",
20
+ "sendrecv_v2_lz4",
21
+ "sendrecv_v2_zstd",
22
+ "sendrecv_v2_dry_run_send",
23
+ ];
3
24
  export class AdbServerTransport {
4
25
  #client;
5
26
  serial;
@@ -9,6 +30,11 @@ export class AdbServerTransport {
9
30
  #closed = new PromiseResolver();
10
31
  #waitAbortController = new AbortController();
11
32
  disconnected;
33
+ get clientFeatures() {
34
+ // No need to get host features (features supported by ADB server)
35
+ // Because we create all ADB packets ourselves
36
+ return ADB_SERVER_DEFAULT_FEATURES;
37
+ }
12
38
  constructor(client, serial, banner, transportId) {
13
39
  this.#client = client;
14
40
  this.serial = serial;
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAY1D,MAAM,OAAO,kBAAkB;IAC3B,OAAO,CAAkB;IAEhB,MAAM,CAAS;IAEf,WAAW,CAAS;IAEpB,cAAc,GAAW,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAEzC,MAAM,CAAY;IAE3B,OAAO,GAAG,IAAI,eAAe,EAAQ,CAAC;IACtC,oBAAoB,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,YAAY,CAAgB;IAErC,YACI,MAAuB,EACvB,MAAc,EACd,MAAiB,EACjB,WAAmB;QAEnB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAE/B,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,OAAO;YACpB,MAAM,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,EAAE,YAAY,EAAE;gBAC1C,MAAM,EAAE,IAAI,CAAC,oBAAoB,CAAC,MAAM;gBACxC,KAAK,EAAE,IAAI;aACd,CAAC;SACL,CAAC,CAAC;IACP,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAe;QACzB,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CACnC;YACI,WAAW,EAAE,IAAI,CAAC,WAAW;SAChC,EACD,OAAO,CACV,CAAC;IACN,CAAC;IAED,KAAK,CAAC,gBAAgB,CAClB,OAAiC,EACjC,OAAgB;QAEhB,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,OAAe;QACrC,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,mBAAmB;QACrB,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,mBAAmB,EAAE,CAAC;IACxD,CAAC;IAED,KAAK;QACD,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACvB,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,CAAC;IACtC,CAAC;CACJ"}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAS1D,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAI5C,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACvC,UAAU,CAAC,OAAO;IAClB,UAAU,CAAC,GAAG;IACd,UAAU,CAAC,MAAM;IACjB,UAAU,CAAC,MAAM;IACjB,UAAU,CAAC,cAAc;IACzB,MAAM;IACN,UAAU,CAAC,GAAG;IACd,yFAAyF;IACzF,gCAAgC;IAChC,8BAA8B;IAC9B,UAAU,CAAC,OAAO;IAClB,eAAe;IACf,WAAW;IACX,UAAU,CAAC,aAAa;IACxB,oBAAoB;IACpB,iBAAiB;IACjB,kBAAkB;IAClB,0BAA0B;CACb,CAAC;AAElB,MAAM,OAAO,kBAAkB;IAC3B,OAAO,CAAkB;IAEhB,MAAM,CAAS;IAEf,WAAW,CAAS;IAEpB,cAAc,GAAW,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAEzC,MAAM,CAAY;IAE3B,OAAO,GAAG,IAAI,eAAe,EAAQ,CAAC;IACtC,oBAAoB,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,YAAY,CAAgB;IAErC,IAAI,cAAc;QACd,kEAAkE;QAClE,8CAA8C;QAC9C,OAAO,2BAA2B,CAAC;IACvC,CAAC;IAED,YACI,MAAuB,EACvB,MAAc,EACd,MAAiB,EACjB,WAAmB;QAEnB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAE/B,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,OAAO;YACpB,MAAM,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,EAAE,YAAY,EAAE;gBAC1C,MAAM,EAAE,IAAI,CAAC,oBAAoB,CAAC,MAAM;gBACxC,KAAK,EAAE,IAAI;aACd,CAAC;SACL,CAAC,CAAC;IACP,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAe;QACzB,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CACnC;YACI,WAAW,EAAE,IAAI,CAAC,WAAW;SAChC,EACD,OAAO,CACV,CAAC;IACN,CAAC;IAED,KAAK,CAAC,gBAAgB,CAClB,OAAiC,EACjC,OAAgB;QAEhB,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,OAAe;QACrC,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,mBAAmB;QACrB,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,mBAAmB,EAAE,CAAC;IACxD,CAAC;IAED,KAAK;QACD,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACvB,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,CAAC;IACtC,CAAC;CACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yume-chan/adb",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "TypeScript implementation of Android Debug Bridge (ADB) protocol.",
5
5
  "keywords": [
6
6
  "adb",
@@ -26,28 +26,28 @@
26
26
  "types": "esm/index.d.ts",
27
27
  "dependencies": {
28
28
  "@yume-chan/async": "^2.2.0",
29
- "@yume-chan/dataview-bigint-polyfill": "^0.0.22",
30
- "@yume-chan/event": "^0.0.22",
31
- "@yume-chan/stream-extra": "^0.0.22",
32
- "@yume-chan/struct": "^0.0.22",
29
+ "@yume-chan/dataview-bigint-polyfill": "^0.0.23",
30
+ "@yume-chan/event": "^0.0.23",
31
+ "@yume-chan/stream-extra": "^0.0.23",
32
+ "@yume-chan/struct": "^0.0.23",
33
33
  "tslib": "^2.6.2"
34
34
  },
35
35
  "devDependencies": {
36
- "@jest/globals": "^29.7.0",
37
- "@types/node": "^20.10.3",
36
+ "@jest/globals": "^30.0.0-alpha.3",
37
+ "@types/node": "^20.11.28",
38
38
  "@yume-chan/eslint-config": "^1.0.0",
39
39
  "@yume-chan/tsconfig": "^1.0.0",
40
40
  "cross-env": "^7.0.3",
41
- "eslint": "^8.55.0",
42
- "jest": "^29.7.0",
43
- "prettier": "^3.1.0",
44
- "ts-jest": "^29.1.1",
45
- "typescript": "^5.3.2"
41
+ "eslint": "^8.57.0",
42
+ "jest": "^30.0.0-alpha.3",
43
+ "prettier": "^3.2.5",
44
+ "ts-jest": "^29.1.2",
45
+ "typescript": "^5.4.2"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -b tsconfig.build.json",
49
49
  "build:watch": "tsc -b tsconfig.build.json",
50
- "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
50
+ "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" TS_JEST_DISABLE_VER_CHECKER=true jest --coverage",
51
51
  "lint": "eslint src/**/*.ts --fix && prettier src/**/*.ts --write --tab-width 4"
52
52
  }
53
53
  }
package/src/adb.ts CHANGED
@@ -40,6 +40,8 @@ export interface AdbTransport extends Closeable {
40
40
 
41
41
  readonly disconnected: Promise<void>;
42
42
 
43
+ readonly clientFeatures: readonly AdbFeature[];
44
+
43
45
  connect(service: string): ValueOrPromise<AdbSocket>;
44
46
 
45
47
  addReverseTunnel(
@@ -71,6 +73,14 @@ export class Adb implements Closeable {
71
73
  return this.transport.disconnected;
72
74
  }
73
75
 
76
+ public get clientFeatures() {
77
+ return this.transport.clientFeatures;
78
+ }
79
+
80
+ public get deviceFeatures() {
81
+ return this.banner.features;
82
+ }
83
+
74
84
  readonly subprocess: AdbSubprocess;
75
85
  readonly power: AdbPower;
76
86
  readonly reverse: AdbReverseCommand;
@@ -85,8 +95,11 @@ export class Adb implements Closeable {
85
95
  this.tcpip = new AdbTcpIpCommand(this);
86
96
  }
87
97
 
88
- supportsFeature(feature: AdbFeature): boolean {
89
- return this.banner.features.includes(feature);
98
+ canUseFeature(feature: AdbFeature): boolean {
99
+ return (
100
+ this.clientFeatures.includes(feature) &&
101
+ this.deviceFeatures.includes(feature)
102
+ );
90
103
  }
91
104
 
92
105
  async createSocket(service: string): Promise<AdbSocket> {
package/src/banner.ts CHANGED
@@ -18,6 +18,7 @@ export class AdbBanner {
18
18
  if (pieces.length > 1) {
19
19
  const props = pieces[1]!;
20
20
  for (const prop of props.split(";")) {
21
+ // istanbul ignore if
21
22
  if (!prop) {
22
23
  continue;
23
24
  }
@@ -38,7 +38,7 @@ const AdbReverseErrorResponse = new Struct()
38
38
  .concat(AdbReverseStringResponse)
39
39
  .postDeserialize((value) => {
40
40
  // https://issuetracker.google.com/issues/37066218
41
- // ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over WiFi),
41
+ // ADB on Android <9 can't create reverse tunnels when connected wirelessly (ADB over Wi-Fi),
42
42
  // and returns this confusing "more than one device/emulator" error.
43
43
  if (value.content === "more than one device/emulator") {
44
44
  throw new AdbReverseNotSupportedError();
@@ -81,7 +81,7 @@ export class AdbReverseCommand extends AutoDisposable {
81
81
  const stream = await this.createBufferedStream("reverse:list-forward");
82
82
 
83
83
  const response = await AdbReverseStringResponse.deserialize(stream);
84
- return response.content!.split("\n").map((line) => {
84
+ return response.content.split("\n").map((line) => {
85
85
  const [deviceSerial, localName, remoteName] = line.split(" ") as [
86
86
  string,
87
87
  string,
@@ -6,12 +6,10 @@ import type {
6
6
  WritableStreamDefaultWriter,
7
7
  } from "@yume-chan/stream-extra";
8
8
  import {
9
- ConsumableTransformStream,
10
9
  ConsumableWritableStream,
11
10
  PushReadableStream,
12
11
  StructDeserializeStream,
13
12
  WritableStream,
14
- pipeFrom,
15
13
  } from "@yume-chan/stream-extra";
16
14
  import type { StructValueType } from "@yume-chan/struct";
17
15
  import Struct, { placeholder } from "@yume-chan/struct";
@@ -37,68 +35,8 @@ const AdbShellProtocolPacket = new Struct({ littleEndian: true })
37
35
  .uint32("length")
38
36
  .uint8Array("data", { lengthField: "length" });
39
37
 
40
- type AdbShellProtocolPacketInit = (typeof AdbShellProtocolPacket)["TInit"];
41
-
42
38
  type AdbShellProtocolPacket = StructValueType<typeof AdbShellProtocolPacket>;
43
39
 
44
- class StdinSerializeStream extends ConsumableTransformStream<
45
- Uint8Array,
46
- AdbShellProtocolPacketInit
47
- > {
48
- constructor() {
49
- super({
50
- async transform(chunk, controller) {
51
- await controller.enqueue({
52
- id: AdbShellProtocolId.Stdin,
53
- data: chunk,
54
- });
55
- },
56
- flush() {
57
- // TODO: AdbShellSubprocessProtocol: support closing stdin
58
- },
59
- });
60
- }
61
- }
62
-
63
- class MultiplexStream<T> {
64
- #readable: PushReadableStream<T>;
65
- #readableController!: PushReadableStreamController<T>;
66
- get readable() {
67
- return this.#readable;
68
- }
69
-
70
- #activeCount = 0;
71
-
72
- constructor() {
73
- this.#readable = new PushReadableStream((controller) => {
74
- this.#readableController = controller;
75
- });
76
- }
77
-
78
- createWriteable() {
79
- return new WritableStream<T>({
80
- start: () => {
81
- this.#activeCount += 1;
82
- },
83
- write: async (chunk) => {
84
- await this.#readableController.enqueue(chunk);
85
- },
86
- abort: () => {
87
- this.#activeCount -= 1;
88
- if (this.#activeCount === 0) {
89
- this.#readableController.close();
90
- }
91
- },
92
- close: () => {
93
- this.#activeCount -= 1;
94
- if (this.#activeCount === 0) {
95
- this.#readableController.close();
96
- }
97
- },
98
- });
99
- }
100
- }
101
-
102
40
  /**
103
41
  * Shell v2 a.k.a Shell Protocol
104
42
  *
@@ -109,7 +47,7 @@ class MultiplexStream<T> {
109
47
  */
110
48
  export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
111
49
  static isSupported(adb: Adb) {
112
- return adb.supportsFeature(AdbFeature.ShellV2);
50
+ return adb.canUseFeature(AdbFeature.ShellV2);
113
51
  }
114
52
 
115
53
  static async pty(adb: Adb, command: string) {
@@ -126,9 +64,7 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
126
64
  }
127
65
 
128
66
  readonly #socket: AdbSocket;
129
- #socketWriter: WritableStreamDefaultWriter<
130
- Consumable<AdbShellProtocolPacketInit>
131
- >;
67
+ #writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
132
68
 
133
69
  #stdin: WritableStream<Consumable<Uint8Array>>;
134
70
  get stdin() {
@@ -153,13 +89,8 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
153
89
  constructor(socket: AdbSocket) {
154
90
  this.#socket = socket;
155
91
 
156
- // Check this image to help you understand the stream graph
157
- // cspell: disable-next-line
158
- // https://www.plantuml.com/plantuml/png/bL91QiCm4Bpx5SAdv90lb1JISmiw5XzaQKf5PIkiLZIqzEyLSg8ks13gYtOykpFhiOw93N6UGjVDqK7rZsxKqNw0U_NTgVAy4empOy2mm4_olC0VEVEE47GUpnGjKdgXoD76q4GIEpyFhOwP_m28hW0NNzxNUig1_JdW0bA7muFIJDco1daJ_1SAX9bgvoPJPyIkSekhNYctvIGXrCH6tIsPL5fs-s6J5yc9BpWXhKtNdF2LgVYPGM_6GlMwfhWUsIt4lbScANrwlgVVUifPSVi__t44qStnwPvZwobdSmHHlL57p2vFuHS0
159
-
160
92
  let stdoutController!: PushReadableStreamController<Uint8Array>;
161
93
  let stderrController!: PushReadableStreamController<Uint8Array>;
162
-
163
94
  this.#stdout = new PushReadableStream<Uint8Array>((controller) => {
164
95
  stdoutController = controller;
165
96
  });
@@ -207,39 +138,33 @@ export class AdbSubprocessShellProtocol implements AdbSubprocessProtocol {
207
138
  },
208
139
  );
209
140
 
210
- const multiplexer = new MultiplexStream<
211
- Consumable<AdbShellProtocolPacketInit>
212
- >();
213
- void multiplexer.readable
214
- .pipeThrough(
215
- new ConsumableTransformStream({
216
- async transform(chunk, controller) {
217
- await controller.enqueue(
218
- AdbShellProtocolPacket.serialize(chunk),
219
- );
220
- },
221
- }),
222
- )
223
- .pipeTo(socket.writable);
224
-
225
- this.#stdin = pipeFrom(
226
- multiplexer.createWriteable(),
227
- new StdinSerializeStream(),
228
- );
141
+ this.#writer = this.#socket.writable.getWriter();
229
142
 
230
- this.#socketWriter = multiplexer.createWriteable().getWriter();
143
+ this.#stdin = new WritableStream<Consumable<Uint8Array>>({
144
+ write: async (chunk) => {
145
+ await ConsumableWritableStream.write(
146
+ this.#writer,
147
+ AdbShellProtocolPacket.serialize({
148
+ id: AdbShellProtocolId.Stdin,
149
+ data: chunk.value,
150
+ }),
151
+ );
152
+ chunk.consume();
153
+ },
154
+ });
231
155
  }
232
156
 
233
157
  async resize(rows: number, cols: number) {
234
- await ConsumableWritableStream.write(this.#socketWriter, {
235
- id: AdbShellProtocolId.WindowSizeChange,
236
- data: encodeUtf8(
158
+ await ConsumableWritableStream.write(
159
+ this.#writer,
160
+ AdbShellProtocolPacket.serialize({
161
+ id: AdbShellProtocolId.WindowSizeChange,
237
162
  // The "correct" format is `${rows}x${cols},${x_pixels}x${y_pixels}`
238
163
  // However, according to https://linux.die.net/man/4/tty_ioctl
239
164
  // `x_pixels` and `y_pixels` are unused, so always sending `0` should be fine.
240
- `${rows}x${cols},0x0\0`,
241
- ),
242
- });
165
+ data: encodeUtf8(`${rows}x${cols},0x0\0`),
166
+ }),
167
+ );
243
168
  }
244
169
 
245
170
  kill() {
@@ -10,7 +10,7 @@ export function escapeArg(s: string) {
10
10
  break;
11
11
  }
12
12
  result += s.substring(base, found);
13
- // a'b becomes a'\'b
13
+ // a'b becomes a'\'b (the backslash is not a escape character)
14
14
  result += String.raw`'\''`;
15
15
  base = found + 1;
16
16
  }
@@ -74,16 +74,15 @@ export class AdbSync extends AutoDisposable {
74
74
  this._adb = adb;
75
75
  this._socket = new AdbSyncSocket(socket, adb.maxPayloadSize);
76
76
 
77
- this.#supportsStat = adb.supportsFeature(AdbFeature.StatV2);
78
- this.#supportsListV2 = adb.supportsFeature(AdbFeature.ListV2);
79
- this.#fixedPushMkdir = adb.supportsFeature(AdbFeature.FixedPushMkdir);
80
- this.#supportsSendReceiveV2 = adb.supportsFeature(
77
+ this.#supportsStat = adb.canUseFeature(AdbFeature.StatV2);
78
+ this.#supportsListV2 = adb.canUseFeature(AdbFeature.ListV2);
79
+ this.#fixedPushMkdir = adb.canUseFeature(AdbFeature.FixedPushMkdir);
80
+ this.#supportsSendReceiveV2 = adb.canUseFeature(
81
81
  AdbFeature.SendReceiveV2,
82
82
  );
83
83
  // https://android.googlesource.com/platform/packages/modules/adb/+/91768a57b7138166e0a3d11f79cd55909dda7014/client/file_sync_client.cpp#1361
84
84
  this.#needPushMkdirWorkaround =
85
- this._adb.supportsFeature(AdbFeature.ShellV2) &&
86
- !this.fixedPushMkdir;
85
+ this._adb.canUseFeature(AdbFeature.ShellV2) && !this.fixedPushMkdir;
87
86
  }
88
87
 
89
88
  /**
@@ -13,7 +13,7 @@ import {
13
13
  ConsumableWritableStream,
14
14
  WritableStream,
15
15
  } from "@yume-chan/stream-extra";
16
- import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
16
+ import { EMPTY_UINT8_ARRAY, NumberFieldType } from "@yume-chan/struct";
17
17
 
18
18
  import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js";
19
19
  import { decodeUtf8, encodeUtf8 } from "../utils/index.js";
@@ -32,6 +32,13 @@ export interface AdbPacketDispatcherOptions {
32
32
  */
33
33
  appendNullToServiceString: boolean;
34
34
  maxPayloadSize: number;
35
+ /**
36
+ * The number of bytes the device can send before receiving an ack packet.
37
+
38
+ * Set to 0 or any negative value to disable delayed ack.
39
+ * Otherwise the value must be in the range of unsigned 32-bit integer.
40
+ */
41
+ initialDelayedAckBytes: number;
35
42
  /**
36
43
  * Whether to preserve the connection open after the `AdbPacketDispatcher` is closed.
37
44
  */
@@ -39,6 +46,11 @@ export interface AdbPacketDispatcherOptions {
39
46
  debugSlowRead?: boolean | undefined;
40
47
  }
41
48
 
49
+ interface SocketOpenResult {
50
+ remoteId: number;
51
+ availableWriteBytes: number;
52
+ }
53
+
42
54
  /**
43
55
  * The dispatcher is the "dumb" part of the connection handling logic.
44
56
  *
@@ -79,6 +91,10 @@ export class AdbPacketDispatcher implements Closeable {
79
91
  options: AdbPacketDispatcherOptions,
80
92
  ) {
81
93
  this.options = options;
94
+ // Don't allow negative values in dispatcher
95
+ if (this.options.initialDelayedAckBytes < 0) {
96
+ this.options.initialDelayedAckBytes = 0;
97
+ }
82
98
 
83
99
  connection.readable
84
100
  .pipeTo(
@@ -169,15 +185,40 @@ export class AdbPacketDispatcher implements Closeable {
169
185
  }
170
186
 
171
187
  #handleOkay(packet: AdbPacketData) {
172
- if (this.#initializers.resolve(packet.arg1, packet.arg0)) {
188
+ let ackBytes: number;
189
+ if (this.options.initialDelayedAckBytes !== 0) {
190
+ if (packet.payload.byteLength !== 4) {
191
+ throw new Error(
192
+ "Invalid OKAY packet. Payload size should be 4",
193
+ );
194
+ }
195
+ ackBytes = NumberFieldType.Uint32.deserialize(packet.payload, true);
196
+ } else {
197
+ if (packet.payload.byteLength !== 0) {
198
+ throw new Error(
199
+ "Invalid OKAY packet. Payload size should be 0",
200
+ );
201
+ }
202
+ ackBytes = Infinity;
203
+ }
204
+
205
+ if (
206
+ this.#initializers.resolve(packet.arg1, {
207
+ remoteId: packet.arg0,
208
+ availableWriteBytes: ackBytes,
209
+ } satisfies SocketOpenResult)
210
+ ) {
173
211
  // Device successfully created the socket
174
212
  return;
175
213
  }
176
214
 
177
215
  const socket = this.#sockets.get(packet.arg1);
178
216
  if (socket) {
179
- // Device has received last `WRTE` to the socket
180
- socket.ack();
217
+ // When delayed ack is enabled, `ackBytes` is a positive number represents
218
+ // how many bytes the device has received from this socket.
219
+ // When delayed ack is disabled, `ackBytes` is always `Infinity` represents
220
+ // the device has received last `WRTE` packet from the socket.
221
+ socket.ack(ackBytes);
181
222
  return;
182
223
  }
183
224
 
@@ -186,6 +227,18 @@ export class AdbPacketDispatcher implements Closeable {
186
227
  void this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0);
187
228
  }
188
229
 
230
+ #sendOkay(localId: number, remoteId: number, ackBytes: number) {
231
+ let payload: Uint8Array;
232
+ if (this.options.initialDelayedAckBytes !== 0) {
233
+ payload = new Uint8Array(4);
234
+ new DataView(payload.buffer).setUint32(0, ackBytes, true);
235
+ } else {
236
+ payload = EMPTY_UINT8_ARRAY;
237
+ }
238
+
239
+ return this.sendPacket(AdbCommand.Okay, localId, remoteId, payload);
240
+ }
241
+
189
242
  async #handleOpen(packet: AdbPacketData) {
190
243
  // `AsyncOperationManager` doesn't support skipping IDs
191
244
  // Use `add` + `resolve` to simulate this behavior
@@ -193,9 +246,20 @@ export class AdbPacketDispatcher implements Closeable {
193
246
  this.#initializers.resolve(localId, undefined);
194
247
 
195
248
  const remoteId = packet.arg0;
196
- let service = decodeUtf8(packet.payload);
197
- if (service.endsWith("\0")) {
198
- service = service.substring(0, service.length - 1);
249
+ let initialDelayedAckBytes = packet.arg1;
250
+ const service = decodeUtf8(packet.payload);
251
+
252
+ if (this.options.initialDelayedAckBytes === 0) {
253
+ if (initialDelayedAckBytes !== 0) {
254
+ throw new Error("Invalid OPEN packet. arg1 should be 0");
255
+ }
256
+ initialDelayedAckBytes = Infinity;
257
+ } else {
258
+ if (initialDelayedAckBytes === 0) {
259
+ throw new Error(
260
+ "Invalid OPEN packet. arg1 should be greater than 0",
261
+ );
262
+ }
199
263
  }
200
264
 
201
265
  const handler = this.#incomingSocketHandlers.get(service);
@@ -211,11 +275,16 @@ export class AdbPacketDispatcher implements Closeable {
211
275
  localCreated: false,
212
276
  service,
213
277
  });
278
+ controller.ack(initialDelayedAckBytes);
214
279
 
215
280
  try {
216
281
  await handler(controller.socket);
217
282
  this.#sockets.set(localId, controller);
218
- await this.sendPacket(AdbCommand.Okay, localId, remoteId);
283
+ await this.#sendOkay(
284
+ localId,
285
+ remoteId,
286
+ this.options.initialDelayedAckBytes,
287
+ );
219
288
  } catch (e) {
220
289
  await this.sendPacket(AdbCommand.Close, 0, remoteId);
221
290
  }
@@ -238,10 +307,10 @@ export class AdbPacketDispatcher implements Closeable {
238
307
  }),
239
308
  (async () => {
240
309
  await socket.enqueue(packet.payload);
241
- await this.sendPacket(
242
- AdbCommand.Okay,
310
+ await this.#sendOkay(
243
311
  packet.arg1,
244
312
  packet.arg0,
313
+ packet.payload.length,
245
314
  );
246
315
  handled = true;
247
316
  })(),
@@ -255,11 +324,17 @@ export class AdbPacketDispatcher implements Closeable {
255
324
  service += "\0";
256
325
  }
257
326
 
258
- const [localId, initializer] = this.#initializers.add<number>();
259
- await this.sendPacket(AdbCommand.Open, localId, 0, service);
327
+ const [localId, initializer] =
328
+ this.#initializers.add<SocketOpenResult>();
329
+ await this.sendPacket(
330
+ AdbCommand.Open,
331
+ localId,
332
+ this.options.initialDelayedAckBytes,
333
+ service,
334
+ );
260
335
 
261
- // Fulfilled by `handleOk`
262
- const remoteId = await initializer;
336
+ // Fulfilled by `handleOkay`
337
+ const { remoteId, availableWriteBytes } = await initializer;
263
338
  const controller = new AdbDaemonSocketController({
264
339
  dispatcher: this,
265
340
  localId,
@@ -267,6 +342,7 @@ export class AdbPacketDispatcher implements Closeable {
267
342
  localCreated: true,
268
343
  service,
269
344
  });
345
+ controller.ack(availableWriteBytes);
270
346
  this.#sockets.set(localId, controller);
271
347
 
272
348
  return controller.socket;