appium-ios-remotexpc 0.13.2 → 0.14.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 (55) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/src/index.d.ts +1 -1
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/lib/plist/binary-plist-creator.d.ts.map +1 -1
  5. package/build/src/lib/plist/binary-plist-creator.js +30 -0
  6. package/build/src/lib/plist/index.d.ts +1 -0
  7. package/build/src/lib/plist/index.d.ts.map +1 -1
  8. package/build/src/lib/plist/index.js +1 -0
  9. package/build/src/lib/plist/plist-uid.d.ts +10 -0
  10. package/build/src/lib/plist/plist-uid.d.ts.map +1 -0
  11. package/build/src/lib/plist/plist-uid.js +10 -0
  12. package/build/src/lib/types.d.ts +165 -2
  13. package/build/src/lib/types.d.ts.map +1 -1
  14. package/build/src/services/ios/dvt/channel-fragmenter.d.ts +21 -0
  15. package/build/src/services/ios/dvt/channel-fragmenter.d.ts.map +1 -0
  16. package/build/src/services/ios/dvt/channel-fragmenter.js +37 -0
  17. package/build/src/services/ios/dvt/channel.d.ts +32 -0
  18. package/build/src/services/ios/dvt/channel.d.ts.map +1 -0
  19. package/build/src/services/ios/dvt/channel.js +44 -0
  20. package/build/src/services/ios/dvt/dtx-message.d.ts +88 -0
  21. package/build/src/services/ios/dvt/dtx-message.d.ts.map +1 -0
  22. package/build/src/services/ios/dvt/dtx-message.js +113 -0
  23. package/build/src/services/ios/dvt/index.d.ts +119 -0
  24. package/build/src/services/ios/dvt/index.d.ts.map +1 -0
  25. package/build/src/services/ios/dvt/index.js +552 -0
  26. package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts +37 -0
  27. package/build/src/services/ios/dvt/instruments/condition-inducer.d.ts.map +1 -0
  28. package/build/src/services/ios/dvt/instruments/condition-inducer.js +99 -0
  29. package/build/src/services/ios/dvt/instruments/location-simulation.d.ts +43 -0
  30. package/build/src/services/ios/dvt/instruments/location-simulation.d.ts.map +1 -0
  31. package/build/src/services/ios/dvt/instruments/location-simulation.js +60 -0
  32. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts +41 -0
  33. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.d.ts.map +1 -0
  34. package/build/src/services/ios/dvt/nskeyedarchiver-decoder.js +190 -0
  35. package/build/src/services/ios/dvt/utils.d.ts +19 -0
  36. package/build/src/services/ios/dvt/utils.d.ts.map +1 -0
  37. package/build/src/services/ios/dvt/utils.js +67 -0
  38. package/build/src/services.d.ts +2 -1
  39. package/build/src/services.d.ts.map +1 -1
  40. package/build/src/services.js +23 -0
  41. package/package.json +4 -1
  42. package/src/index.ts +6 -0
  43. package/src/lib/plist/binary-plist-creator.ts +30 -0
  44. package/src/lib/plist/index.ts +2 -0
  45. package/src/lib/plist/plist-uid.ts +9 -0
  46. package/src/lib/types.ts +179 -1
  47. package/src/services/ios/dvt/channel-fragmenter.ts +42 -0
  48. package/src/services/ios/dvt/channel.ts +58 -0
  49. package/src/services/ios/dvt/dtx-message.ts +162 -0
  50. package/src/services/ios/dvt/index.ts +727 -0
  51. package/src/services/ios/dvt/instruments/condition-inducer.ts +140 -0
  52. package/src/services/ios/dvt/instruments/location-simulation.ts +83 -0
  53. package/src/services/ios/dvt/nskeyedarchiver-decoder.ts +219 -0
  54. package/src/services/ios/dvt/utils.ts +89 -0
  55. package/src/services.ts +33 -0
@@ -0,0 +1,140 @@
1
+ import { getLogger } from '../../../../lib/logger.js';
2
+ import type { ConditionGroup } from '../../../../lib/types.js';
3
+ import type { Channel } from '../channel.js';
4
+ import { MessageAux } from '../dtx-message.js';
5
+ import type { DVTSecureSocketProxyService } from '../index.js';
6
+
7
+ const log = getLogger('ConditionInducer');
8
+
9
+ /**
10
+ * Condition Inducer service for simulating various device conditions
11
+ * such as network conditions, thermal states, etc.
12
+ */
13
+ export class ConditionInducer {
14
+ static readonly IDENTIFIER =
15
+ 'com.apple.instruments.server.services.ConditionInducer';
16
+
17
+ private channel: Channel | null = null;
18
+
19
+ constructor(private readonly dvt: DVTSecureSocketProxyService) {}
20
+
21
+ /**
22
+ * Initialize the condition inducer channel
23
+ */
24
+ async initialize(): Promise<void> {
25
+ if (this.channel) {
26
+ return;
27
+ }
28
+ this.channel = await this.dvt.makeChannel(ConditionInducer.IDENTIFIER);
29
+ }
30
+
31
+ /**
32
+ * List all available condition inducers and their profiles
33
+ * @returns Array of condition groups with their available profiles
34
+ */
35
+ async list(): Promise<ConditionGroup[]> {
36
+ await this.initialize();
37
+
38
+ await this.channel!.call('availableConditionInducers')();
39
+ const result = await this.channel!.receivePlist();
40
+
41
+ // Handle different response formats
42
+ if (!result) {
43
+ log.warn(
44
+ 'Received null/undefined response from availableConditionInducers',
45
+ );
46
+ return [];
47
+ }
48
+
49
+ // If result is already an array, return it
50
+ if (Array.isArray(result)) {
51
+ return result as ConditionGroup[];
52
+ }
53
+
54
+ throw new Error(
55
+ `Unexpected response format from availableConditionInducers: ${JSON.stringify(result)}`,
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Set a specific condition profile
61
+ * @param profileIdentifier The identifier of the profile to enable
62
+ * @throws Error if the profile identifier is not found
63
+ * @throws Error if a condition is already active
64
+ */
65
+ async set(profileIdentifier: string): Promise<void> {
66
+ await this.initialize();
67
+
68
+ const groups = await this.list();
69
+
70
+ // Find the profile in the available groups
71
+ for (const group of groups) {
72
+ const profiles = group.profiles || [];
73
+ for (const profile of profiles) {
74
+ if (profileIdentifier !== profile.identifier) {
75
+ continue;
76
+ }
77
+
78
+ log.info(
79
+ `Enabling condition: ${profile.description || profile.identifier}`,
80
+ );
81
+
82
+ const args = new MessageAux()
83
+ .appendObj(group.identifier)
84
+ .appendObj(profile.identifier);
85
+
86
+ await this.channel!.call(
87
+ 'enableConditionWithIdentifier_profileIdentifier_',
88
+ )(args);
89
+
90
+ // Wait for response which may be a raised NSError
91
+ await this.channel!.receivePlist();
92
+
93
+ log.info(
94
+ `Successfully enabled condition profile: ${profileIdentifier}`,
95
+ );
96
+ return;
97
+ }
98
+ }
99
+
100
+ const availableProfiles = groups.flatMap((group) =>
101
+ (group.profiles || []).map((p) => p.identifier),
102
+ );
103
+
104
+ throw new Error(
105
+ `Invalid profile identifier: ${profileIdentifier}. Available profiles: ${availableProfiles.join(', ')}`,
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Disable the currently active condition
111
+ *
112
+ * Note: This method is idempotent - calling it when no condition is active
113
+ * will not throw an error.
114
+ */
115
+ async disable(): Promise<void> {
116
+ await this.initialize();
117
+
118
+ await this.channel!.call('disableActiveCondition')();
119
+ const response = await this.channel!.receivePlist();
120
+
121
+ // Response can be:
122
+ // - true (successfully disabled condition)
123
+ // - NSError object, when no condition is active
124
+ if (response === true) {
125
+ log.info('Disabled active condition');
126
+ } else if (this.isNSError(response)) {
127
+ log.debug('No active condition to disable');
128
+ } else {
129
+ throw new Error(
130
+ `Unexpected response from disableActiveCondition: ${JSON.stringify(response)}`,
131
+ );
132
+ }
133
+ }
134
+
135
+ private isNSError(obj: any): boolean {
136
+ return ['NSCode', 'NSUserInfo', 'NSDomain'].some(
137
+ (prop) => obj?.[prop] !== undefined,
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,83 @@
1
+ import { getLogger } from '../../../../lib/logger.js';
2
+ import type { Channel } from '../channel.js';
3
+ import { MessageAux } from '../dtx-message.js';
4
+ import type { DVTSecureSocketProxyService } from '../index.js';
5
+
6
+ const log = getLogger('LocationSimulation');
7
+
8
+ /**
9
+ * Geographic coordinates
10
+ */
11
+ export interface LocationCoordinates {
12
+ latitude: number;
13
+ longitude: number;
14
+ }
15
+
16
+ /**
17
+ * Location simulation service for simulating device GPS location
18
+ */
19
+ export class LocationSimulation {
20
+ static readonly IDENTIFIER =
21
+ 'com.apple.instruments.server.services.LocationSimulation';
22
+
23
+ private channel: Channel | null = null;
24
+
25
+ constructor(private readonly dvt: DVTSecureSocketProxyService) {}
26
+
27
+ /**
28
+ * Initialize the location simulation channel
29
+ */
30
+ async initialize(): Promise<void> {
31
+ if (this.channel) {
32
+ return;
33
+ }
34
+
35
+ this.channel = await this.dvt.makeChannel(LocationSimulation.IDENTIFIER);
36
+ }
37
+
38
+ /**
39
+ * Set the simulated GPS location
40
+ * @param coordinates The location coordinates
41
+ */
42
+ async set(coordinates: LocationCoordinates): Promise<void> {
43
+ await this.initialize();
44
+
45
+ const args = new MessageAux()
46
+ .appendObj(coordinates.latitude)
47
+ .appendObj(coordinates.longitude);
48
+
49
+ await this.channel!.call('simulateLocationWithLatitude_longitude_')(args);
50
+ await this.channel!.receivePlist();
51
+
52
+ log.info(
53
+ `Location set to: ${coordinates.latitude}, ${coordinates.longitude}`,
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Set the simulated GPS location
59
+ * @param latitude The latitude coordinate
60
+ * @param longitude The longitude coordinate
61
+ */
62
+ async setLocation(latitude: number, longitude: number): Promise<void> {
63
+ await this.set({ latitude, longitude });
64
+ }
65
+
66
+ /**
67
+ * Stop location simulation and restore the actual device location
68
+ *
69
+ * Note: This method is safe to call even if no location simulation is currently active.
70
+ */
71
+ async clear(): Promise<void> {
72
+ await this.initialize();
73
+ await this.channel!.call('stopLocationSimulation')();
74
+ log.info('Location simulation stopped');
75
+ }
76
+
77
+ /**
78
+ * Stop location simulation (alias for clear)
79
+ */
80
+ async stop(): Promise<void> {
81
+ await this.clear();
82
+ }
83
+ }
@@ -0,0 +1,219 @@
1
+ import { getLogger } from '../../../lib/logger.js';
2
+
3
+ const log = getLogger('NSKeyedArchiverDecoder');
4
+
5
+ /**
6
+ * Decode NSKeyedArchiver formatted data into native JavaScript objects
7
+ *
8
+ * NSKeyedArchiver is Apple's serialization format that stores object graphs
9
+ * with references. The format includes:
10
+ * - $version: Archive version (typically 100000)
11
+ * - $archiver: "NSKeyedArchiver"
12
+ * - $top: Root object references
13
+ * - $objects: Array of all objects with cross-references
14
+ */
15
+ export class NSKeyedArchiverDecoder {
16
+ private readonly objects: any[];
17
+ private readonly decoded: Map<number, any>;
18
+ private readonly archive: any;
19
+
20
+ constructor(data: any) {
21
+ if (!NSKeyedArchiverDecoder.isNSKeyedArchive(data)) {
22
+ throw new Error('Data is not in NSKeyedArchiver format');
23
+ }
24
+
25
+ this.archive = data;
26
+ this.objects = data.$objects || [];
27
+ this.decoded = new Map();
28
+ }
29
+
30
+ /**
31
+ * Check if data is in NSKeyedArchiver format
32
+ */
33
+ static isNSKeyedArchive(data: any): boolean {
34
+ if (!data || typeof data !== 'object') {
35
+ return false;
36
+ }
37
+
38
+ return (
39
+ '$archiver' in data &&
40
+ data.$archiver === 'NSKeyedArchiver' &&
41
+ '$objects' in data &&
42
+ Array.isArray(data.$objects)
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Decode the entire archive starting from the root
48
+ */
49
+ decode(): any {
50
+ if (!this.objects || this.objects.length === 0) {
51
+ return null;
52
+ }
53
+
54
+ // Extract root reference from $top
55
+ let rootIndex: number | null = null;
56
+
57
+ if (this.archive.$top && typeof this.archive.$top === 'object') {
58
+ const top = this.archive.$top;
59
+ if ('root' in top) {
60
+ const root = top.root;
61
+ if (typeof root === 'number') {
62
+ rootIndex = root;
63
+ } else if (typeof root === 'object' && root && 'CF$UID' in root) {
64
+ rootIndex = (root as any).CF$UID;
65
+ }
66
+ }
67
+ }
68
+
69
+ // If we found the root index, decode it
70
+ if (rootIndex !== null) {
71
+ return this.decodeObject(rootIndex);
72
+ }
73
+
74
+ // Fallback: decode first non-null object
75
+ log.warn('Could not find root reference, using fallback');
76
+ return this.decodeObject(1);
77
+ }
78
+
79
+ /**
80
+ * Decode an object at a specific index
81
+ */
82
+ private decodeObject(index: number): any {
83
+ if (index < 0 || index >= this.objects.length) {
84
+ return null;
85
+ }
86
+
87
+ // Check cache
88
+ if (this.decoded.has(index)) {
89
+ return this.decoded.get(index);
90
+ }
91
+
92
+ const obj = this.objects[index];
93
+
94
+ // Handle null marker
95
+ if (obj === '$null' || obj === null) {
96
+ return null;
97
+ }
98
+
99
+ // Handle primitive types
100
+ if (typeof obj !== 'object') {
101
+ return obj;
102
+ }
103
+
104
+ // Handle UID references
105
+ if ('CF$UID' in obj) {
106
+ return this.decodeObject(obj.CF$UID);
107
+ }
108
+
109
+ // Handle NSDictionary (NS.keys + NS.objects) - check this FIRST before NSArray
110
+ if ('NS.keys' in obj && 'NS.objects' in obj) {
111
+ const result = this.decodeDictionary(obj['NS.keys'], obj['NS.objects']);
112
+ this.decoded.set(index, result);
113
+ return result;
114
+ }
115
+
116
+ // Handle NSArray (NS.objects only, without NS.keys)
117
+ if ('NS.objects' in obj) {
118
+ const result = this.decodeArray(obj['NS.objects']);
119
+ this.decoded.set(index, result);
120
+ return result;
121
+ }
122
+
123
+ // Handle regular objects - just return as-is but resolve references
124
+ const result: any = {};
125
+ for (const [key, value] of Object.entries(obj)) {
126
+ if (key === '$class') {
127
+ continue; // Skip class metadata
128
+ }
129
+
130
+ if (typeof value === 'number') {
131
+ // Could be a reference or primitive
132
+ const referenced = this.objects[value];
133
+ if (
134
+ referenced &&
135
+ typeof referenced === 'object' &&
136
+ referenced !== '$null'
137
+ ) {
138
+ result[key] = this.decodeObject(value);
139
+ } else {
140
+ result[key] = value;
141
+ }
142
+ } else if (typeof value === 'object' && value && 'CF$UID' in value) {
143
+ const uid = (value as any).CF$UID;
144
+ if (typeof uid === 'number') {
145
+ result[key] = this.decodeObject(uid);
146
+ } else {
147
+ result[key] = value;
148
+ }
149
+ } else {
150
+ result[key] = value;
151
+ }
152
+ }
153
+
154
+ this.decoded.set(index, result);
155
+ return result;
156
+ }
157
+
158
+ /**
159
+ * Decode an NSArray
160
+ */
161
+ private decodeArray(refs: any): any[] {
162
+ if (!Array.isArray(refs)) {
163
+ return [];
164
+ }
165
+
166
+ return refs.map((ref) => {
167
+ if (typeof ref === 'number') {
168
+ return this.decodeObject(ref);
169
+ } else if (typeof ref === 'object' && ref && 'CF$UID' in ref) {
170
+ return this.decodeObject(ref.CF$UID);
171
+ }
172
+ return ref;
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Decode an NSDictionary
178
+ */
179
+ private decodeDictionary(keyRefs: any, valueRefs: any): any {
180
+ if (!Array.isArray(keyRefs) || !Array.isArray(valueRefs)) {
181
+ return {};
182
+ }
183
+
184
+ const result: any = {};
185
+
186
+ for (let i = 0; i < keyRefs.length && i < valueRefs.length; i++) {
187
+ const key = this.decodeObject(keyRefs[i]);
188
+ const value = this.decodeObject(valueRefs[i]);
189
+
190
+ if (typeof key === 'string') {
191
+ result[key] = value;
192
+ }
193
+ }
194
+
195
+ return result;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Decode NSKeyedArchiver data or return as-is if not archived
201
+ */
202
+ export function decodeNSKeyedArchiver(data: any): any {
203
+ if (!data) {
204
+ return data;
205
+ }
206
+
207
+ // Check if this is NSKeyedArchiver format
208
+ if (!NSKeyedArchiverDecoder.isNSKeyedArchive(data)) {
209
+ return data;
210
+ }
211
+
212
+ try {
213
+ const decoder = new NSKeyedArchiverDecoder(data);
214
+ return decoder.decode();
215
+ } catch (error) {
216
+ log.warn('Failed to decode NSKeyedArchiver data:', error);
217
+ return data;
218
+ }
219
+ }
@@ -0,0 +1,89 @@
1
+ import type { PlistDictionary } from '../../../lib/types.js';
2
+
3
+ export function isPlainObject(value: any): value is Record<string, any> {
4
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+
7
+ export function hasProperties(obj: any, ...props: string[]): boolean {
8
+ return isPlainObject(obj) ? props.every((prop) => prop in obj) : false;
9
+ }
10
+
11
+ export function isNSKeyedArchiverFormat(data: any): boolean {
12
+ return (
13
+ hasProperties(data, '$objects') &&
14
+ Array.isArray(data.$objects) &&
15
+ data.$objects.length > 0
16
+ );
17
+ }
18
+
19
+ export function isNSDictionaryFormat(obj: any): boolean {
20
+ return hasProperties(obj, 'NS.keys', 'NS.objects');
21
+ }
22
+
23
+ export function hasNSErrorIndicators(obj: any): boolean {
24
+ if (!isPlainObject(obj)) {
25
+ return false;
26
+ }
27
+
28
+ const errorProps = ['NSCode', 'NSUserInfo', 'NSDomain'];
29
+ return errorProps.some((prop) => prop in obj);
30
+ }
31
+
32
+ /**
33
+ * Extract $objects array from NSKeyedArchiver format, returns null if invalid
34
+ */
35
+ export function extractNSKeyedArchiverObjects(data: any): any[] | null {
36
+ if (!isNSKeyedArchiverFormat(data)) {
37
+ return null;
38
+ }
39
+
40
+ const objects = data.$objects;
41
+ return objects.length > 1 ? objects : null;
42
+ }
43
+
44
+ /**
45
+ * Extract NSDictionary from NSKeyedArchiver objects using key/value references
46
+ */
47
+ export function extractNSDictionary(
48
+ dictObj: any,
49
+ objects: any[],
50
+ ): PlistDictionary {
51
+ if (!isNSDictionaryFormat(dictObj)) {
52
+ return {};
53
+ }
54
+
55
+ const keysRef = dictObj['NS.keys'];
56
+ const valuesRef = dictObj['NS.objects'];
57
+
58
+ if (!Array.isArray(keysRef) || !Array.isArray(valuesRef)) {
59
+ return {};
60
+ }
61
+
62
+ const result: PlistDictionary = {};
63
+ for (let i = 0; i < keysRef.length; i++) {
64
+ const key = objects[keysRef[i]];
65
+ const value = objects[valuesRef[i]];
66
+ if (typeof key === 'string') {
67
+ result[key] = value;
68
+ }
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ /**
75
+ * Extract strings from NSKeyedArchiver objects array as dictionary keys
76
+ */
77
+ export function extractCapabilityStrings(objects: any[]): PlistDictionary {
78
+ const result: PlistDictionary = {};
79
+
80
+ // Start from index 1 because index 0 is always '$null' in NSKeyedArchiver format
81
+ for (let i = 1; i < objects.length; i++) {
82
+ const obj = objects[i];
83
+ if (typeof obj === 'string' && obj !== '$null') {
84
+ result[obj] = true;
85
+ }
86
+ }
87
+
88
+ return result;
89
+ }
package/src/services.ts CHANGED
@@ -4,6 +4,7 @@ import { RemoteXpcConnection } from './lib/remote-xpc/remote-xpc-connection.js';
4
4
  import { TunnelManager } from './lib/tunnel/index.js';
5
5
  import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
6
6
  import type {
7
+ DVTServiceWithConnection,
7
8
  DiagnosticsServiceWithConnection,
8
9
  MisagentServiceWithConnection,
9
10
  MobileConfigServiceWithConnection,
@@ -16,6 +17,9 @@ import type {
16
17
  } from './lib/types.js';
17
18
  import AfcService from './services/ios/afc/index.js';
18
19
  import DiagnosticsService from './services/ios/diagnostic-service/index.js';
20
+ import { DVTSecureSocketProxyService } from './services/ios/dvt/index.js';
21
+ import { ConditionInducer } from './services/ios/dvt/instruments/condition-inducer.js';
22
+ import { LocationSimulation } from './services/ios/dvt/instruments/location-simulation.js';
19
23
  import { MisagentService } from './services/ios/misagent/index.js';
20
24
  import { MobileConfigService } from './services/ios/mobile-config/index.js';
21
25
  import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
@@ -176,6 +180,35 @@ export async function startWebInspectorService(
176
180
  };
177
181
  }
178
182
 
183
+ export async function startDVTService(
184
+ udid: string,
185
+ ): Promise<DVTServiceWithConnection> {
186
+ const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
187
+ const dvtServiceDescriptor = remoteXPC.findService(
188
+ DVTSecureSocketProxyService.RSD_SERVICE_NAME,
189
+ );
190
+
191
+ // Create DVT service instance
192
+ const dvtService = new DVTSecureSocketProxyService([
193
+ tunnelConnection.host,
194
+ parseInt(dvtServiceDescriptor.port, 10),
195
+ ]);
196
+
197
+ // Connect to DVT service
198
+ await dvtService.connect();
199
+
200
+ // Create instrument services
201
+ const locationSimulation = new LocationSimulation(dvtService);
202
+ const conditionInducer = new ConditionInducer(dvtService);
203
+
204
+ return {
205
+ remoteXPC: remoteXPC as RemoteXpcConnection,
206
+ dvtService,
207
+ locationSimulation,
208
+ conditionInducer,
209
+ };
210
+ }
211
+
179
212
  export async function createRemoteXPCConnection(udid: string) {
180
213
  const tunnelConnection = await getTunnelInformation(udid);
181
214
  const remoteXPC = await startService(