appium-ios-remotexpc 0.7.0 → 0.9.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.
@@ -0,0 +1,100 @@
1
+ import { logger } from '@appium/support';
2
+
3
+ import type {
4
+ PlistDictionary,
5
+ PowerAssertionService as PowerAssertionServiceInterface,
6
+ } from '../../../lib/types.js';
7
+ import { ServiceConnection } from '../../../service-connection.js';
8
+ import { BaseService } from '../base-service.js';
9
+
10
+ const log = logger.getLogger('PowerAssertionService');
11
+
12
+ /**
13
+ * Power assertion types that can be used to prevent system sleep
14
+ */
15
+ export enum PowerAssertionType {
16
+ WIRELESS_SYNC = 'AMDPowerAssertionTypeWirelessSync',
17
+ PREVENT_USER_IDLE_SYSTEM_SLEEP = 'PreventUserIdleSystemSleep',
18
+ PREVENT_SYSTEM_SLEEP = 'PreventSystemSleep',
19
+ }
20
+
21
+ /**
22
+ * Options for power assertion creation
23
+ */
24
+ export interface PowerAssertionOptions {
25
+ type: PowerAssertionType;
26
+ name: string;
27
+ timeout: number; // timeout in seconds
28
+ details?: string;
29
+ }
30
+
31
+ /**
32
+ * PowerAssertionService provides an API to create power assertions.
33
+ */
34
+ class PowerAssertionService
35
+ extends BaseService
36
+ implements PowerAssertionServiceInterface
37
+ {
38
+ static readonly RSD_SERVICE_NAME =
39
+ 'com.apple.mobile.assertion_agent.shim.remote';
40
+
41
+ private _conn: ServiceConnection | null = null;
42
+
43
+ /**
44
+ * Create a power assertion to prevent system sleep
45
+ * @param options Options for creating the power assertion
46
+ * @returns Promise that resolves when the assertion is created
47
+ */
48
+ async createPowerAssertion(options: PowerAssertionOptions): Promise<void> {
49
+ if (!this._conn) {
50
+ this._conn = await this.connectToPowerAssertionService();
51
+ }
52
+
53
+ const request = this.buildCreateAssertionRequest(options);
54
+ await this._conn.sendPlistRequest(request);
55
+ log.info(
56
+ `Power assertion created: type="${options.type}", name="${options.name}", timeout=${options.timeout}s`,
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Close the connection to the power assertion service
62
+ */
63
+ async close(): Promise<void> {
64
+ if (this._conn) {
65
+ await this._conn.close();
66
+ this._conn = null;
67
+ log.debug('Power assertion service connection closed');
68
+ }
69
+ }
70
+
71
+ private async connectToPowerAssertionService(): Promise<ServiceConnection> {
72
+ const service = {
73
+ serviceName: PowerAssertionService.RSD_SERVICE_NAME,
74
+ port: this.address[1].toString(),
75
+ };
76
+ log.debug(
77
+ `Connecting to power assertion service at ${this.address[0]}:${this.address[1]}`,
78
+ );
79
+ return await this.startLockdownService(service);
80
+ }
81
+
82
+ private buildCreateAssertionRequest(
83
+ options: PowerAssertionOptions,
84
+ ): PlistDictionary {
85
+ const request: PlistDictionary = {
86
+ CommandKey: 'CommandCreateAssertion',
87
+ AssertionTypeKey: options.type,
88
+ AssertionNameKey: options.name,
89
+ AssertionTimeoutKey: options.timeout,
90
+ };
91
+
92
+ if (options.details !== undefined) {
93
+ request.AssertionDetailKey = options.details;
94
+ }
95
+
96
+ return request;
97
+ }
98
+ }
99
+
100
+ export { PowerAssertionService };
@@ -0,0 +1,372 @@
1
+ import { logger } from '@appium/support';
2
+ import { randomUUID } from 'crypto';
3
+ import { EventEmitter } from 'events';
4
+
5
+ import type { PlistDictionary, PlistMessage } from '../../../lib/types.js';
6
+ import { ServiceConnection } from '../../../service-connection.js';
7
+ import { BaseService } from '../base-service.js';
8
+
9
+ const log = logger.getLogger('WebInspectorService');
10
+
11
+ /**
12
+ * Interface for WebInspector message structure
13
+ */
14
+ export interface WebInspectorMessage extends PlistDictionary {
15
+ __selector: string;
16
+ __argument: PlistDictionary;
17
+ }
18
+
19
+ /**
20
+ * WebInspectorService provides an API to:
21
+ * - Send messages to webinspectord
22
+ * - Listen to messages from webinspectord
23
+ * - Communicate with web views and Safari on iOS devices
24
+ *
25
+ * This service is used for web automation, inspection, and debugging.
26
+ */
27
+ export class WebInspectorService extends BaseService {
28
+ static readonly RSD_SERVICE_NAME = 'com.apple.webinspector.shim.remote';
29
+
30
+ // RPC method selectors
31
+ private static readonly RPC_REPORT_IDENTIFIER = '_rpc_reportIdentifier:';
32
+ private static readonly RPC_REQUEST_APPLICATION_LAUNCH =
33
+ '_rpc_requestApplicationLaunch:';
34
+ private static readonly RPC_GET_CONNECTED_APPLICATIONS =
35
+ '_rpc_getConnectedApplications:';
36
+ private static readonly RPC_FORWARD_GET_LISTING = '_rpc_forwardGetListing:';
37
+ private static readonly RPC_FORWARD_AUTOMATION_SESSION_REQUEST =
38
+ '_rpc_forwardAutomationSessionRequest:';
39
+ private static readonly RPC_FORWARD_SOCKET_SETUP = '_rpc_forwardSocketSetup:';
40
+ private static readonly RPC_FORWARD_SOCKET_DATA = '_rpc_forwardSocketData:';
41
+ private static readonly RPC_FORWARD_INDICATE_WEB_VIEW =
42
+ '_rpc_forwardIndicateWebView:';
43
+
44
+ private connection: ServiceConnection | null = null;
45
+ private messageEmitter: EventEmitter = new EventEmitter();
46
+ private isReceiving: boolean = false;
47
+ private readonly connectionId: string;
48
+ private receivePromise: Promise<void> | null = null;
49
+
50
+ constructor(address: [string, number]) {
51
+ super(address);
52
+ this.connectionId = randomUUID().toUpperCase();
53
+ }
54
+
55
+ /**
56
+ * Send a message to the WebInspector service
57
+ * @param selector The RPC selector (e.g., '_rpc_reportIdentifier:')
58
+ * @param args The arguments dictionary for the message
59
+ * @returns Promise that resolves when the message is sent
60
+ */
61
+ async sendMessage(
62
+ selector: string,
63
+ args: PlistDictionary = {},
64
+ ): Promise<void> {
65
+ const connection = await this.connectToWebInspectorService();
66
+
67
+ // Add connection identifier to all messages
68
+ const message: WebInspectorMessage = {
69
+ __selector: selector,
70
+ __argument: {
71
+ ...args,
72
+ WIRConnectionIdentifierKey: this.connectionId,
73
+ },
74
+ };
75
+
76
+ log.debug(`Sending WebInspector message: ${selector}`);
77
+
78
+ connection.sendPlist(message);
79
+ }
80
+
81
+ /**
82
+ * Listen to messages from the WebInspector service using async generator
83
+ * @yields PlistMessage - Messages received from the WebInspector service
84
+ */
85
+ async *listenMessage(): AsyncGenerator<PlistMessage, void, unknown> {
86
+ await this.connectToWebInspectorService();
87
+
88
+ // Start receiving messages in background if not already started
89
+ if (!this.isReceiving) {
90
+ this.startMessageReceiver();
91
+ }
92
+
93
+ const queue: PlistMessage[] = [];
94
+ let resolveNext: ((value: IteratorResult<PlistMessage>) => void) | null =
95
+ null;
96
+ let stopped = false;
97
+
98
+ const messageHandler = (message: PlistMessage) => {
99
+ if (resolveNext) {
100
+ resolveNext({ value: message, done: false });
101
+ resolveNext = null;
102
+ } else {
103
+ queue.push(message);
104
+ }
105
+ };
106
+
107
+ const stopHandler = () => {
108
+ stopped = true;
109
+ if (resolveNext) {
110
+ resolveNext({ value: undefined, done: true });
111
+ resolveNext = null;
112
+ }
113
+ };
114
+
115
+ this.messageEmitter.on('message', messageHandler);
116
+ this.messageEmitter.once('stop', stopHandler);
117
+
118
+ try {
119
+ while (!stopped) {
120
+ if (queue.length > 0) {
121
+ yield queue.shift()!;
122
+ } else {
123
+ const message = await new Promise<PlistMessage | null>((resolve) => {
124
+ if (stopped) {
125
+ resolve(null);
126
+ return;
127
+ }
128
+ resolveNext = (result) => {
129
+ resolve(result.done ? null : result.value);
130
+ };
131
+ });
132
+
133
+ if (message === null) {
134
+ break;
135
+ }
136
+
137
+ yield message;
138
+ }
139
+ }
140
+ } finally {
141
+ this.messageEmitter.off('message', messageHandler);
142
+ this.messageEmitter.off('stop', stopHandler);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Stop listening to messages
148
+ */
149
+ stopListening(): void {
150
+ this.isReceiving = false;
151
+ this.messageEmitter.emit('stop');
152
+ }
153
+
154
+ /**
155
+ * Close the connection and clean up resources
156
+ */
157
+ async close(): Promise<void> {
158
+ this.stopListening();
159
+
160
+ if (this.connection) {
161
+ await this.connection.close();
162
+ this.connection = null;
163
+ log.debug('WebInspector connection closed');
164
+ }
165
+
166
+ if (this.receivePromise) {
167
+ await this.receivePromise;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get the connection ID being used for this service
173
+ * @returns The connection identifier
174
+ */
175
+ getConnectionId(): string {
176
+ return this.connectionId;
177
+ }
178
+
179
+ /**
180
+ * Request application launch
181
+ * @param bundleId The bundle identifier of the application to launch
182
+ */
183
+ async requestApplicationLaunch(bundleId: string): Promise<void> {
184
+ await this.sendMessage(WebInspectorService.RPC_REQUEST_APPLICATION_LAUNCH, {
185
+ WIRApplicationBundleIdentifierKey: bundleId,
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Get connected applications
191
+ */
192
+ async getConnectedApplications(): Promise<void> {
193
+ await this.sendMessage(
194
+ WebInspectorService.RPC_GET_CONNECTED_APPLICATIONS,
195
+ {},
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Forward get listing for an application
201
+ * @param appId The application identifier
202
+ */
203
+ async forwardGetListing(appId: string): Promise<void> {
204
+ await this.sendMessage(WebInspectorService.RPC_FORWARD_GET_LISTING, {
205
+ WIRApplicationIdentifierKey: appId,
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Forward automation session request
211
+ * @param sessionId The session identifier
212
+ * @param appId The application identifier
213
+ * @param capabilities Optional session capabilities
214
+ */
215
+ async forwardAutomationSessionRequest(
216
+ sessionId: string,
217
+ appId: string,
218
+ capabilities?: PlistDictionary,
219
+ ): Promise<void> {
220
+ const defaultCapabilities: PlistDictionary = {
221
+ 'org.webkit.webdriver.webrtc.allow-insecure-media-capture': true,
222
+ 'org.webkit.webdriver.webrtc.suppress-ice-candidate-filtering': false,
223
+ };
224
+
225
+ await this.sendMessage(
226
+ WebInspectorService.RPC_FORWARD_AUTOMATION_SESSION_REQUEST,
227
+ {
228
+ WIRApplicationIdentifierKey: appId,
229
+ WIRSessionIdentifierKey: sessionId,
230
+ WIRSessionCapabilitiesKey: {
231
+ ...defaultCapabilities,
232
+ ...(capabilities ?? {}),
233
+ },
234
+ },
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Forward socket setup for inspector connection
240
+ * @param sessionId The session identifier
241
+ * @param appId The application identifier
242
+ * @param pageId The page identifier
243
+ * @param automaticallyPause Whether to automatically pause (defaults to true)
244
+ */
245
+ async forwardSocketSetup(
246
+ sessionId: string,
247
+ appId: string,
248
+ pageId: number,
249
+ automaticallyPause: boolean = true,
250
+ ): Promise<void> {
251
+ const message: PlistDictionary = {
252
+ WIRApplicationIdentifierKey: appId,
253
+ WIRPageIdentifierKey: pageId,
254
+ WIRSenderKey: sessionId,
255
+ WIRMessageDataTypeChunkSupportedKey: 0,
256
+ };
257
+
258
+ if (!automaticallyPause) {
259
+ message.WIRAutomaticallyPause = false;
260
+ }
261
+
262
+ await this.sendMessage(
263
+ WebInspectorService.RPC_FORWARD_SOCKET_SETUP,
264
+ message,
265
+ );
266
+ }
267
+
268
+ /**
269
+ * Forward socket data to a page
270
+ * @param sessionId The session identifier
271
+ * @param appId The application identifier
272
+ * @param pageId The page identifier
273
+ * @param data The data to send (will be JSON stringified)
274
+ */
275
+ async forwardSocketData(
276
+ sessionId: string,
277
+ appId: string,
278
+ pageId: number,
279
+ data: any,
280
+ ): Promise<void> {
281
+ const socketData = typeof data === 'string' ? data : JSON.stringify(data);
282
+
283
+ await this.sendMessage(WebInspectorService.RPC_FORWARD_SOCKET_DATA, {
284
+ WIRApplicationIdentifierKey: appId,
285
+ WIRPageIdentifierKey: pageId,
286
+ WIRSessionIdentifierKey: sessionId,
287
+ WIRSenderKey: sessionId,
288
+ WIRSocketDataKey: Buffer.from(socketData, 'utf-8'),
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Forward indicate web view
294
+ * @param appId The application identifier
295
+ * @param pageId The page identifier
296
+ * @param enable Whether to enable indication
297
+ */
298
+ async forwardIndicateWebView(
299
+ appId: string,
300
+ pageId: number,
301
+ enable: boolean,
302
+ ): Promise<void> {
303
+ await this.sendMessage(WebInspectorService.RPC_FORWARD_INDICATE_WEB_VIEW, {
304
+ WIRApplicationIdentifierKey: appId,
305
+ WIRPageIdentifierKey: pageId,
306
+ WIRIndicateEnabledKey: enable,
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Connect to the WebInspector service
312
+ * @returns Promise resolving to the ServiceConnection instance
313
+ */
314
+ private async connectToWebInspectorService(): Promise<ServiceConnection> {
315
+ if (this.connection) {
316
+ return this.connection;
317
+ }
318
+
319
+ const service = {
320
+ serviceName: WebInspectorService.RSD_SERVICE_NAME,
321
+ port: this.address[1].toString(),
322
+ };
323
+
324
+ this.connection = await this.startLockdownService(service);
325
+
326
+ // Consume the StartService response from RSDCheckin
327
+ const startServiceResponse = await this.connection.receive();
328
+ if (startServiceResponse?.Request !== 'StartService') {
329
+ log.warn(
330
+ `Expected StartService response, got: ${JSON.stringify(startServiceResponse)}`,
331
+ );
332
+ }
333
+
334
+ // Send initial identifier report
335
+ await this.sendMessage(WebInspectorService.RPC_REPORT_IDENTIFIER, {});
336
+
337
+ log.debug('Connected to WebInspector service');
338
+ return this.connection;
339
+ }
340
+
341
+ /**
342
+ * Start receiving messages from the WebInspector service in the background
343
+ */
344
+ private startMessageReceiver(): void {
345
+ if (this.isReceiving || !this.connection) {
346
+ return;
347
+ }
348
+
349
+ this.isReceiving = true;
350
+
351
+ this.receivePromise = (async () => {
352
+ try {
353
+ while (this.isReceiving && this.connection) {
354
+ try {
355
+ const message = await this.connection.receive();
356
+ this.messageEmitter.emit('message', message);
357
+ } catch (error) {
358
+ if (this.isReceiving) {
359
+ log.error('Error receiving message:', error);
360
+ this.messageEmitter.emit('error', error);
361
+ }
362
+ break;
363
+ }
364
+ }
365
+ } finally {
366
+ this.isReceiving = false;
367
+ }
368
+ })();
369
+ }
370
+ }
371
+
372
+ export default WebInspectorService;
package/src/services.ts CHANGED
@@ -8,15 +8,19 @@ import type {
8
8
  MobileConfigServiceWithConnection,
9
9
  MobileImageMounterServiceWithConnection,
10
10
  NotificationProxyServiceWithConnection,
11
+ PowerAssertionServiceWithConnection,
11
12
  SpringboardServiceWithConnection,
12
13
  SyslogService as SyslogServiceType,
14
+ WebInspectorServiceWithConnection,
13
15
  } from './lib/types.js';
14
16
  import DiagnosticsService from './services/ios/diagnostic-service/index.js';
15
17
  import { MobileConfigService } from './services/ios/mobile-config/index.js';
16
18
  import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
17
19
  import { NotificationProxyService } from './services/ios/notification-proxy/index.js';
20
+ import { PowerAssertionService } from './services/ios/power-assertion/index.js';
18
21
  import { SpringBoardService } from './services/ios/springboard-service/index.js';
19
22
  import SyslogService from './services/ios/syslog-service/index.js';
23
+ import { WebInspectorService } from './services/ios/webinspector/index.js';
20
24
 
21
25
  const APPIUM_XCUITEST_DRIVER_NAME = 'appium-xcuitest-driver';
22
26
  const TUNNEL_REGISTRY_PORT = 'tunnelRegistryPort';
@@ -100,6 +104,22 @@ export async function startSpringboardService(
100
104
  };
101
105
  }
102
106
 
107
+ export async function startPowerAssertionService(
108
+ udid: string,
109
+ ): Promise<PowerAssertionServiceWithConnection> {
110
+ const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
111
+ const powerAssertionService = remoteXPC.findService(
112
+ PowerAssertionService.RSD_SERVICE_NAME,
113
+ );
114
+ return {
115
+ remoteXPC: remoteXPC as RemoteXpcConnection,
116
+ powerAssertionService: new PowerAssertionService([
117
+ tunnelConnection.host,
118
+ parseInt(powerAssertionService.port, 10),
119
+ ]),
120
+ };
121
+ }
122
+
103
123
  export async function startSyslogService(
104
124
  udid: string,
105
125
  ): Promise<SyslogServiceType> {
@@ -107,6 +127,22 @@ export async function startSyslogService(
107
127
  return new SyslogService([tunnelConnection.host, tunnelConnection.port]);
108
128
  }
109
129
 
130
+ export async function startWebInspectorService(
131
+ udid: string,
132
+ ): Promise<WebInspectorServiceWithConnection> {
133
+ const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
134
+ const webInspectorService = remoteXPC.findService(
135
+ WebInspectorService.RSD_SERVICE_NAME,
136
+ );
137
+ return {
138
+ remoteXPC: remoteXPC as RemoteXpcConnection,
139
+ webInspectorService: new WebInspectorService([
140
+ tunnelConnection.host,
141
+ parseInt(webInspectorService.port, 10),
142
+ ]),
143
+ };
144
+ }
145
+
110
146
  export async function createRemoteXPCConnection(udid: string) {
111
147
  const tunnelConnection = await getTunnelInformation(udid);
112
148
  const remoteXPC = await startService(