native-run
Advanced tools
Comparing version 0.0.7 to 0.0.10
@@ -6,7 +6,7 @@ "use strict"; | ||
Run an APK on a device or emulator target | ||
Run an .apk on a device or emulator target | ||
Targets are selected as follows: | ||
1) --target using device/emulator serial number or AVD ID | ||
2) A connected device, unless --emulator is used | ||
2) A connected device, unless --virtual is used | ||
3) A running emulator | ||
@@ -23,6 +23,10 @@ | ||
--sdk-info ........... Print SDK information, then quit | ||
--json ............... Output JSON | ||
--apk <path> ......... Deploy specified APK file | ||
--app <path> ......... Deploy specified .apk file | ||
--device ............. Use a device if available | ||
--emulator ........... Prefer an emulator | ||
With --list prints connected devices | ||
--virtual ............ Prefer an emulator | ||
With --list prints available emulators | ||
--target <id> ........ Use a specific target | ||
@@ -29,0 +33,0 @@ --connect ............ Tie process to app process |
@@ -9,4 +9,5 @@ "use strict"; | ||
if (args.includes('--list')) { | ||
const cmd = await Promise.resolve().then(() => require('./list')); | ||
return cmd.run(args); | ||
const list = await Promise.resolve().then(() => require('./list')); | ||
process.stdout.write(await list.run(args)); | ||
return; | ||
} | ||
@@ -13,0 +14,0 @@ if (args.includes('--sdk-info')) { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const list_1 = require("../utils/list"); | ||
const adb_1 = require("./utils/adb"); | ||
@@ -8,5 +9,16 @@ const avd_1 = require("./utils/avd"); | ||
const sdk = await sdk_1.getSDK(); | ||
const devices = (await adb_1.getDevices(sdk)) | ||
const [devices, virtualDevices] = await Promise.all([ | ||
getDeviceTargets(sdk), | ||
getVirtualTargets(sdk), | ||
]); | ||
return list_1.list(args, devices, virtualDevices); | ||
} | ||
exports.run = run; | ||
async function getDeviceTargets(sdk) { | ||
return (await adb_1.getDevices(sdk)) | ||
.filter(device => device.type === 'hardware') | ||
.map(device => deviceToTarget(device)); | ||
.map(deviceToTarget); | ||
} | ||
exports.getDeviceTargets = getDeviceTargets; | ||
async function getVirtualTargets(sdk) { | ||
const avds = await avd_1.getInstalledAVDs(sdk); | ||
@@ -17,27 +29,5 @@ const defaultAvd = await avd_1.getDefaultAVD(sdk, avds); | ||
} | ||
const virtualDevices = avds.map(avd => avdToTarget(avd)); | ||
if (args.includes('--json')) { | ||
process.stdout.write(JSON.stringify({ devices, virtualDevices })); | ||
return; | ||
} | ||
process.stdout.write('Devices:\n\n'); | ||
if (devices.length === 0) { | ||
process.stdout.write(' No connected devices found\n'); | ||
} | ||
else { | ||
for (const device of devices) { | ||
process.stdout.write(` ${formatTarget(device)}\n`); | ||
} | ||
} | ||
process.stdout.write('\nVirtual Devices:\n\n'); | ||
if (virtualDevices.length === 0) { | ||
process.stdout.write(' No virtual devices found\n'); | ||
} | ||
else { | ||
for (const avd of virtualDevices) { | ||
process.stdout.write(` ${formatTarget(avd)}\n`); | ||
} | ||
} | ||
return avds.map(avdToTarget); | ||
} | ||
exports.run = run; | ||
exports.getVirtualTargets = getVirtualTargets; | ||
function deviceToTarget(device) { | ||
@@ -48,2 +38,5 @@ return { | ||
id: device.serial, | ||
format() { | ||
return `${this.model} (API ${this.sdkVersion}) ${this.id}`; | ||
}, | ||
}; | ||
@@ -56,8 +49,6 @@ } | ||
id: avd.id, | ||
format() { | ||
return `${this.name} (API ${this.sdkVersion}) ${this.id}`; | ||
}, | ||
}; | ||
} | ||
function formatTarget(target) { | ||
return ` | ||
${target.name ? `${target.name} ` : ''}${target.model ? `${target.model} ` : ''}(API ${target.sdkVersion}) ${target.id} | ||
`.trim(); | ||
} |
@@ -8,2 +8,3 @@ "use strict"; | ||
const adb_1 = require("./utils/adb"); | ||
const apk_1 = require("./utils/apk"); | ||
const avd_1 = require("./utils/avd"); | ||
@@ -14,10 +15,4 @@ const run_1 = require("./utils/run"); | ||
const sdk = await sdk_1.getSDK(); | ||
const apk = cli_1.getOptionValue(args, '--apk'); | ||
// TODO: get application id and activity from apk | ||
const app = cli_1.getOptionValue(args, '--app'); | ||
const activity = cli_1.getOptionValue(args, '--activity', '.MainActivity'); | ||
if (!apk) { | ||
throw new errors_1.RunException('--apk is required'); | ||
} | ||
if (!app) { | ||
const apkPath = cli_1.getOptionValue(args, '--app'); | ||
if (!apkPath) { | ||
throw new errors_1.RunException('--app is required'); | ||
@@ -27,13 +22,14 @@ } | ||
log_1.log(`Selected ${device.type === 'hardware' ? 'hardware device' : 'emulator'} ${device.serial}\n`); | ||
const { appId, activityName } = await apk_1.getApkInfo(apkPath); | ||
await adb_1.waitForBoot(sdk, device); | ||
await run_1.installApkToDevice(sdk, device, apk, app); | ||
log_1.log(`Starting application activity ${app}/${activity}...\n`); | ||
await adb_1.startActivity(sdk, device, app, activity); | ||
await run_1.installApkToDevice(sdk, device, apkPath, appId); | ||
log_1.log(`Starting application activity ${appId}/${activityName}...\n`); | ||
await adb_1.startActivity(sdk, device, appId, activityName); | ||
log_1.log(`Run Successful\n`); | ||
if (args.includes('--connect')) { | ||
process_1.onBeforeExit(async () => { | ||
await adb_1.closeApp(sdk, device, app); | ||
await adb_1.closeApp(sdk, device, appId); | ||
}); | ||
log_1.log(`Waiting for app to close...\n`); | ||
await adb_1.waitForClose(sdk, device, app); | ||
await adb_1.waitForClose(sdk, device, appId); | ||
} | ||
@@ -46,3 +42,3 @@ } | ||
const target = cli_1.getOptionValue(args, '--target'); | ||
const preferEmulator = args.includes('--emulator'); | ||
const preferEmulator = args.includes('--virtual'); | ||
if (target) { | ||
@@ -49,0 +45,0 @@ const targetDevice = await run_1.selectDeviceByTarget(sdk, devices, avds, target); |
@@ -73,3 +73,3 @@ "use strict"; | ||
exports.selectVirtualDevice = selectVirtualDevice; | ||
async function installApkToDevice(sdk, device, apk, app) { | ||
async function installApkToDevice(sdk, device, apk, appId) { | ||
process.stdout.write(`Installing ${apk}...\n`); | ||
@@ -83,3 +83,3 @@ try { | ||
process.stdout.write(`${e.message} Uninstalling and trying again...\n`); | ||
await adb_1.uninstallApp(sdk, device, app); | ||
await adb_1.uninstallApp(sdk, device, appId); | ||
await adb_1.installApk(sdk, device, apk); | ||
@@ -86,0 +86,0 @@ return; |
@@ -11,2 +11,3 @@ "use strict"; | ||
--verbose ............ Print verbose output to stderr | ||
--list ............... Print connected devices and virtual devices | ||
@@ -13,0 +14,0 @@ `; |
@@ -24,2 +24,5 @@ "use strict"; | ||
} | ||
else if (platform === '--list') { | ||
await list(args); | ||
} | ||
else { | ||
@@ -40,2 +43,20 @@ if (!platform || platform === 'help' || args.includes('--help') || args.includes('-h') || platform.startsWith('-')) { | ||
exports.run = run; | ||
async function list(args) { | ||
const [iosOutput, androidOutput] = await Promise.all([ | ||
Promise.resolve().then(() => require('./ios/list')).then(iosList => iosList.run(args)), | ||
Promise.resolve().then(() => require('./android/list')).then(androidList => androidList.run(args)), | ||
]); | ||
if (!args.includes('--json')) { | ||
process.stdout.write(`iOS ${iosOutput}\n`); | ||
process.stdout.write(`Android ${androidOutput}`); | ||
} | ||
else { | ||
const adjustLines = (output) => output.split('\n').map(line => ` ${line}`).join('\n').trim(); | ||
process.stdout.write(` | ||
{ | ||
"ios": ${adjustLines(iosOutput)}, | ||
"android": ${adjustLines(androidOutput)} | ||
}`); | ||
} | ||
} | ||
function serializeError(e) { | ||
@@ -42,0 +63,0 @@ let error; |
@@ -6,8 +6,31 @@ "use strict"; | ||
Run an .app or .ipa on a device or simulator target | ||
Targets are selected as follows: | ||
1) --target using device/simulator UUID | ||
2) A connected device, unless --virtual is used | ||
3) A running simulator | ||
If the above criteria are not met, the app is run on the default simulator | ||
(the last simulator in the list). | ||
Use --list to list available targets. | ||
Options: | ||
--list ............... Print available targets, then quit | ||
--json ............... Output JSON | ||
--app <path> ......... Deploy specified .app or .ipa file | ||
--device ............. Use a device if available | ||
With --list prints connected devices | ||
--virtual ............ Prefer a simulator | ||
With --list prints available simulators | ||
--target <id> ........ Use a specific target | ||
--connect ............ Tie process to app process | ||
`; | ||
async function run() { | ||
process.stdout.write(help); | ||
process.stdout.write(`${help}\n`); | ||
} | ||
exports.run = run; |
@@ -10,3 +10,4 @@ "use strict"; | ||
const list = await Promise.resolve().then(() => require('./list')); | ||
return list.run(args); | ||
process.stdout.write(await list.run(args)); | ||
return; | ||
} | ||
@@ -13,0 +14,0 @@ const runCmd = await Promise.resolve().then(() => require('./run')); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const list_1 = require("../utils/list"); | ||
const device_1 = require("./utils/device"); | ||
const simulator_1 = require("./utils/simulator"); | ||
async function run(args) { | ||
// TODO check for darwin? | ||
const simulators = await device_1.getSimulators(); | ||
const devices = await device_1.getConnectedDevicesInfo(); | ||
if (args.includes('--json')) { | ||
const result = { devices, simulators }; | ||
process.stdout.write(JSON.stringify(result, undefined, 2) + '\n'); | ||
return; | ||
} | ||
process.stdout.write('Devices:\n\n'); | ||
if (devices.length === 0) { | ||
process.stdout.write(' No connected devices found\n'); | ||
} | ||
else { | ||
for (const device of devices) { | ||
process.stdout.write(` ${formatDevice(device)}\n`); | ||
} | ||
} | ||
process.stdout.write('\nSimulators:\n\n'); | ||
for (const sim of simulators) { | ||
process.stdout.write(` ${formatSimulator(sim)}\n`); | ||
} | ||
const [devices, simulators] = await Promise.all([ | ||
(await device_1.getConnectedDevices()).map(deviceToTarget), | ||
(await simulator_1.getSimulators()).map(simulatorToTarget), | ||
]); | ||
return list_1.list(args, devices, simulators); | ||
} | ||
exports.run = run; | ||
function formatDevice(device) { | ||
return ` | ||
${device.name} ${device.model} (${device.sdkVersion}) ${device.id} | ||
`.trim(); | ||
function deviceToTarget(device) { | ||
return { | ||
name: device.DeviceName, | ||
model: device.ProductType, | ||
sdkVersion: device.ProductVersion, | ||
id: device.UniqueDeviceID, | ||
format() { | ||
return `${this.name} ${this.model} ${this.sdkVersion} ${this.id}`; | ||
}, | ||
}; | ||
} | ||
function formatSimulator(sim) { | ||
return ` | ||
${sim.name} (${sim.sdkVersion}) ${sim.id} | ||
`.trim(); | ||
function simulatorToTarget(simulator) { | ||
return { | ||
name: simulator.name, | ||
sdkVersion: simulator.runtime.version, | ||
id: simulator.udid, | ||
format() { | ||
return `${this.name} ${this.sdkVersion} ${this.id}`; | ||
}, | ||
}; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const utils_fs_1 = require("@ionic/utils-fs"); | ||
const child_process_1 = require("child_process"); | ||
const Debug = require("debug"); | ||
const fs_1 = require("fs"); | ||
const node_ioslib_1 = require("node-ioslib"); | ||
const path = require("path"); | ||
const util_1 = require("util"); | ||
const errors_1 = require("../errors"); | ||
const cli_1 = require("../utils/cli"); | ||
const process_1 = require("../utils/process"); | ||
const app_1 = require("./utils/app"); | ||
const device_1 = require("./utils/device"); | ||
const simulator_1 = require("./utils/simulator"); | ||
const debug = Debug('native-run:ios:run'); | ||
const wait = util_1.promisify(setTimeout); | ||
async function run(args) { | ||
let appPath = cli_1.getOptionValue(args, '--app'); | ||
if (!appPath) { | ||
throw new errors_1.RunException('--app argument is required.'); | ||
throw new errors_1.Exception('--app argument is required.'); | ||
} | ||
const udid = cli_1.getOptionValue(args, '--target'); | ||
const preferSimulator = args.includes('--simulator'); | ||
const preferSimulator = args.includes('--virtual'); | ||
const waitForApp = args.includes('--connect'); | ||
@@ -30,20 +26,20 @@ const isIPA = appPath.endsWith('.ipa'); | ||
debug(`Unzipping .ipa to ${tempDir}`); | ||
const appDir = await unzipApp(appPath, tempDir); | ||
const appDir = await app_1.unzipIPA(appPath, tempDir); | ||
appPath = path.join(tempDir, appDir); | ||
} | ||
const bundleId = await getBundleId(appPath); | ||
const bundleId = await app_1.getBundleId(appPath); | ||
const [devices, simulators] = await Promise.all([ | ||
device_1.getConnectedDevicesInfo(), | ||
device_1.getSimulators(), | ||
device_1.getConnectedDevices(), | ||
simulator_1.getSimulators(), | ||
]); | ||
// try to run on device or simulator with udid | ||
if (udid) { | ||
if (devices.find(d => d.id === udid)) { | ||
await runOnDevice(udid, appPath, bundleId, waitForApp); | ||
if (devices.find(d => d.UniqueDeviceID === udid)) { | ||
await device_1.runOnDevice(udid, appPath, bundleId, waitForApp); | ||
} | ||
else if (simulators.find(s => s.id === udid)) { | ||
await runOnSimulator(udid, appPath, bundleId, waitForApp); | ||
else if (simulators.find(s => s.udid === udid)) { | ||
await simulator_1.runOnSimulator(udid, appPath, bundleId, waitForApp); | ||
} | ||
else { | ||
throw new Error(`No device or simulator with udid ${udid} found`); | ||
throw new errors_1.Exception(`No device or simulator with udid ${udid} found`); | ||
} | ||
@@ -53,7 +49,7 @@ } | ||
// no udid, use first connected device | ||
await runOnDevice(devices[0].id, appPath, bundleId, waitForApp); | ||
await device_1.runOnDevice(devices[0].UniqueDeviceID, appPath, bundleId, waitForApp); | ||
} | ||
else { | ||
// use default sim | ||
await runOnSimulator(simulators[simulators.length - 1].id, appPath, bundleId, waitForApp); | ||
await simulator_1.runOnSimulator(simulators[simulators.length - 1].udid, appPath, bundleId, waitForApp); | ||
} | ||
@@ -71,207 +67,1 @@ } | ||
exports.run = run; | ||
async function runOnSimulator(udid, appPath, bundleId, waitForApp) { | ||
debug(`Booting simulator ${udid}`); | ||
const bootResult = child_process_1.spawnSync('xcrun', ['simctl', 'boot', udid], { encoding: 'utf8' }); | ||
// TODO: is there a better way to check this? | ||
if (bootResult.status && !bootResult.stderr.includes('Unable to boot device in current state: Booted')) { | ||
throw new Error(`There was an error booting simulator: ${bootResult.stderr}`); | ||
} | ||
debug(`Installing ${appPath} on ${udid}`); | ||
const installResult = child_process_1.spawnSync('xcrun', ['simctl', 'install', udid, appPath], { encoding: 'utf8' }); | ||
if (installResult.status) { | ||
throw new Error(`There was an error installing app on simulator: ${installResult.stderr}`); | ||
} | ||
const xCodePath = await getXCodePath(); | ||
debug(`Running simulator ${udid}`); | ||
const openResult = child_process_1.spawnSync('open', [`${xCodePath}/Applications/Simulator.app`, '--args', '-CurrentDeviceUDID', udid], { encoding: 'utf8' }); | ||
if (openResult.status) { | ||
throw new Error(`There was an error opening simulator: ${openResult.stderr}`); | ||
} | ||
debug(`Launching ${appPath} on ${udid}`); | ||
const launchResult = child_process_1.spawnSync('xcrun', ['simctl', 'launch', udid, bundleId], { encoding: 'utf8' }); | ||
if (launchResult.status) { | ||
throw new Error(`There was an error launching app on simulator: ${launchResult.stderr}`); | ||
} | ||
if (waitForApp) { | ||
process_1.onBeforeExit(async () => { | ||
const terminateResult = child_process_1.spawnSync('xcrun', ['simctl', 'terminate', udid, bundleId], { encoding: 'utf8' }); | ||
if (terminateResult.status) { | ||
debug('Unable to terminate app on simulator'); | ||
} | ||
}); | ||
process.stdout.write(`Waiting for app to close...\n`); | ||
await waitForSimulatorClose(udid, bundleId); | ||
} | ||
} | ||
async function waitForSimulatorClose(udid, bundleId) { | ||
return new Promise(resolve => { | ||
// poll service list for bundle id | ||
const interval = setInterval(async () => { | ||
try { | ||
const data = child_process_1.spawnSync('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'list'], { encoding: 'utf8' }); | ||
// if bundle id isn't in list, app isn't running | ||
if (data.stdout.indexOf(bundleId) === -1) { | ||
clearInterval(interval); | ||
resolve(); | ||
} | ||
} | ||
catch (e) { | ||
debug('Error received from launchctl: %O', e); | ||
debug('App %s no longer found in process list for %s', bundleId, udid); | ||
clearInterval(interval); | ||
resolve(); | ||
} | ||
}, 500); | ||
}); | ||
} | ||
async function runOnDevice(udid, appPath, bundleId, waitForApp) { | ||
const clientManager = await node_ioslib_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 wait(200); | ||
const debugServerClient = await launchApp(clientManager, appInfo); | ||
if (waitForApp) { | ||
process_1.onBeforeExit(async () => { | ||
// causes continue() to return | ||
debugServerClient.halt(); | ||
// give continue() time to return response | ||
await 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(); | ||
} | ||
} | ||
} | ||
finally { | ||
clientManager.end(); | ||
} | ||
} | ||
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 getDeveloperDiskImagePath(version); | ||
const developerDiskImageSig = 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 node_ioslib_1.AFCError && err.status === node_ioslib_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 wait(500); | ||
} | ||
else { | ||
throw new errors_1.RunException(`There was an error launching app: ${result}`); | ||
} | ||
} | ||
throw new errors_1.RunException('Unable to launch app, number of tries exceeded'); | ||
} | ||
async function getXCodePath() { | ||
try { | ||
const { stdout } = await process_1.execFile('xcode-select', ['-p'], { encoding: 'utf8' }); | ||
if (stdout) { | ||
return stdout.trim(); | ||
} | ||
} | ||
catch (_a) { } // tslint:disable-line | ||
throw new Error('Unable to get Xcode location. Is Xcode installed?'); | ||
} | ||
async function getDeveloperDiskImagePath(version) { | ||
const xCodePath = await getXCodePath(); | ||
const versionDirs = await utils_fs_1.readDir(`${xCodePath}/Platforms/iPhoneOS.platform/DeviceSupport/`); | ||
const versionPrefix = version.match(/\d+\.\d+/); | ||
if (versionPrefix === null) { | ||
throw new Error(`Invalid iOS version: ${version}`); | ||
} | ||
// Can look like "11.2 (15C107)" | ||
for (const dir of versionDirs) { | ||
if (dir.includes(versionPrefix[0])) { | ||
return `${xCodePath}/Platforms/iPhoneOS.platform/DeviceSupport/${dir}/DeveloperDiskImage.dmg`; | ||
} | ||
} | ||
throw new Error(`Unable to find Developer Disk Image path for SDK ${version}. Do you have the right version of Xcode?`); | ||
} | ||
// TODO: cross platform? Use plist/bplist | ||
async function getBundleId(packagePath) { | ||
const plistPath = path.resolve(packagePath, 'Info.plist'); | ||
try { | ||
const { stdout } = await process_1.execFile('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', plistPath], { encoding: 'utf8' }); | ||
if (stdout) { | ||
return stdout.trim(); | ||
} | ||
} | ||
catch (_a) { } // tslint:disable-line | ||
throw new Error('Unable to get app bundle identifier'); | ||
} | ||
async function unzipApp(srcPath, destPath) { | ||
const yauzl = await Promise.resolve().then(() => require('yauzl')); | ||
const open = util_1.promisify(yauzl.open.bind(yauzl)); | ||
let appDir = ''; | ||
return new Promise(async (resolve, reject) => { | ||
const zipfile = await open(srcPath, { lazyEntries: true }); | ||
const openReadStream = util_1.promisify(zipfile.openReadStream.bind(zipfile)); | ||
zipfile.once('error', reject); | ||
zipfile.once('end', () => { appDir ? resolve(appDir) : reject('Unable to determine .app directory from .ipa'); }); | ||
zipfile.readEntry(); | ||
zipfile.on('entry', async (entry) => { | ||
debug(`Unzip: ${entry.fileName}`); | ||
const dest = path.join(destPath, entry.fileName); | ||
if (entry.fileName.endsWith('/')) { | ||
await utils_fs_1.mkdirp(dest); | ||
if (entry.fileName.endsWith('.app/')) { | ||
appDir = entry.fileName; | ||
} | ||
zipfile.readEntry(); | ||
} | ||
else { | ||
await utils_fs_1.mkdirp(path.dirname(dest)); | ||
const readStream = await openReadStream(entry); | ||
readStream.on('end', () => { zipfile.readEntry(); }); | ||
const writeStream = fs_1.createWriteStream(path.join(destPath, entry.fileName)); | ||
readStream.pipe(writeStream); | ||
} | ||
}); | ||
}); | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const child_process_1 = require("child_process"); // TODO: need cross-spawn for windows? | ||
const Debug = require("debug"); | ||
const fs_1 = require("fs"); | ||
const node_ioslib_1 = require("node-ioslib"); | ||
async function getSimulators() { | ||
const simctl = child_process_1.spawnSync('xcrun', ['simctl', 'list', '--json'], { encoding: 'utf8' }); | ||
const output = JSON.parse(simctl.stdout); | ||
return output.runtimes | ||
.filter(runtime => runtime.name.indexOf('watch') === -1 && runtime.name.indexOf('tv') === -1) | ||
.map(runtime => output.devices[runtime.name] | ||
.filter(device => !device.availability.includes('unavailable')) | ||
.map(device => ({ | ||
name: device.name, | ||
sdkVersion: runtime.version, | ||
id: device.udid, | ||
}))) | ||
.reduce((prev, next) => prev.concat(next)) // flatten array of runtime devices arrays | ||
.sort((a, b) => a.name < b.name ? -1 : 1); | ||
} | ||
exports.getSimulators = getSimulators; | ||
async function getConnectedDevicesInfo() { | ||
const path = require("path"); | ||
const util_1 = require("util"); | ||
const errors_1 = require("../../errors"); | ||
const process_1 = require("../../utils/process"); | ||
const path_1 = require("./path"); | ||
const debug = Debug('native-run:ios:utils:device'); | ||
const wait = util_1.promisify(setTimeout); | ||
async function getConnectedDevices() { | ||
const usbmuxClient = new node_ioslib_1.UsbmuxdClient(node_ioslib_1.UsbmuxdClient.connectUsbmuxdSocket()); | ||
const devices = await usbmuxClient.getDevices(); | ||
const usbmuxDevices = await usbmuxClient.getDevices(); | ||
usbmuxClient.socket.end(); | ||
const deviceInfos = await Promise.all(devices.map(async (device) => { | ||
const socket = await new node_ioslib_1.UsbmuxdClient(node_ioslib_1.UsbmuxdClient.connectUsbmuxdSocket()).connect(device, 62078); | ||
const deviceInfo = await new node_ioslib_1.LockdowndClient(socket).getAllValues(); | ||
return Promise.all(usbmuxDevices.map(async (d) => { | ||
const socket = await new node_ioslib_1.UsbmuxdClient(node_ioslib_1.UsbmuxdClient.connectUsbmuxdSocket()).connect(d, 62078); | ||
const device = await new node_ioslib_1.LockdowndClient(socket).getAllValues(); | ||
socket.end(); | ||
return deviceInfo; | ||
return device; | ||
})); | ||
return deviceInfos.map(deviceInfo => ({ | ||
name: deviceInfo.DeviceName, | ||
model: deviceInfo.ProductType, | ||
sdkVersion: deviceInfo.ProductVersion, | ||
id: deviceInfo.UniqueDeviceID, | ||
})); | ||
} | ||
exports.getConnectedDevicesInfo = getConnectedDevicesInfo; | ||
exports.getConnectedDevices = getConnectedDevices; | ||
async function runOnDevice(udid, appPath, bundleId, waitForApp) { | ||
const clientManager = await node_ioslib_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 wait(200); | ||
const debugServerClient = await launchApp(clientManager, appInfo); | ||
if (waitForApp) { | ||
process_1.onBeforeExit(async () => { | ||
// causes continue() to return | ||
debugServerClient.halt(); | ||
// give continue() time to return response | ||
await 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(); | ||
} | ||
} | ||
} | ||
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 path_1.getDeveloperDiskImagePath(version); | ||
const developerDiskImageSig = 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 node_ioslib_1.AFCError && err.status === node_ioslib_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 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'); | ||
} |
{ | ||
"name": "native-run", | ||
"version": "0.0.7", | ||
"version": "0.0.10", | ||
"description": "A CLI for running apps on iOS/Android devices and simulators/emulators", | ||
@@ -5,0 +5,0 @@ "bin": { |
107687
41
2674
5
21