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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/build/src/index.d.ts +2 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +1 -0
- package/build/src/lib/types.d.ts +30 -0
- package/build/src/lib/types.d.ts.map +1 -1
- package/build/src/lib/types.js +2 -0
- package/build/src/services/index.d.ts +3 -1
- package/build/src/services/index.d.ts.map +1 -1
- package/build/src/services/index.js +3 -1
- package/build/src/services/ios/afc/codec.d.ts +46 -0
- package/build/src/services/ios/afc/codec.d.ts.map +1 -0
- package/build/src/services/ios/afc/codec.js +263 -0
- package/build/src/services/ios/afc/constants.d.ts +11 -0
- package/build/src/services/ios/afc/constants.d.ts.map +1 -0
- package/build/src/services/ios/afc/constants.js +22 -0
- package/build/src/services/ios/afc/enums.d.ts +66 -0
- package/build/src/services/ios/afc/enums.d.ts.map +1 -0
- package/build/src/services/ios/afc/enums.js +70 -0
- package/build/src/services/ios/afc/index.d.ts +72 -0
- package/build/src/services/ios/afc/index.d.ts.map +1 -0
- package/build/src/services/ios/afc/index.js +385 -0
- package/build/src/services/ios/afc/stream-utils.d.ts +14 -0
- package/build/src/services/ios/afc/stream-utils.d.ts.map +1 -0
- package/build/src/services/ios/afc/stream-utils.js +60 -0
- package/build/src/services/ios/power-assertion/index.d.ts +40 -0
- package/build/src/services/ios/power-assertion/index.d.ts.map +1 -0
- package/build/src/services/ios/power-assertion/index.js +64 -0
- package/build/src/services.d.ts +8 -1
- package/build/src/services.d.ts.map +1 -1
- package/build/src/services.js +25 -0
- package/package.json +3 -1
- package/src/index.ts +4 -0
- package/src/lib/types.ts +34 -0
- package/src/services/index.ts +4 -0
- package/src/services/ios/afc/codec.ts +365 -0
- package/src/services/ios/afc/constants.ts +29 -0
- package/src/services/ios/afc/enums.ts +70 -0
- package/src/services/ios/afc/index.ts +511 -0
- package/src/services/ios/afc/stream-utils.ts +102 -0
- package/src/services/ios/power-assertion/index.ts +100 -0
- 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 };
|
package/build/src/services.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/build/src/services.js
CHANGED
|
@@ -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.
|
|
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
|
*/
|
package/src/services/index.ts
CHANGED
|
@@ -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]);
|