141 lines
6.5 KiB
JavaScript
141 lines
6.5 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.runOnDevice = exports.getConnectedDevices = void 0;
|
|
const child_process_1 = require("child_process");
|
|
const Debug = require("debug");
|
|
const fs_1 = require("fs");
|
|
const path = require("path");
|
|
const errors_1 = require("../../errors");
|
|
const process_1 = require("../../utils/process");
|
|
const lib_1 = require("../lib");
|
|
const xcode_1 = require("./xcode");
|
|
const debug = Debug('native-run:ios:utils:device');
|
|
async function getConnectedDevices() {
|
|
const usbmuxClient = new lib_1.UsbmuxdClient(lib_1.UsbmuxdClient.connectUsbmuxdSocket());
|
|
const usbmuxDevices = await usbmuxClient.getDevices();
|
|
usbmuxClient.socket.end();
|
|
return Promise.all(usbmuxDevices.map(async (d) => {
|
|
const socket = await new lib_1.UsbmuxdClient(lib_1.UsbmuxdClient.connectUsbmuxdSocket()).connect(d, 62078);
|
|
const device = await new lib_1.LockdowndClient(socket).getAllValues();
|
|
socket.end();
|
|
// For network-connected devices, UniqueDeviceID may not be present in lockdownd response
|
|
// Use SerialNumber from usbmuxd device info as fallback (they are the same value)
|
|
if (!device.UniqueDeviceID && d.Properties && d.Properties.SerialNumber) {
|
|
device.UniqueDeviceID = d.Properties.SerialNumber;
|
|
debug(`Using SerialNumber as UniqueDeviceID for network device: ${device.UniqueDeviceID}`);
|
|
}
|
|
return device;
|
|
}));
|
|
}
|
|
exports.getConnectedDevices = getConnectedDevices;
|
|
async function runOnDevice(udid, appPath, bundleId, waitForApp) {
|
|
const clientManager = await lib_1.ClientManager.create(udid);
|
|
try {
|
|
await mountDeveloperDiskImage(clientManager);
|
|
const packageName = path.basename(appPath);
|
|
const destPackagePath = path.join('PublicStaging', packageName);
|
|
await uploadApp(clientManager, appPath, destPackagePath);
|
|
const installer = await clientManager.getInstallationProxyClient();
|
|
await installer.installApp(destPackagePath, bundleId);
|
|
const { [bundleId]: appInfo } = await installer.lookupApp([bundleId]);
|
|
// launch fails with EBusy or ENotFound if you try to launch immediately after install
|
|
await (0, process_1.wait)(200);
|
|
try {
|
|
const debugServerClient = await launchApp(clientManager, appInfo);
|
|
if (waitForApp) {
|
|
(0, process_1.onBeforeExit)(async () => {
|
|
// causes continue() to return
|
|
debugServerClient.halt();
|
|
// give continue() time to return response
|
|
await (0, process_1.wait)(64);
|
|
});
|
|
debug(`Waiting for app to close...\n`);
|
|
const result = await debugServerClient.continue();
|
|
// TODO: I have no idea what this packet means yet (successful close?)
|
|
// if not a close (ie, most likely due to halt from onBeforeExit), then kill the app
|
|
if (result !== 'W00') {
|
|
await debugServerClient.kill();
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
// if launching app throws, try with devicectl, but requires Xcode 15
|
|
const [xcodeVersion] = (0, xcode_1.getXcodeVersionInfo)();
|
|
const xcodeMajorVersion = Number(xcodeVersion.split('.')[0]);
|
|
if (xcodeMajorVersion >= 15) {
|
|
const launchResult = (0, child_process_1.spawn)('xcrun', ['devicectl', 'device', 'process', 'launch', '--device', udid, bundleId]);
|
|
return new Promise((resolve, reject) => {
|
|
launchResult.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
}
|
|
else {
|
|
reject(new errors_1.Exception(`There was an error launching app on device`));
|
|
}
|
|
});
|
|
launchResult.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
throw new errors_1.Exception(`running on iOS 17 devices requires Xcode 15 and later`);
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
clientManager.end();
|
|
}
|
|
}
|
|
exports.runOnDevice = runOnDevice;
|
|
async function mountDeveloperDiskImage(clientManager) {
|
|
const imageMounter = await clientManager.getMobileImageMounterClient();
|
|
// Check if already mounted. If not, mount.
|
|
if (!(await imageMounter.lookupImage()).ImageSignature) {
|
|
// verify DeveloperDiskImage exists (TODO: how does this work on Windows/Linux?)
|
|
// TODO: if windows/linux, download?
|
|
const version = await (await clientManager.getLockdowndClient()).getValue('ProductVersion');
|
|
const developerDiskImagePath = await (0, xcode_1.getDeveloperDiskImagePath)(version);
|
|
const developerDiskImageSig = (0, fs_1.readFileSync)(`${developerDiskImagePath}.signature`);
|
|
await imageMounter.uploadImage(developerDiskImagePath, developerDiskImageSig);
|
|
await imageMounter.mountImage(developerDiskImagePath, developerDiskImageSig);
|
|
}
|
|
}
|
|
async function uploadApp(clientManager, srcPath, destinationPath) {
|
|
const afcClient = await clientManager.getAFCClient();
|
|
try {
|
|
await afcClient.getFileInfo('PublicStaging');
|
|
}
|
|
catch (err) {
|
|
if (err instanceof lib_1.AFCError && err.status === lib_1.AFC_STATUS.OBJECT_NOT_FOUND) {
|
|
await afcClient.makeDirectory('PublicStaging');
|
|
}
|
|
else {
|
|
throw err;
|
|
}
|
|
}
|
|
await afcClient.uploadDirectory(srcPath, destinationPath);
|
|
}
|
|
async function launchApp(clientManager, appInfo) {
|
|
let tries = 0;
|
|
while (tries < 3) {
|
|
const debugServerClient = await clientManager.getDebugserverClient();
|
|
await debugServerClient.setMaxPacketSize(1024);
|
|
await debugServerClient.setWorkingDir(appInfo.Container);
|
|
await debugServerClient.launchApp(appInfo.Path, appInfo.CFBundleExecutable);
|
|
const result = await debugServerClient.checkLaunchSuccess();
|
|
if (result === 'OK') {
|
|
return debugServerClient;
|
|
}
|
|
else if (result === 'EBusy' || result === 'ENotFound') {
|
|
debug('Device busy or app not found, trying to launch again in .5s...');
|
|
tries++;
|
|
debugServerClient.socket.end();
|
|
await (0, process_1.wait)(500);
|
|
}
|
|
else {
|
|
throw new errors_1.Exception(`There was an error launching app: ${result}`);
|
|
}
|
|
}
|
|
throw new errors_1.Exception('Unable to launch app, number of tries exceeded');
|
|
}
|