appium-ios-remotexpc 0.6.2 → 0.8.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/build/src/index.d.ts +1 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/lib/types.d.ts +184 -0
- package/build/src/lib/types.d.ts.map +1 -1
- package/build/src/service-connection.d.ts +6 -0
- package/build/src/service-connection.d.ts.map +1 -1
- package/build/src/service-connection.js +8 -0
- package/build/src/services/index.d.ts +2 -1
- package/build/src/services/index.d.ts.map +1 -1
- package/build/src/services/index.js +2 -1
- package/build/src/services/ios/springboard-service/index.d.ts +39 -0
- package/build/src/services/ios/springboard-service/index.d.ts.map +1 -0
- package/build/src/services/ios/springboard-service/index.js +174 -0
- package/build/src/services/ios/webinspector/index.d.ts +114 -0
- package/build/src/services/ios/webinspector/index.d.ts.map +1 -0
- package/build/src/services/ios/webinspector/index.js +286 -0
- package/build/src/services.d.ts +3 -1
- package/build/src/services.d.ts.map +1 -1
- package/build/src/services.js +24 -0
- package/package.json +3 -1
- package/src/index.ts +4 -0
- package/src/lib/types.ts +226 -0
- package/src/service-connection.ts +9 -0
- package/src/services/index.ts +2 -0
- package/src/services/ios/springboard-service/index.ts +197 -0
- package/src/services/ios/webinspector/index.ts +372 -0
- package/src/services.ts +36 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PlistDictionary,
|
|
3
|
+
type SpringboardService as SpringboardInterface,
|
|
4
|
+
} from '../../../lib/types.js';
|
|
5
|
+
import { ServiceConnection } from '../../../service-connection.js';
|
|
6
|
+
import { BaseService } from '../base-service.js';
|
|
7
|
+
|
|
8
|
+
enum InterfaceOrientation {
|
|
9
|
+
PORTRAIT = 1, // 0 degrees (default)
|
|
10
|
+
PORTRAIT_UPSIDE_DOWN = 2, // 180 degrees
|
|
11
|
+
LANDSCAPE = 3, // 90 degrees clockwise
|
|
12
|
+
LANDSCAPE_HOME_TO_LEFT = 4, // 270 degrees clockwise
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class SpringBoardService extends BaseService implements SpringboardInterface {
|
|
16
|
+
static readonly RSD_SERVICE_NAME =
|
|
17
|
+
'com.apple.springboardservices.shim.remote';
|
|
18
|
+
private _conn: ServiceConnection | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(address: [string, number]) {
|
|
21
|
+
super(address);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getIconState(): Promise<PlistDictionary> {
|
|
25
|
+
try {
|
|
26
|
+
const req = {
|
|
27
|
+
command: 'getIconState',
|
|
28
|
+
formatVersion: '2',
|
|
29
|
+
};
|
|
30
|
+
return await this.sendRequestAndReceive(req);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof Error) {
|
|
33
|
+
throw new Error(`Failed to get Icon state: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* TODO: This does not work currently due to a bug in Apple protocol implementation (maybe?)
|
|
41
|
+
* Uncomment tests when it is fixed
|
|
42
|
+
*/
|
|
43
|
+
async setIconState(newState: PlistDictionary[] = []): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
const req = {
|
|
46
|
+
command: 'setIconState',
|
|
47
|
+
iconState: newState,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await this.sendRequestAndReceive(req);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
throw new Error(`Failed to set icon state: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getIconPNGData(bundleID: string): Promise<Buffer> {
|
|
60
|
+
try {
|
|
61
|
+
const req = {
|
|
62
|
+
command: 'getIconPNGData',
|
|
63
|
+
bundleId: bundleID,
|
|
64
|
+
};
|
|
65
|
+
const res = await this.sendRequestAndReceive(req);
|
|
66
|
+
return res.pngData as Buffer;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof Error) {
|
|
69
|
+
throw new Error(`Failed to get Icon PNG data: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* TODO: This does not work currently due to a bug in Apple protocol implementation
|
|
77
|
+
* Add tests when it is fixed
|
|
78
|
+
*/
|
|
79
|
+
async getWallpaperInfo(wallpaperName: string): Promise<PlistDictionary> {
|
|
80
|
+
try {
|
|
81
|
+
const req = {
|
|
82
|
+
command: 'getWallpaperInfo',
|
|
83
|
+
wallpaperName,
|
|
84
|
+
};
|
|
85
|
+
return await this.sendRequestAndReceive(req);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
throw new Error(`Failed to get wallpaper info: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getWallpaperPreviewImage(
|
|
95
|
+
wallpaperName: 'homescreen' | 'lockscreen',
|
|
96
|
+
): Promise<Buffer> {
|
|
97
|
+
try {
|
|
98
|
+
const req = {
|
|
99
|
+
command: 'getWallpaperPreviewImage',
|
|
100
|
+
wallpaperName,
|
|
101
|
+
};
|
|
102
|
+
const res = await this.sendRequestAndReceive(req);
|
|
103
|
+
return res.pngData as Buffer;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof Error) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Failed to get wallpaper preview image: ${error.message}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getHomescreenIconMetrics(): Promise<PlistDictionary> {
|
|
115
|
+
try {
|
|
116
|
+
const req = {
|
|
117
|
+
command: 'getHomeScreenIconMetrics',
|
|
118
|
+
};
|
|
119
|
+
return await this.sendRequestAndReceive(req);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof Error) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Failed to get homescreen icon metrics: ${error.message}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async getInterfaceOrientation(): Promise<InterfaceOrientation> {
|
|
131
|
+
try {
|
|
132
|
+
const req = {
|
|
133
|
+
command: 'getInterfaceOrientation',
|
|
134
|
+
};
|
|
135
|
+
const res = await this.sendRequestAndReceive(req);
|
|
136
|
+
return res.interfaceOrientation as InterfaceOrientation;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof Error) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Failed to get interface orientation: ${error.message}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* TODO: This does not work currently due to a bug in Apple protocol implementation
|
|
148
|
+
* Add tests when it is fixed
|
|
149
|
+
*/
|
|
150
|
+
async getWallpaperPNGData(wallpaperName: string): Promise<Buffer> {
|
|
151
|
+
try {
|
|
152
|
+
const req = {
|
|
153
|
+
command: 'getHomeScreenWallpaperPNGData',
|
|
154
|
+
wallpaperName,
|
|
155
|
+
};
|
|
156
|
+
const res = await this.sendRequestAndReceive(req);
|
|
157
|
+
return res.pngData as Buffer;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof Error) {
|
|
160
|
+
throw new Error(`Failed to get wallpaper PNG data: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async connectToSpringboardService(): Promise<ServiceConnection> {
|
|
167
|
+
if (this._conn) {
|
|
168
|
+
return this._conn;
|
|
169
|
+
}
|
|
170
|
+
const service = this.getServiceConfig();
|
|
171
|
+
this._conn = await this.startLockdownService(service);
|
|
172
|
+
return this._conn;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async sendRequestAndReceive(
|
|
176
|
+
request: PlistDictionary,
|
|
177
|
+
): Promise<PlistDictionary> {
|
|
178
|
+
if (!this._conn) {
|
|
179
|
+
this._conn = await this.connectToSpringboardService();
|
|
180
|
+
}
|
|
181
|
+
// Skip StartService response
|
|
182
|
+
await this._conn.sendAndReceive(request);
|
|
183
|
+
return await this._conn.sendPlistRequest(request);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private getServiceConfig(): {
|
|
187
|
+
serviceName: string;
|
|
188
|
+
port: string;
|
|
189
|
+
} {
|
|
190
|
+
return {
|
|
191
|
+
serviceName: SpringBoardService.RSD_SERVICE_NAME,
|
|
192
|
+
port: this.address[1].toString(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { SpringBoardService, InterfaceOrientation };
|
|
@@ -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,13 +8,17 @@ import type {
|
|
|
8
8
|
MobileConfigServiceWithConnection,
|
|
9
9
|
MobileImageMounterServiceWithConnection,
|
|
10
10
|
NotificationProxyServiceWithConnection,
|
|
11
|
+
SpringboardServiceWithConnection,
|
|
11
12
|
SyslogService as SyslogServiceType,
|
|
13
|
+
WebInspectorServiceWithConnection,
|
|
12
14
|
} from './lib/types.js';
|
|
13
15
|
import DiagnosticsService from './services/ios/diagnostic-service/index.js';
|
|
14
16
|
import { MobileConfigService } from './services/ios/mobile-config/index.js';
|
|
15
17
|
import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
|
|
16
18
|
import { NotificationProxyService } from './services/ios/notification-proxy/index.js';
|
|
19
|
+
import { SpringBoardService } from './services/ios/springboard-service/index.js';
|
|
17
20
|
import SyslogService from './services/ios/syslog-service/index.js';
|
|
21
|
+
import { WebInspectorService } from './services/ios/webinspector/index.js';
|
|
18
22
|
|
|
19
23
|
const APPIUM_XCUITEST_DRIVER_NAME = 'appium-xcuitest-driver';
|
|
20
24
|
const TUNNEL_REGISTRY_PORT = 'tunnelRegistryPort';
|
|
@@ -82,6 +86,22 @@ export async function startMobileImageMounterService(
|
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
export async function startSpringboardService(
|
|
90
|
+
udid: string,
|
|
91
|
+
): Promise<SpringboardServiceWithConnection> {
|
|
92
|
+
const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
|
|
93
|
+
const springboardService = remoteXPC.findService(
|
|
94
|
+
SpringBoardService.RSD_SERVICE_NAME,
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
remoteXPC: remoteXPC as RemoteXpcConnection,
|
|
98
|
+
springboardService: new SpringBoardService([
|
|
99
|
+
tunnelConnection.host,
|
|
100
|
+
parseInt(springboardService.port, 10),
|
|
101
|
+
]),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
85
105
|
export async function startSyslogService(
|
|
86
106
|
udid: string,
|
|
87
107
|
): Promise<SyslogServiceType> {
|
|
@@ -89,6 +109,22 @@ export async function startSyslogService(
|
|
|
89
109
|
return new SyslogService([tunnelConnection.host, tunnelConnection.port]);
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
export async function startWebInspectorService(
|
|
113
|
+
udid: string,
|
|
114
|
+
): Promise<WebInspectorServiceWithConnection> {
|
|
115
|
+
const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
|
|
116
|
+
const webInspectorService = remoteXPC.findService(
|
|
117
|
+
WebInspectorService.RSD_SERVICE_NAME,
|
|
118
|
+
);
|
|
119
|
+
return {
|
|
120
|
+
remoteXPC: remoteXPC as RemoteXpcConnection,
|
|
121
|
+
webInspectorService: new WebInspectorService([
|
|
122
|
+
tunnelConnection.host,
|
|
123
|
+
parseInt(webInspectorService.port, 10),
|
|
124
|
+
]),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
export async function createRemoteXPCConnection(udid: string) {
|
|
93
129
|
const tunnelConnection = await getTunnelInformation(udid);
|
|
94
130
|
const remoteXPC = await startService(
|