appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
240 lines (206 loc) • 7.72 kB
JavaScript
/**
* Image mounter script for mounting and unmounting Personalized Developer Disk Image on iOS devices
* This script provides a CLI interface to mount/unmount DDI via remote XPC services
*/
import {logger} from 'appium/support.js';
import {promises as fs} from 'fs';
import path from 'path';
import {Command} from 'commander';
const log = logger.getLogger('ImageMounter');
/**
* ImageMounter class for managing image mounting and unmounting operations
*/
class ImageMounter {
constructor() {
this.remoteXPCModule = null;
}
async initializeRemoteXPC() {
if (this.remoteXPCModule) {
return this.remoteXPCModule;
}
try {
this.remoteXPCModule = await import('appium-ios-remotexpc');
return this.remoteXPCModule;
} catch {
throw new Error(
'appium-ios-remotexpc is not installed. Please install it using:\n' +
'npm install appium-ios-remotexpc\n\n' +
'Note: This is an optional dependency required for image mounting functionality.'
);
}
}
async validateFile(filePath, fileType) {
const absolutePath = path.resolve(filePath);
try {
const stat = await fs.stat(absolutePath);
if (!stat.isFile()) {
throw new Error(`${fileType} path is not a file: ${absolutePath}`);
}
return absolutePath;
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`${fileType} file not found: ${absolutePath}`);
}
throw error;
}
}
async getTargetDevice(udid) {
const {createUsbmux} = await this.initializeRemoteXPC();
log.info('Connecting to usbmuxd...');
const usbmux = await createUsbmux();
const devices = await usbmux.listDevices();
if (devices.length === 0) {
await usbmux.close();
throw new Error(
'No iOS devices found. Ensure a device is connected, unlocked, and trusted. ' +
'See https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/ for details.'
);
}
let targetDevice;
if (udid) {
targetDevice = devices.find((device) => device.Properties.SerialNumber === udid);
if (!targetDevice) {
await usbmux.close();
throw new Error(
`Device with UDID ${udid} not found. Available devices:\n` +
devices.map((d) => ` - ${d.Properties.SerialNumber}`).join('\n')
);
}
} else {
if (devices.length > 1) {
log.warn(`Multiple devices found. Using first device: ${devices[0].Properties.SerialNumber}`);
log.warn('Available devices:');
devices.forEach((device) =>
log.warn(` - ${device.Properties.SerialNumber}`
));
log.warn('Use --udid flag to specify a particular device.');
}
targetDevice = devices[0];
}
return {usbmux, targetDevice};
}
async withImageMounterService(udid, fn) {
const {usbmux, targetDevice} = await this.getTargetDevice(udid);
const deviceUdid = targetDevice.Properties.SerialNumber;
try {
const remoteXPCModule = await this.initializeRemoteXPC();
const {
Services
} = remoteXPCModule;
log.info('Starting mobile image mounter service...');
const {
mobileImageMounterService: imageMounterService,
remoteXPC
} = await Services.startMobileImageMounterService(deviceUdid);
try {
return await fn(imageMounterService, deviceUdid);
} finally {
await imageMounterService.cleanup();
if (remoteXPC) {
log.info(`Closing remoteXPC connection for device ${deviceUdid}`);
await remoteXPC.close();
}
}
} finally {
await usbmux.close();
}
}
/**
* Mount Personalized Developer Disk Image on device
* @param {string} imagePath - Path to the .dmg file
* @param {string} manifestPath - Path to the BuildManifest.plist file
* @param {string} trustCachePath - Path to the .trustcache file
* @param {string} [udid] - Device UDID
*/
async mount(imagePath, manifestPath, trustCachePath, udid) {
const [validatedImagePath, validatedManifestPath, validatedTrustCachePath] = await Promise.all([
this.validateFile(imagePath, 'Image (.dmg)'),
this.validateFile(manifestPath, 'Build Manifest (.plist)'),
this.validateFile(trustCachePath, 'Trust Cache (.trustcache)')
]);
await this.withImageMounterService(udid, async (imageMounterService, deviceUdid) => {
log.info(`Mounting image on device: ${deviceUdid}`);
if (await imageMounterService.isPersonalizedImageMounted()) {
log.info('✅ Personalized image is already mounted on the device');
return;
}
await imageMounterService.mount(
validatedImagePath,
validatedManifestPath,
validatedTrustCachePath
);
log.info('✅ Image mounted successfully!');
});
}
/**
* Unmount image from device
* @param {string} [udid] - Device UDID
* @param {string} [mountPath='/System/Developer'] - Mount path to unmount
*/
async unmount(udid, mountPath = '/System/Developer') {
await this.withImageMounterService(udid, async (imageMounterService, deviceUdid) => {
log.info(`Unmounting image from device: ${deviceUdid}`);
log.info(`Mount path: ${mountPath}`);
await imageMounterService.unmountImage(mountPath);
log.info('✅ Image unmounted successfully!');
});
}
}
/**
* CLI with Commander.js
*/
async function main() {
const program = new Command();
const imageMounter = new ImageMounter();
program
.name('appium driver run xcuitest image-mounter')
.description('Mount and unmount Developer Disk Images on iOS devices')
.version('1.0.0')
.addHelpText('after', `
NOTE:
This script requires the 'appium-ios-remotexpc' package to be installed.
Install it using: npm install appium-ios-remotexpc`);
// Mount command
program
.command('mount')
.description('Mount a Developer Disk Image on iOS device')
.requiredOption('-i, --image <path>', 'Path to the .dmg image file')
.requiredOption('-m, --manifest <path>', 'Path to the BuildManifest.plist file')
.requiredOption('-t, --trustcache <path>', 'Path to the .trustcache file')
.option('-u, --udid <udid>', 'Target device UDID (optional, uses first device if not specified)')
.addHelpText('after', `
EXAMPLES:
# Mount Developer Disk Image
appium driver run xcuitest image-mounter mount --image DeveloperDiskImage.dmg --manifest BuildManifest.plist --trustcache DeveloperDiskImage.trustcache
# Mount on specific device
appium driver run xcuitest image-mounter mount --image DeveloperDiskImage.dmg --manifest BuildManifest.plist --trustcache DeveloperDiskImage.trustcache --udid <udid>`)
.action(async (options) => {
await imageMounter.mount(
options.image,
options.manifest,
options.trustcache,
options.udid
);
});
// Unmount command
program
.command('unmount')
.description('Unmount a Developer Disk Image from iOS device')
.option('-u, --udid <udid>', 'Target device UDID (optional, uses first device if not specified)')
.option('-p, --mount-path <path>', 'Mount path to unmount', '/System/Developer')
.addHelpText('after', `
EXAMPLES:
# Unmount from default path
appium driver run xcuitest image-mounter unmount
# Unmount from specific device
appium driver run xcuitest image-mounter unmount --udid <udid>`)
.action(async (options) => {
await imageMounter.unmount(
options.udid,
options.mountPath
);
});
await program.parseAsync(process.argv);
}
(async () => await main())();