appium-ios-remotexpc 0.3.2 → 0.4.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 +16 -0
- package/build/src/index.d.ts +1 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-creator.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-creator.js +17 -5
- package/build/src/lib/plist/binary-plist-parser.d.ts.map +1 -1
- package/build/src/lib/plist/binary-plist-parser.js +19 -9
- package/build/src/lib/plist/plist-creator.d.ts.map +1 -1
- package/build/src/lib/plist/plist-creator.js +4 -0
- package/build/src/lib/tss/index.d.ts +71 -0
- package/build/src/lib/tss/index.d.ts.map +1 -0
- package/build/src/lib/tss/index.js +243 -0
- package/build/src/lib/types.d.ts +79 -0
- package/build/src/lib/types.d.ts.map +1 -1
- 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/mobile-image-mounter/index.d.ts +122 -0
- package/build/src/services/ios/mobile-image-mounter/index.d.ts.map +1 -0
- package/build/src/services/ios/mobile-image-mounter/index.js +363 -0
- package/build/src/services.d.ts +2 -1
- package/build/src/services.d.ts.map +1 -1
- package/build/src/services.js +12 -0
- package/package.json +5 -3
- package/src/index.ts +2 -0
- package/src/lib/plist/binary-plist-creator.ts +20 -5
- package/src/lib/plist/binary-plist-parser.ts +22 -12
- package/src/lib/plist/plist-creator.ts +4 -0
- package/src/lib/tss/index.ts +338 -0
- package/src/lib/types.ts +98 -0
- package/src/services/index.ts +2 -0
- package/src/services/ios/mobile-image-mounter/index.ts +525 -0
- package/src/services.ts +18 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { logger } from '@appium/support';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { Stats, promises as fs } from 'fs';
|
|
4
|
+
import { performance } from 'perf_hooks';
|
|
5
|
+
import { Readable } from 'stream';
|
|
6
|
+
|
|
7
|
+
import { parseXmlPlist } from '../../../lib/plist/index.js';
|
|
8
|
+
import { getManifestFromTSS } from '../../../lib/tss/index.js';
|
|
9
|
+
import type {
|
|
10
|
+
MobileImageMounterService as MobileImageMounterServiceInterface,
|
|
11
|
+
PlistDictionary,
|
|
12
|
+
} from '../../../lib/types.js';
|
|
13
|
+
import { ServiceConnection } from '../../../service-connection.js';
|
|
14
|
+
import { BaseService } from '../base-service.js';
|
|
15
|
+
|
|
16
|
+
const log = logger.getLogger('MobileImageMounterService');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base interface for service responses
|
|
20
|
+
*/
|
|
21
|
+
interface BaseResponse {
|
|
22
|
+
Status?: string;
|
|
23
|
+
Error?: string;
|
|
24
|
+
DetailedError?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Interface for image-related responses
|
|
29
|
+
*/
|
|
30
|
+
export interface ImageResponse extends BaseResponse {
|
|
31
|
+
ImagePresent?: boolean;
|
|
32
|
+
ImageSignature?: Buffer[] | Buffer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* MobileImageMounterService provides an API to:
|
|
37
|
+
* - Mount Developer Disk Images on iOS devices
|
|
38
|
+
* - Lookup mounted images and their signatures
|
|
39
|
+
* - Check if personalized images are mounted
|
|
40
|
+
* - Unmount images when needed
|
|
41
|
+
*/
|
|
42
|
+
class MobileImageMounterService
|
|
43
|
+
extends BaseService
|
|
44
|
+
implements MobileImageMounterServiceInterface
|
|
45
|
+
{
|
|
46
|
+
static readonly RSD_SERVICE_NAME =
|
|
47
|
+
'com.apple.mobile.mobile_image_mounter.shim.remote';
|
|
48
|
+
|
|
49
|
+
// Constants
|
|
50
|
+
private static readonly FILE_TYPE_IMAGE = 'image';
|
|
51
|
+
private static readonly FILE_TYPE_BUILD_MANIFEST = 'build_manifest';
|
|
52
|
+
private static readonly FILE_TYPE_TRUST_CACHE = 'trust_cache';
|
|
53
|
+
private static readonly IMAGE_TYPE = 'Personalized';
|
|
54
|
+
private static readonly MOUNT_PATH = '/System/Developer';
|
|
55
|
+
private static readonly UPLOAD_IMAGE_TIMEOUT = 20000;
|
|
56
|
+
|
|
57
|
+
// Connection cache
|
|
58
|
+
private connection: ServiceConnection | null = null;
|
|
59
|
+
|
|
60
|
+
constructor(address: [string, number]) {
|
|
61
|
+
super(address);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clean up resources when service is no longer needed
|
|
66
|
+
*/
|
|
67
|
+
async cleanup(): Promise<void> {
|
|
68
|
+
this.closeConnection();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Lookup mounted images by type
|
|
73
|
+
* @param imageType Type of image to lookup (defaults to 'Personalized')
|
|
74
|
+
* @returns Array of signatures of mounted images
|
|
75
|
+
*/
|
|
76
|
+
async lookup(
|
|
77
|
+
imageType = MobileImageMounterService.IMAGE_TYPE,
|
|
78
|
+
): Promise<Buffer[]> {
|
|
79
|
+
const response = (await this.sendRequest({
|
|
80
|
+
Command: 'LookupImage',
|
|
81
|
+
ImageType: imageType,
|
|
82
|
+
})) as ImageResponse;
|
|
83
|
+
|
|
84
|
+
const signatures = response.ImageSignature || [];
|
|
85
|
+
return signatures.filter(Buffer.isBuffer) as Buffer[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if personalized image is mounted
|
|
90
|
+
* @returns True if personalized image is mounted
|
|
91
|
+
*/
|
|
92
|
+
async isPersonalizedImageMounted(): Promise<boolean> {
|
|
93
|
+
try {
|
|
94
|
+
return (await this.lookup()).length > 0;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mount personalized image for device (iOS >= 17)
|
|
102
|
+
* @param imageFilePath Path to the image file (.dmg)
|
|
103
|
+
* @param buildManifestFilePath Path to the build manifest file (.plist)
|
|
104
|
+
* @param trustCacheFilePath Path to the trust cache file (.trustcache)
|
|
105
|
+
* @param infoPlist Optional info plist dictionary
|
|
106
|
+
*/
|
|
107
|
+
async mount(
|
|
108
|
+
imageFilePath: string,
|
|
109
|
+
buildManifestFilePath: string,
|
|
110
|
+
trustCacheFilePath: string,
|
|
111
|
+
infoPlist?: PlistDictionary,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
if (await this.isPersonalizedImageMounted()) {
|
|
114
|
+
log.info('Personalized image is already mounted');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const start = performance.now();
|
|
119
|
+
|
|
120
|
+
// Validate files and read content
|
|
121
|
+
await Promise.all([
|
|
122
|
+
this.assertIsFile(
|
|
123
|
+
imageFilePath,
|
|
124
|
+
MobileImageMounterService.FILE_TYPE_IMAGE,
|
|
125
|
+
),
|
|
126
|
+
this.assertIsFile(
|
|
127
|
+
buildManifestFilePath,
|
|
128
|
+
MobileImageMounterService.FILE_TYPE_BUILD_MANIFEST,
|
|
129
|
+
),
|
|
130
|
+
this.assertIsFile(
|
|
131
|
+
trustCacheFilePath,
|
|
132
|
+
MobileImageMounterService.FILE_TYPE_TRUST_CACHE,
|
|
133
|
+
),
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const [image, trustCache, buildManifestContent] = await Promise.all([
|
|
137
|
+
fs.readFile(imageFilePath),
|
|
138
|
+
fs.readFile(trustCacheFilePath),
|
|
139
|
+
fs.readFile(buildManifestFilePath, 'utf8'),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
const buildManifest = parseXmlPlist(
|
|
143
|
+
buildManifestContent,
|
|
144
|
+
) as PlistDictionary;
|
|
145
|
+
const manifest = await this.getOrRetrieveManifestFromTSS(
|
|
146
|
+
image,
|
|
147
|
+
buildManifest,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await this.uploadImage(
|
|
151
|
+
MobileImageMounterService.IMAGE_TYPE,
|
|
152
|
+
image,
|
|
153
|
+
manifest,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const extras: Record<string, any> = {
|
|
157
|
+
ImageTrustCache: trustCache,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (infoPlist) {
|
|
161
|
+
extras.ImageInfoPlist = infoPlist;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await this.mountImage(
|
|
165
|
+
MobileImageMounterService.IMAGE_TYPE,
|
|
166
|
+
manifest,
|
|
167
|
+
extras,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const end = performance.now();
|
|
171
|
+
log.info(
|
|
172
|
+
`Successfully mounted personalized image in ${(end - start).toFixed(2)} ms`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Unmount image from device
|
|
178
|
+
* @param mountPath Mount path to unmount (defaults to '/System/Developer')
|
|
179
|
+
*/
|
|
180
|
+
async unmountImage(
|
|
181
|
+
mountPath = MobileImageMounterService.MOUNT_PATH,
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
const response = (await this.sendRequest({
|
|
184
|
+
Command: 'UnmountImage',
|
|
185
|
+
MountPath: mountPath,
|
|
186
|
+
})) as BaseResponse;
|
|
187
|
+
|
|
188
|
+
if (response.Error === 'UnknownCommand') {
|
|
189
|
+
throw new Error('Unmount command is not supported on this iOS version');
|
|
190
|
+
}
|
|
191
|
+
if (response.DetailedError?.includes('There is no matching entry')) {
|
|
192
|
+
throw new Error(`No mounted image found at path: ${mountPath}`);
|
|
193
|
+
}
|
|
194
|
+
if (response.Error === 'InternalError') {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Internal error occurred while unmounting: ${JSON.stringify(response)}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.checkIfError(response);
|
|
201
|
+
log.info(`Successfully unmounted image from ${mountPath}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Query developer mode status (iOS 16+)
|
|
206
|
+
* @returns True if developer mode is enabled (defaults to true for older iOS)
|
|
207
|
+
*/
|
|
208
|
+
async queryDeveloperModeStatus(): Promise<boolean> {
|
|
209
|
+
try {
|
|
210
|
+
const response = await this.sendRequest({
|
|
211
|
+
Command: 'QueryDeveloperModeStatus',
|
|
212
|
+
});
|
|
213
|
+
this.checkIfError(response);
|
|
214
|
+
return Boolean(response.DeveloperModeStatus);
|
|
215
|
+
} catch {
|
|
216
|
+
return true; // Default for older iOS versions
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Query personalization nonce for personalized images
|
|
222
|
+
* @param personalizedImageType Optional personalized image type
|
|
223
|
+
* @returns Personalization nonce as Buffer
|
|
224
|
+
*/
|
|
225
|
+
async queryNonce(personalizedImageType?: string): Promise<Buffer> {
|
|
226
|
+
const request: PlistDictionary = { Command: 'QueryNonce' };
|
|
227
|
+
if (personalizedImageType) {
|
|
228
|
+
request.PersonalizedImageType = personalizedImageType;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const response = await this.sendRequest(request);
|
|
232
|
+
this.checkIfError(response);
|
|
233
|
+
|
|
234
|
+
const nonce = response.PersonalizationNonce;
|
|
235
|
+
if (!Buffer.isBuffer(nonce)) {
|
|
236
|
+
throw new Error('Invalid nonce received from device');
|
|
237
|
+
}
|
|
238
|
+
return nonce;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Query personalization identifiers from the device
|
|
243
|
+
* @returns Personalization identifiers dictionary
|
|
244
|
+
*/
|
|
245
|
+
async queryPersonalizationIdentifiers(): Promise<PlistDictionary> {
|
|
246
|
+
const response = await this.sendRequest({
|
|
247
|
+
Command: 'QueryPersonalizationIdentifiers',
|
|
248
|
+
});
|
|
249
|
+
this.checkIfError(response);
|
|
250
|
+
return response.PersonalizationIdentifiers as PlistDictionary;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Copy devices info (only for mounted images)
|
|
255
|
+
* @returns List of mounted devices
|
|
256
|
+
*/
|
|
257
|
+
async copyDevices(): Promise<any[]> {
|
|
258
|
+
const response = await this.sendRequest({ Command: 'CopyDevices' });
|
|
259
|
+
return (response.EntryList as any[]) || [];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Query personalization manifest from device
|
|
264
|
+
* @param imageType The image type
|
|
265
|
+
* @param signature The image signature/hash
|
|
266
|
+
* @returns Personalization manifest as Buffer
|
|
267
|
+
*/
|
|
268
|
+
async queryPersonalizationManifest(
|
|
269
|
+
imageType: string,
|
|
270
|
+
signature: Buffer,
|
|
271
|
+
): Promise<Buffer> {
|
|
272
|
+
try {
|
|
273
|
+
const response = await this.sendRequest({
|
|
274
|
+
Command: 'QueryPersonalizationManifest',
|
|
275
|
+
PersonalizedImageType: imageType,
|
|
276
|
+
ImageType: imageType,
|
|
277
|
+
ImageSignature: signature,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.checkIfError(response);
|
|
281
|
+
const manifest = response.ImageSignature;
|
|
282
|
+
|
|
283
|
+
if (!manifest || !Buffer.isBuffer(manifest)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
'MissingManifestError: Personalization manifest not found on device',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return manifest;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (
|
|
292
|
+
error instanceof Error &&
|
|
293
|
+
error.message.includes('MissingManifestError')
|
|
294
|
+
) {
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
throw new Error(
|
|
298
|
+
'MissingManifestError: Personalization manifest not found on device',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Upload image to device
|
|
305
|
+
* @param imageType The image type
|
|
306
|
+
* @param image The image data
|
|
307
|
+
* @param signature The image signature/manifest
|
|
308
|
+
* @param timeout Optional timeout for upload operation (defaults to 20000ms)
|
|
309
|
+
*/
|
|
310
|
+
async uploadImage(
|
|
311
|
+
imageType: string,
|
|
312
|
+
image: Buffer,
|
|
313
|
+
signature: Buffer,
|
|
314
|
+
timeout = MobileImageMounterService.UPLOAD_IMAGE_TIMEOUT,
|
|
315
|
+
): Promise<void> {
|
|
316
|
+
const receiveBytesResult = (await this.sendRequest({
|
|
317
|
+
Command: 'ReceiveBytes',
|
|
318
|
+
ImageType: imageType,
|
|
319
|
+
ImageSize: image.length,
|
|
320
|
+
ImageSignature: signature,
|
|
321
|
+
})) as BaseResponse;
|
|
322
|
+
|
|
323
|
+
this.checkIfError(receiveBytesResult);
|
|
324
|
+
|
|
325
|
+
if (receiveBytesResult.Status !== 'ReceiveBytesAck') {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Unexpected return from mobile_image_mounter: ${JSON.stringify(receiveBytesResult)}`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const conn = await this.connectToMobileImageMounterService();
|
|
332
|
+
const socket = conn.getSocket();
|
|
333
|
+
|
|
334
|
+
await new Promise<void>((resolve, reject) => {
|
|
335
|
+
socket.write(image, (error?: Error | null) =>
|
|
336
|
+
error ? reject(error) : resolve(),
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const uploadResult = await conn.receive(timeout);
|
|
341
|
+
if (uploadResult.Status !== 'Complete') {
|
|
342
|
+
throw new Error(`Image upload failed: ${JSON.stringify(uploadResult)}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
log.debug('Image uploaded successfully');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Mount image on device
|
|
350
|
+
* @param imageType The image type
|
|
351
|
+
* @param signature The image signature/manifest
|
|
352
|
+
* @param extras Additional parameters for mounting
|
|
353
|
+
*/
|
|
354
|
+
async mountImage(
|
|
355
|
+
imageType: string,
|
|
356
|
+
signature: Buffer,
|
|
357
|
+
extras?: Record<string, any>,
|
|
358
|
+
): Promise<void> {
|
|
359
|
+
const request = {
|
|
360
|
+
Command: 'MountImage',
|
|
361
|
+
ImageType: imageType,
|
|
362
|
+
ImageSignature: signature,
|
|
363
|
+
...extras,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const response = (await this.sendRequest(request)) as BaseResponse;
|
|
367
|
+
|
|
368
|
+
if (response.DetailedError?.includes('is already mounted')) {
|
|
369
|
+
log.info('Image was already mounted');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (response.DetailedError?.includes('Developer mode is not enabled')) {
|
|
374
|
+
throw new Error('Developer mode is not enabled on this device');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.checkIfError(response);
|
|
378
|
+
|
|
379
|
+
if (response.Status !== 'Complete') {
|
|
380
|
+
throw new Error(`Mount image failed: ${JSON.stringify(response)}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
log.debug('Image mounted successfully');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private async sendRequest(
|
|
387
|
+
request: PlistDictionary,
|
|
388
|
+
timeout?: number,
|
|
389
|
+
): Promise<PlistDictionary> {
|
|
390
|
+
const isNewConnection = !this.connection || this.isConnectionDestroyed();
|
|
391
|
+
const conn = await this.connectToMobileImageMounterService();
|
|
392
|
+
const res = await conn.sendPlistRequest(request, timeout);
|
|
393
|
+
|
|
394
|
+
if (isNewConnection && res?.Request === 'StartService') {
|
|
395
|
+
return await conn.receive();
|
|
396
|
+
}
|
|
397
|
+
return res;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Calculate hash of a buffer asynchronously
|
|
402
|
+
* @param buffer The buffer to hash
|
|
403
|
+
* @returns Promise resolving to the hash digest
|
|
404
|
+
*/
|
|
405
|
+
private async hashLargeBufferAsync(buffer: Buffer): Promise<Buffer> {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const hash = createHash('sha384');
|
|
408
|
+
const stream = Readable.from(buffer);
|
|
409
|
+
|
|
410
|
+
stream.on('data', (chunk) => {
|
|
411
|
+
hash.update(chunk);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
stream.on('end', () => {
|
|
415
|
+
resolve(hash.digest());
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
stream.on('error', (err) => {
|
|
419
|
+
reject(err);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async getOrRetrieveManifestFromTSS(
|
|
425
|
+
image: Buffer,
|
|
426
|
+
buildManifest: PlistDictionary,
|
|
427
|
+
): Promise<Buffer> {
|
|
428
|
+
try {
|
|
429
|
+
const imageHash = await this.hashLargeBufferAsync(image);
|
|
430
|
+
const manifest = await this.queryPersonalizationManifest(
|
|
431
|
+
'DeveloperDiskImage',
|
|
432
|
+
imageHash,
|
|
433
|
+
);
|
|
434
|
+
log.debug(
|
|
435
|
+
'Successfully retrieved existing personalization manifest from device',
|
|
436
|
+
);
|
|
437
|
+
return manifest;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if ((error as Error).message?.includes('MissingManifestError')) {
|
|
440
|
+
log.debug('Personalization manifest not found on device, using TSS...');
|
|
441
|
+
|
|
442
|
+
const identifiers = await this.queryPersonalizationIdentifiers();
|
|
443
|
+
const ecid = identifiers.UniqueChipID as number;
|
|
444
|
+
|
|
445
|
+
if (!ecid) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
'Could not retrieve device ECID from personalization identifiers',
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const manifest = await getManifestFromTSS(
|
|
452
|
+
ecid,
|
|
453
|
+
buildManifest,
|
|
454
|
+
() => this.queryPersonalizationIdentifiers(),
|
|
455
|
+
(type: string) => this.queryNonce(type),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
log.debug('Successfully generated manifest from TSS');
|
|
459
|
+
return manifest;
|
|
460
|
+
}
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private isConnectionDestroyed(): boolean {
|
|
466
|
+
try {
|
|
467
|
+
const socket = this.connection!.getSocket();
|
|
468
|
+
return !socket || socket.destroyed;
|
|
469
|
+
} catch {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async connectToMobileImageMounterService(): Promise<ServiceConnection> {
|
|
475
|
+
if (this.connection && !this.isConnectionDestroyed()) {
|
|
476
|
+
return this.connection;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const newConnection = await this.startLockdownService({
|
|
480
|
+
serviceName: MobileImageMounterService.RSD_SERVICE_NAME,
|
|
481
|
+
port: this.address[1].toString(),
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
this.connection = newConnection;
|
|
485
|
+
return newConnection;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private closeConnection(): void {
|
|
489
|
+
if (this.connection) {
|
|
490
|
+
try {
|
|
491
|
+
this.connection.close();
|
|
492
|
+
} catch {
|
|
493
|
+
// Ignore close errors
|
|
494
|
+
}
|
|
495
|
+
this.connection = null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private checkIfError(response: BaseResponse): void {
|
|
500
|
+
if (response.Error) {
|
|
501
|
+
throw new Error(response.Error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private async assertIsFile(
|
|
506
|
+
filePath: string,
|
|
507
|
+
fileType: string,
|
|
508
|
+
): Promise<Stats> {
|
|
509
|
+
try {
|
|
510
|
+
const fileStat = await fs.stat(filePath);
|
|
511
|
+
if (!fileStat.isFile()) {
|
|
512
|
+
throw new Error(`Expected ${fileType} file, got non-file: ${filePath}`);
|
|
513
|
+
}
|
|
514
|
+
return fileStat;
|
|
515
|
+
} catch (error: any) {
|
|
516
|
+
if (error.code === 'ENOENT') {
|
|
517
|
+
throw new Error(`${fileType} file not found: ${filePath}`);
|
|
518
|
+
}
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export default MobileImageMounterService;
|
|
525
|
+
export { MobileImageMounterService };
|
package/src/services.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { TunnelManager } from './lib/tunnel/index.js';
|
|
|
5
5
|
import { TunnelApiClient } from './lib/tunnel/tunnel-api-client.js';
|
|
6
6
|
import type {
|
|
7
7
|
DiagnosticsServiceWithConnection,
|
|
8
|
+
MobileImageMounterServiceWithConnection,
|
|
8
9
|
NotificationProxyServiceWithConnection,
|
|
9
10
|
SyslogService as SyslogServiceType,
|
|
10
11
|
} from './lib/types.js';
|
|
11
12
|
import DiagnosticsService from './services/ios/diagnostic-service/index.js';
|
|
13
|
+
import MobileImageMounterService from './services/ios/mobile-image-mounter/index.js';
|
|
12
14
|
import { NotificationProxyService } from './services/ios/notification-proxy/index.js';
|
|
13
15
|
import SyslogService from './services/ios/syslog-service/index.js';
|
|
14
16
|
|
|
@@ -47,6 +49,22 @@ export async function startNotificationProxyService(
|
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
export async function startMobileImageMounterService(
|
|
53
|
+
udid: string,
|
|
54
|
+
): Promise<MobileImageMounterServiceWithConnection> {
|
|
55
|
+
const { remoteXPC, tunnelConnection } = await createRemoteXPCConnection(udid);
|
|
56
|
+
const mobileImageMounterService = remoteXPC.findService(
|
|
57
|
+
MobileImageMounterService.RSD_SERVICE_NAME,
|
|
58
|
+
);
|
|
59
|
+
return {
|
|
60
|
+
remoteXPC: remoteXPC as RemoteXpcConnection,
|
|
61
|
+
mobileImageMounterService: new MobileImageMounterService([
|
|
62
|
+
tunnelConnection.host,
|
|
63
|
+
parseInt(mobileImageMounterService.port, 10),
|
|
64
|
+
]),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
export async function startSyslogService(
|
|
51
69
|
udid: string,
|
|
52
70
|
): Promise<SyslogServiceType> {
|