appium-ios-remotexpc 0.8.0 → 0.10.0

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 (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/build/src/index.d.ts +2 -1
  4. package/build/src/index.d.ts.map +1 -1
  5. package/build/src/index.js +1 -0
  6. package/build/src/lib/types.d.ts +30 -0
  7. package/build/src/lib/types.d.ts.map +1 -1
  8. package/build/src/lib/types.js +2 -0
  9. package/build/src/services/index.d.ts +3 -1
  10. package/build/src/services/index.d.ts.map +1 -1
  11. package/build/src/services/index.js +3 -1
  12. package/build/src/services/ios/afc/codec.d.ts +46 -0
  13. package/build/src/services/ios/afc/codec.d.ts.map +1 -0
  14. package/build/src/services/ios/afc/codec.js +263 -0
  15. package/build/src/services/ios/afc/constants.d.ts +11 -0
  16. package/build/src/services/ios/afc/constants.d.ts.map +1 -0
  17. package/build/src/services/ios/afc/constants.js +22 -0
  18. package/build/src/services/ios/afc/enums.d.ts +66 -0
  19. package/build/src/services/ios/afc/enums.d.ts.map +1 -0
  20. package/build/src/services/ios/afc/enums.js +70 -0
  21. package/build/src/services/ios/afc/index.d.ts +72 -0
  22. package/build/src/services/ios/afc/index.d.ts.map +1 -0
  23. package/build/src/services/ios/afc/index.js +385 -0
  24. package/build/src/services/ios/afc/stream-utils.d.ts +14 -0
  25. package/build/src/services/ios/afc/stream-utils.d.ts.map +1 -0
  26. package/build/src/services/ios/afc/stream-utils.js +60 -0
  27. package/build/src/services/ios/power-assertion/index.d.ts +40 -0
  28. package/build/src/services/ios/power-assertion/index.d.ts.map +1 -0
  29. package/build/src/services/ios/power-assertion/index.js +64 -0
  30. package/build/src/services.d.ts +8 -1
  31. package/build/src/services.d.ts.map +1 -1
  32. package/build/src/services.js +25 -0
  33. package/package.json +3 -1
  34. package/src/index.ts +4 -0
  35. package/src/lib/types.ts +34 -0
  36. package/src/services/index.ts +4 -0
  37. package/src/services/ios/afc/codec.ts +365 -0
  38. package/src/services/ios/afc/constants.ts +29 -0
  39. package/src/services/ios/afc/enums.ts +70 -0
  40. package/src/services/ios/afc/index.ts +511 -0
  41. package/src/services/ios/afc/stream-utils.ts +102 -0
  42. package/src/services/ios/power-assertion/index.ts +100 -0
  43. package/src/services.ts +33 -0
@@ -0,0 +1,64 @@
1
+ import { logger } from '@appium/support';
2
+ import { ServiceConnection } from '../../../service-connection.js';
3
+ import { BaseService } from '../base-service.js';
4
+ const log = logger.getLogger('PowerAssertionService');
5
+ /**
6
+ * Power assertion types that can be used to prevent system sleep
7
+ */
8
+ export var PowerAssertionType;
9
+ (function (PowerAssertionType) {
10
+ PowerAssertionType["WIRELESS_SYNC"] = "AMDPowerAssertionTypeWirelessSync";
11
+ PowerAssertionType["PREVENT_USER_IDLE_SYSTEM_SLEEP"] = "PreventUserIdleSystemSleep";
12
+ PowerAssertionType["PREVENT_SYSTEM_SLEEP"] = "PreventSystemSleep";
13
+ })(PowerAssertionType || (PowerAssertionType = {}));
14
+ /**
15
+ * PowerAssertionService provides an API to create power assertions.
16
+ */
17
+ class PowerAssertionService extends BaseService {
18
+ static RSD_SERVICE_NAME = 'com.apple.mobile.assertion_agent.shim.remote';
19
+ _conn = null;
20
+ /**
21
+ * Create a power assertion to prevent system sleep
22
+ * @param options Options for creating the power assertion
23
+ * @returns Promise that resolves when the assertion is created
24
+ */
25
+ async createPowerAssertion(options) {
26
+ if (!this._conn) {
27
+ this._conn = await this.connectToPowerAssertionService();
28
+ }
29
+ const request = this.buildCreateAssertionRequest(options);
30
+ await this._conn.sendPlistRequest(request);
31
+ log.info(`Power assertion created: type="${options.type}", name="${options.name}", timeout=${options.timeout}s`);
32
+ }
33
+ /**
34
+ * Close the connection to the power assertion service
35
+ */
36
+ async close() {
37
+ if (this._conn) {
38
+ await this._conn.close();
39
+ this._conn = null;
40
+ log.debug('Power assertion service connection closed');
41
+ }
42
+ }
43
+ async connectToPowerAssertionService() {
44
+ const service = {
45
+ serviceName: PowerAssertionService.RSD_SERVICE_NAME,
46
+ port: this.address[1].toString(),
47
+ };
48
+ log.debug(`Connecting to power assertion service at ${this.address[0]}:${this.address[1]}`);
49
+ return await this.startLockdownService(service);
50
+ }
51
+ buildCreateAssertionRequest(options) {
52
+ const request = {
53
+ CommandKey: 'CommandCreateAssertion',
54
+ AssertionTypeKey: options.type,
55
+ AssertionNameKey: options.name,
56
+ AssertionTimeoutKey: options.timeout,
57
+ };
58
+ if (options.details !== undefined) {
59
+ request.AssertionDetailKey = options.details;
60
+ }
61
+ return request;
62
+ }
63
+ }
64
+ export { PowerAssertionService };
@@ -1,11 +1,18 @@
1
1
  import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
2
- import type { DiagnosticsServiceWithConnection, MobileConfigServiceWithConnection, MobileImageMounterServiceWithConnection, NotificationProxyServiceWithConnection, SpringboardServiceWithConnection, SyslogService as SyslogServiceType, WebInspectorServiceWithConnection } from './lib/types.js';
2
+ import type { DiagnosticsServiceWithConnection, MobileConfigServiceWithConnection, MobileImageMounterServiceWithConnection, NotificationProxyServiceWithConnection, PowerAssertionServiceWithConnection, SpringboardServiceWithConnection, SyslogService as SyslogServiceType, WebInspectorServiceWithConnection } from './lib/types.js';
3
+ import AfcService from './services/ios/afc/index.js';
3
4
  export declare function startDiagnosticsService(udid: string): Promise<DiagnosticsServiceWithConnection>;
4
5
  export declare function startNotificationProxyService(udid: string): Promise<NotificationProxyServiceWithConnection>;
5
6
  export declare function startMobileConfigService(udid: string): Promise<MobileConfigServiceWithConnection>;
6
7
  export declare function startMobileImageMounterService(udid: string): Promise<MobileImageMounterServiceWithConnection>;
7
8
  export declare function startSpringboardService(udid: string): Promise<SpringboardServiceWithConnection>;
9
+ export declare function startPowerAssertionService(udid: string): Promise<PowerAssertionServiceWithConnection>;
8
10
  export declare function startSyslogService(udid: string): Promise<SyslogServiceType>;
11
+ /**
12
+ * Start AFC service over RemoteXPC shim.
13
+ * Resolves the AFC service port via RemoteXPC and returns a ready-to-use AfcService instance.
14
+ */
15
+ export declare function startAfcService(udid: string): Promise<AfcService>;
9
16
  export declare function startWebInspectorService(udid: string): Promise<WebInspectorServiceWithConnection>;
10
17
  export declare function createRemoteXPCConnection(udid: string): Promise<{
11
18
  remoteXPC: RemoteXpcConnection;
@@ -1 +1 @@
1
- {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/services.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAGhF,OAAO,KAAK,EACV,gCAAgC,EAChC,iCAAiC,EACjC,uCAAuC,EACvC,sCAAsC,EACtC,gCAAgC,EAChC,aAAa,IAAI,iBAAiB,EAClC,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AAYxB,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sCAAsC,CAAC,CAYjD;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AACD,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,uCAAuC,CAAC,CAYlD;AAED,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAG5B;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,MAAM;;;;;;;;GAO3D"}
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/services.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAGhF,OAAO,KAAK,EACV,gCAAgC,EAChC,iCAAiC,EACjC,uCAAuC,EACvC,sCAAsC,EACtC,mCAAmC,EACnC,gCAAgC,EAChC,aAAa,IAAI,iBAAiB,EAClC,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AACxB,OAAO,UAAU,MAAM,6BAA6B,CAAC;AAarD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sCAAsC,CAAC,CAYjD;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AAED,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,uCAAuC,CAAC,CAYlD;AAED,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gCAAgC,CAAC,CAY3C;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,mCAAmC,CAAC,CAY9C;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAG5B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAOvE;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iCAAiC,CAAC,CAY5C;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,MAAM;;;;;;;;GAO3D"}
@@ -2,10 +2,12 @@ import { strongbox } from '@appium/strongbox';
2
2
  import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
3
3
  import { TunnelManager } from './lib/tunnel/index.js';
4
4
  import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
5
+ import AfcService from './services/ios/afc/index.js';
5
6
  import DiagnosticsService from './services/ios/diagnostic-service/index.js';
6
7
  import { MobileConfigService } from './services/ios/mobile-config/index.js';
7
8
  import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
8
9
  import { NotificationProxyService } from './services/ios/notification-proxy/index.js';
10
+ import { PowerAssertionService } from './services/ios/power-assertion/index.js';
9
11
  import { SpringBoardService } from './services/ios/springboard-service/index.js';
10
12
  import SyslogService from './services/ios/syslog-service/index.js';
11
13
  import { WebInspectorService } from './services/ios/webinspector/index.js';
@@ -66,10 +68,33 @@ export async function startSpringboardService(udid) {
66
68
  ]),
67
69
  };
68
70
  }
71
+ export async function startPowerAssertionService(udid) {
72
+ const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
73
+ const powerAssertionService = remoteXPC.findService(PowerAssertionService.RSD_SERVICE_NAME);
74
+ return {
75
+ remoteXPC: remoteXPC,
76
+ powerAssertionService: new PowerAssertionService([
77
+ tunnelConnection.host,
78
+ parseInt(powerAssertionService.port, 10),
79
+ ]),
80
+ };
81
+ }
69
82
  export async function startSyslogService(udid) {
70
83
  const { tunnelConnection } = await createRemoteXPCConnection(udid);
71
84
  return new SyslogService([tunnelConnection.host, tunnelConnection.port]);
72
85
  }
86
+ /**
87
+ * Start AFC service over RemoteXPC shim.
88
+ * Resolves the AFC service port via RemoteXPC and returns a ready-to-use AfcService instance.
89
+ */
90
+ export async function startAfcService(udid) {
91
+ const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
92
+ const afcDescriptor = remoteXPC.findService(AfcService.RSD_SERVICE_NAME);
93
+ return new AfcService([
94
+ tunnelConnection.host,
95
+ parseInt(afcDescriptor.port, 10),
96
+ ]);
97
+ }
73
98
  export async function startWebInspectorService(udid) {
74
99
  const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
75
100
  const webInspectorService = remoteXPC.findService(WebInspectorService.RSD_SERVICE_NAME);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-remotexpc",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "main": "build/src/index.js",
5
5
  "types": "build/src/index.d.ts",
6
6
  "type": "module",
@@ -33,6 +33,8 @@
33
33
  "test:mobile-config": "mocha test/integration/mobile-config-test.ts --exit --timeout 1m",
34
34
  "test:springboard": "mocha test/integration/springboard-service-test.ts --exit --timeout 1m",
35
35
  "test:webinspector": "mocha test/integration/webinspector-test.ts --exit --timeout 1m",
36
+ "test:afc": "mocha test/integration/afc-test.ts --exit --timeout 1m",
37
+ "test:power-assertion": "mocha test/integration/power-assertion-test.ts --exit --timeout 1m",
36
38
  "test:unit": "mocha 'test/unit/**/*.ts' --exit --timeout 2m",
37
39
  "test:tunnel-creation": "sudo tsx scripts/test-tunnel-creation.ts",
38
40
  "test:tunnel-creation:lsof": "sudo tsx scripts/test-tunnel-creation.ts --keep-open"
package/src/index.ts CHANGED
@@ -17,6 +17,8 @@ export type {
17
17
  MobileImageMounterService,
18
18
  NotificationProxyService,
19
19
  MobileConfigService,
20
+ PowerAssertionService,
21
+ PowerAssertionOptions,
20
22
  SpringboardService,
21
23
  WebInspectorService,
22
24
  SyslogService,
@@ -28,9 +30,11 @@ export type {
28
30
  MobileImageMounterServiceWithConnection,
29
31
  NotificationProxyServiceWithConnection,
30
32
  MobileConfigServiceWithConnection,
33
+ PowerAssertionServiceWithConnection,
31
34
  SpringboardServiceWithConnection,
32
35
  WebInspectorServiceWithConnection,
33
36
  } from './lib/types.js';
37
+ export { PowerAssertionType } from './lib/types.js';
34
38
  export {
35
39
  createUsbmux,
36
40
  Services,
package/src/lib/types.ts CHANGED
@@ -6,10 +6,15 @@ import { EventEmitter } from 'events';
6
6
 
7
7
  import type { ServiceConnection } from '../service-connection.js';
8
8
  import type { BaseService, Service } from '../services/ios/base-service.js';
9
+ import type { PowerAssertionOptions } from '../services/ios/power-assertion/index.js';
10
+ import { PowerAssertionType } from '../services/ios/power-assertion/index.js';
9
11
  import type { InterfaceOrientation } from '../services/ios/springboard-service/index.js';
10
12
  import type { RemoteXpcConnection } from './remote-xpc/remote-xpc-connection.js';
11
13
  import type { Device } from './usbmux/index.js';
12
14
 
15
+ export type { PowerAssertionOptions };
16
+ export { PowerAssertionType };
17
+
13
18
  /**
14
19
  * Represents a value that can be stored in a plist
15
20
  */
@@ -213,6 +218,24 @@ export interface NotificationProxyService extends BaseService {
213
218
  expectNotification(timeout?: number): Promise<PlistMessage>;
214
219
  }
215
220
 
221
+ /**
222
+ * Represents the PowerAssertionService for preventing system sleep
223
+ */
224
+ export interface PowerAssertionService extends BaseService {
225
+ /**
226
+ * Create a power assertion to prevent system sleep
227
+ * @param options Options for creating the power assertion
228
+ * @returns Promise that resolves when the assertion is created
229
+ */
230
+ createPowerAssertion(options: PowerAssertionOptions): Promise<void>;
231
+
232
+ /**
233
+ * Close the connection to the power assertion service
234
+ * @returns Promise that resolves when the connection is closed
235
+ */
236
+ close(): Promise<void>;
237
+ }
238
+
216
239
  /**
217
240
  * Represents the static side of MobileConfigService
218
241
  */
@@ -312,6 +335,17 @@ export interface MobileConfigServiceWithConnection {
312
335
  remoteXPC: RemoteXpcConnection;
313
336
  }
314
337
 
338
+ /**
339
+ * Represents a PowerAssertionService instance with its associated RemoteXPC connection
340
+ * This allows callers to properly manage the connection lifecycle
341
+ */
342
+ export interface PowerAssertionServiceWithConnection {
343
+ /** The PowerAssertionService instance */
344
+ powerAssertionService: PowerAssertionService;
345
+ /** The RemoteXPC connection that can be used to close the connection */
346
+ remoteXPC: RemoteXpcConnection;
347
+ }
348
+
315
349
  /**
316
350
  * Represents the WebInspectorService
317
351
  */
@@ -2,8 +2,10 @@ import {
2
2
  TunnelRegistryServer,
3
3
  startTunnelRegistryServer,
4
4
  } from '../lib/tunnel/tunnel-registry-server.js';
5
+ import * as afc from './ios/afc/index.js';
5
6
  import * as diagnostics from './ios/diagnostic-service/index.js';
6
7
  import * as mobileImageMounter from './ios/mobile-image-mounter/index.js';
8
+ import * as powerAssertion from './ios/power-assertion/index.js';
7
9
  import * as syslog from './ios/syslog-service/index.js';
8
10
  import * as tunnel from './ios/tunnel-service/index.js';
9
11
  import * as webinspector from './ios/webinspector/index.js';
@@ -11,8 +13,10 @@ import * as webinspector from './ios/webinspector/index.js';
11
13
  export {
12
14
  diagnostics,
13
15
  mobileImageMounter,
16
+ powerAssertion,
14
17
  syslog,
15
18
  tunnel,
19
+ afc,
16
20
  webinspector,
17
21
  TunnelRegistryServer,
18
22
  startTunnelRegistryServer,
@@ -0,0 +1,365 @@
1
+ import net from 'node:net';
2
+
3
+ import { createPlist } from '../../../lib/plist/plist-creator.js';
4
+ import { parsePlist } from '../../../lib/plist/unified-plist-parser.js';
5
+ import { AFCMAGIC, AFC_HEADER_SIZE, NULL_BYTE } from './constants.js';
6
+ import { AfcError, AfcFopenMode, AfcOpcode } from './enums.js';
7
+
8
+ export interface AfcHeader {
9
+ magic: Buffer;
10
+ entireLength: bigint;
11
+ thisLength: bigint;
12
+ packetNum: bigint;
13
+ operation: bigint;
14
+ }
15
+
16
+ export interface AfcResponse {
17
+ status: AfcError;
18
+ data: Buffer;
19
+ operation: AfcOpcode;
20
+ rawHeader: AfcHeader;
21
+ }
22
+
23
+ export function writeUInt64LE(value: bigint | number): Buffer {
24
+ const buf = Buffer.alloc(8);
25
+ buf.writeBigUInt64LE(BigInt(value), 0);
26
+ return buf;
27
+ }
28
+
29
+ export function readUInt64LE(buf: Buffer, offset = 0): bigint {
30
+ return buf.readBigUInt64LE(offset);
31
+ }
32
+
33
+ export function cstr(str: string): Buffer {
34
+ const s = Buffer.from(str, 'utf8');
35
+ return Buffer.concat([s, NULL_BYTE]);
36
+ }
37
+
38
+ export function encodeHeader(
39
+ op: AfcOpcode,
40
+ packetNum: bigint,
41
+ payloadLen: number,
42
+ thisLenOverride?: number,
43
+ ): Buffer {
44
+ const entireLen = BigInt(AFC_HEADER_SIZE + payloadLen);
45
+ const thisLen = BigInt(thisLenOverride ?? AFC_HEADER_SIZE + payloadLen);
46
+
47
+ const header = Buffer.alloc(AFC_HEADER_SIZE);
48
+ // magic
49
+ AFCMAGIC.copy(header, 0);
50
+ // entire_length
51
+ writeUInt64LE(entireLen).copy(header, 8);
52
+ // this_length
53
+ writeUInt64LE(thisLen).copy(header, 16);
54
+ // packet_num
55
+ writeUInt64LE(packetNum).copy(header, 24);
56
+ // operation
57
+ writeUInt64LE(BigInt(op)).copy(header, 32);
58
+
59
+ return header;
60
+ }
61
+
62
+ /**
63
+ * Internal per-socket buffered reader to avoid re-emitting data and race conditions.
64
+ */
65
+ type SocketWaiter = {
66
+ n: number;
67
+ resolve: (buf: Buffer) => void;
68
+ reject: (err: Error) => void;
69
+ timer?: NodeJS.Timeout;
70
+ };
71
+ type SocketState = {
72
+ buffer: Buffer;
73
+ waiters: SocketWaiter[];
74
+ onData: (chunk: Buffer) => void;
75
+ onError: (err: Error) => void;
76
+ onClose: () => void;
77
+ };
78
+
79
+ const SOCKET_STATES = new WeakMap<net.Socket, SocketState>();
80
+
81
+ function cleanupSocketState(socket: net.Socket, error?: Error): void {
82
+ const state = SOCKET_STATES.get(socket);
83
+ if (!state) {
84
+ return;
85
+ }
86
+
87
+ // Remove all event listeners to prevent memory leaks
88
+ socket.removeListener('data', state.onData);
89
+ socket.removeListener('error', state.onError);
90
+ socket.removeListener('close', state.onClose);
91
+ socket.removeListener('end', state.onClose);
92
+
93
+ // Reject any pending waiters
94
+ const err = error || new Error('Socket closed');
95
+ while (state.waiters.length) {
96
+ const w = state.waiters.shift()!;
97
+ if (w.timer) {
98
+ clearTimeout(w.timer);
99
+ }
100
+ w.reject(err);
101
+ }
102
+
103
+ // Remove from WeakMap
104
+ SOCKET_STATES.delete(socket);
105
+ }
106
+
107
+ function ensureSocketState(socket: net.Socket): SocketState {
108
+ let state = SOCKET_STATES.get(socket);
109
+ if (state) {
110
+ return state;
111
+ }
112
+
113
+ state = {
114
+ buffer: Buffer.alloc(0),
115
+ waiters: [],
116
+ onData: (chunk: Buffer) => {
117
+ const st = SOCKET_STATES.get(socket);
118
+ if (!st) {
119
+ return;
120
+ }
121
+ st.buffer = Buffer.concat([st.buffer, chunk]);
122
+
123
+ while (st.waiters.length && st.buffer.length >= st.waiters[0].n) {
124
+ const w = st.waiters.shift()!;
125
+ const out = st.buffer.subarray(0, w.n);
126
+ st.buffer = st.buffer.subarray(w.n);
127
+ if (w.timer) {
128
+ clearTimeout(w.timer);
129
+ }
130
+ w.resolve(out);
131
+ }
132
+ },
133
+ onError: (err: Error) => {
134
+ cleanupSocketState(socket, err);
135
+ },
136
+ onClose: () => {
137
+ cleanupSocketState(socket);
138
+ },
139
+ };
140
+
141
+ socket.on('data', state.onData);
142
+ socket.once('error', state.onError);
143
+ socket.once('close', state.onClose);
144
+ socket.once('end', state.onClose);
145
+ SOCKET_STATES.set(socket, state);
146
+ return state;
147
+ }
148
+
149
+ export async function readExact(
150
+ socket: net.Socket,
151
+ n: number,
152
+ timeoutMs = 30000,
153
+ ): Promise<Buffer> {
154
+ const state = ensureSocketState(socket);
155
+
156
+ if (state.buffer.length >= n) {
157
+ const out = state.buffer.subarray(0, n);
158
+ state.buffer = state.buffer.subarray(n);
159
+ return out;
160
+ }
161
+
162
+ return await new Promise<Buffer>((resolve, reject) => {
163
+ const waiter: SocketWaiter = { n, resolve, reject };
164
+ state.waiters.push(waiter);
165
+ waiter.timer = setTimeout(() => {
166
+ const idx = state.waiters.indexOf(waiter);
167
+ if (idx >= 0) {
168
+ state.waiters.splice(idx, 1);
169
+ reject(new Error(`readExact timeout after ${timeoutMs}ms`));
170
+ }
171
+ }, timeoutMs);
172
+ });
173
+ }
174
+
175
+ export async function readAfcHeader(socket: net.Socket): Promise<AfcHeader> {
176
+ const buf = await readExact(socket, AFC_HEADER_SIZE);
177
+ const magic = buf.subarray(0, 8);
178
+ if (!magic.equals(AFCMAGIC)) {
179
+ throw new Error(`Invalid AFC magic: ${magic.toString('hex')}`);
180
+ }
181
+ const entireLength = readUInt64LE(buf, 8);
182
+ const thisLength = readUInt64LE(buf, 16);
183
+ const packetNum = readUInt64LE(buf, 24);
184
+ const operation = readUInt64LE(buf, 32);
185
+ return {
186
+ magic,
187
+ entireLength,
188
+ thisLength,
189
+ packetNum,
190
+ operation,
191
+ };
192
+ }
193
+
194
+ export async function readAfcResponse(
195
+ socket: net.Socket,
196
+ ): Promise<AfcResponse> {
197
+ const header = await readAfcHeader(socket);
198
+ const payloadLen = Number(header.entireLength - BigInt(AFC_HEADER_SIZE));
199
+ const payload =
200
+ payloadLen > 0 ? await readExact(socket, payloadLen) : Buffer.alloc(0);
201
+ const op = Number(header.operation) as AfcOpcode;
202
+
203
+ if (op === AfcOpcode.STATUS) {
204
+ const status = Number(readUInt64LE(payload.subarray(0, 8))) as AfcError;
205
+ return { status, data: Buffer.alloc(0), operation: op, rawHeader: header };
206
+ }
207
+
208
+ return {
209
+ status: AfcError.SUCCESS,
210
+ data: payload,
211
+ operation: op,
212
+ rawHeader: header,
213
+ };
214
+ }
215
+
216
+ export async function sendAfcPacket(
217
+ socket: net.Socket,
218
+ op: AfcOpcode,
219
+ packetNum: bigint,
220
+ payload: Buffer = Buffer.alloc(0),
221
+ thisLenOverride?: number,
222
+ ): Promise<void> {
223
+ const header = encodeHeader(op, packetNum, payload.length, thisLenOverride);
224
+ await new Promise<void>((resolve, reject) => {
225
+ socket.write(header, (err) => {
226
+ if (err) {
227
+ return reject(err);
228
+ }
229
+ if (payload.length) {
230
+ socket.write(payload, (err2) => {
231
+ if (err2) {
232
+ return reject(err2);
233
+ }
234
+ resolve();
235
+ });
236
+ } else {
237
+ resolve();
238
+ }
239
+ });
240
+ });
241
+ }
242
+
243
+ export function parseCStringArray(buf: Buffer): string[] {
244
+ const parts: string[] = [];
245
+ let start = 0;
246
+ for (let i = 0; i < buf.length; i++) {
247
+ if (buf[i] === 0x00) {
248
+ const slice = buf.subarray(start, i);
249
+ parts.push(slice.toString('utf8'));
250
+ start = i + 1;
251
+ }
252
+ }
253
+ if (start < buf.length) {
254
+ parts.push(buf.subarray(start).toString('utf8'));
255
+ }
256
+ while (parts.length && parts[parts.length - 1] === '') {
257
+ parts.pop();
258
+ }
259
+ return parts;
260
+ }
261
+
262
+ export function parseKeyValueNullList(buf: Buffer): Record<string, string> {
263
+ const arr = parseCStringArray(buf);
264
+ if (arr.length % 2 !== 0) {
265
+ throw new Error('Invalid key/value AFC list (odd number of entries)');
266
+ }
267
+ return Object.fromEntries(
268
+ Array.from({ length: arr.length / 2 }, (_, i) => [
269
+ arr[i * 2],
270
+ arr[i * 2 + 1],
271
+ ]),
272
+ );
273
+ }
274
+
275
+ export function buildFopenPayload(mode: AfcFopenMode, path: string): Buffer {
276
+ return Buffer.concat([writeUInt64LE(mode), cstr(path)]);
277
+ }
278
+
279
+ export function buildReadPayload(
280
+ handle: bigint | number,
281
+ size: bigint | number,
282
+ ): Buffer {
283
+ return Buffer.concat([writeUInt64LE(handle), writeUInt64LE(BigInt(size))]);
284
+ }
285
+
286
+ export function buildClosePayload(handle: bigint | number): Buffer {
287
+ return writeUInt64LE(handle);
288
+ }
289
+
290
+ export function buildRemovePayload(path: string): Buffer {
291
+ return cstr(path);
292
+ }
293
+
294
+ export function buildMkdirPayload(path: string): Buffer {
295
+ return cstr(path);
296
+ }
297
+
298
+ export function buildStatPayload(path: string): Buffer {
299
+ return cstr(path);
300
+ }
301
+
302
+ export function buildRenamePayload(src: string, dst: string): Buffer {
303
+ return Buffer.concat([cstr(src), cstr(dst)]);
304
+ }
305
+
306
+ export function buildLinkPayload(
307
+ type: number,
308
+ target: string,
309
+ source: string,
310
+ ): Buffer {
311
+ return Buffer.concat([writeUInt64LE(type), cstr(target), cstr(source)]);
312
+ }
313
+
314
+ /**
315
+ * Receive a single length-prefixed plist from the socket
316
+ */
317
+ export async function recvOnePlist(socket: net.Socket): Promise<any> {
318
+ const lenBuf = await readExact(socket, 4);
319
+ const respLen = lenBuf.readUInt32BE(0);
320
+ const respBody = await readExact(socket, respLen);
321
+ return parsePlist(respBody);
322
+ }
323
+
324
+ export async function rsdHandshakeForRawService(
325
+ socket: net.Socket,
326
+ ): Promise<void> {
327
+ const request = {
328
+ Label: 'appium-internal',
329
+ ProtocolVersion: '2',
330
+ Request: 'RSDCheckin',
331
+ };
332
+ const xml = createPlist(request);
333
+ const body = Buffer.from(xml, 'utf8');
334
+ const header = Buffer.alloc(4);
335
+ header.writeUInt32BE(body.length, 0);
336
+ await new Promise<void>((resolve, reject) => {
337
+ socket.write(Buffer.concat([header, body]), (err) =>
338
+ err ? reject(err) : resolve(),
339
+ );
340
+ });
341
+
342
+ const first = await recvOnePlist(socket);
343
+ if (!first || first.Request !== 'RSDCheckin') {
344
+ throw new Error(`Invalid RSDCheckin response: ${JSON.stringify(first)}`);
345
+ }
346
+
347
+ const second = await recvOnePlist(socket);
348
+ if (!second || second.Request !== 'StartService') {
349
+ throw new Error(`Invalid StartService response: ${JSON.stringify(second)}`);
350
+ }
351
+ }
352
+
353
+ export function nextReadChunkSize(left: bigint | number): number {
354
+ const leftNum = typeof left === 'bigint' ? Number(left) : left;
355
+ return leftNum;
356
+ }
357
+
358
+ /**
359
+ * Convert nanoseconds to milliseconds for Date construction
360
+ * @param nanoseconds - Time value in nanoseconds as a string
361
+ * @returns Time value in milliseconds
362
+ */
363
+ export function nanosecondsToMilliseconds(nanoseconds: string): number {
364
+ return Number(BigInt(nanoseconds) / 1000000n);
365
+ }
@@ -0,0 +1,29 @@
1
+ import { AfcFopenMode } from './enums.js';
2
+
3
+ /**
4
+ * AFC protocol constants
5
+ */
6
+
7
+ // Magic bytes at start of every AFC header
8
+ export const AFCMAGIC = Buffer.from('CFA6LPAA', 'ascii');
9
+
10
+ // IO chunk sizes
11
+ export const MAXIMUM_READ_SIZE = 4 * 1024 * 1024; // 4 MiB
12
+
13
+ // Mapping of textual fopen modes to AFC modes
14
+ export const AFC_FOPEN_TEXTUAL_MODES: Record<string, AfcFopenMode> = {
15
+ r: AfcFopenMode.RDONLY,
16
+ 'r+': AfcFopenMode.RW,
17
+ w: AfcFopenMode.WRONLY,
18
+ 'w+': AfcFopenMode.WR,
19
+ a: AfcFopenMode.APPEND,
20
+ 'a+': AfcFopenMode.RDAPPEND,
21
+ };
22
+
23
+ // Header size: magic (8) + entire_length (8) + this_length (8) + packet_num (8) + operation (8)
24
+ export const AFC_HEADER_SIZE = 40;
25
+
26
+ // Override for WRITE packets' this_length
27
+ export const AFC_WRITE_THIS_LENGTH = 48;
28
+
29
+ export const NULL_BYTE = Buffer.from([0]);