Skip to content

Commit

Permalink
feat(iOS): Add 5 second retry on DeviceLocked for iOS (#167)
Browse files Browse the repository at this point in the history
Co-authored-by: Joseph Pender <joey@prodcode.net>
Co-authored-by: Dan Imhoff <dwieeb@gmail.com>
  • Loading branch information
3 people committed Dec 7, 2020
1 parent b0b8978 commit f451e46
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 33 deletions.
4 changes: 3 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const ERR_SDK_UNSATISFIED_PACKAGES = 'ERR_SDK_UNSATISFIED_PACKAGES';
export const ERR_TARGET_NOT_FOUND = 'ERR_TARGET_NOT_FOUND';
export const ERR_NO_DEVICE = 'ERR_NO_DEVICE';
export const ERR_NO_TARGET = 'ERR_NO_TARGET';
export const ERR_DEVICE_LOCKED = 'ERR_DEVICE_LOCKED';
export const ERR_UNKNOWN_AVD = 'ERR_UNKNOWN_AVD';
export const ERR_UNSUPPORTED_API_LEVEL = 'ERR_UNSUPPORTED_API_LEVEL';

Expand Down Expand Up @@ -123,7 +124,8 @@ export class SDKException extends AndroidException<SDKExceptionCode> {}
export type IOSRunExceptionCode =
| typeof ERR_TARGET_NOT_FOUND
| typeof ERR_NO_DEVICE
| typeof ERR_NO_TARGET;
| typeof ERR_NO_TARGET
| typeof ERR_DEVICE_LOCKED;

export class IOSRunException extends Exception<IOSRunExceptionCode> {}

Expand Down
10 changes: 10 additions & 0 deletions src/ios/lib/lib-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Type union of error codes we get back from the protocol.
*/
export type IOSLibErrorCode = 'DeviceLocked';

export class IOSLibError extends Error implements NodeJS.ErrnoException {
constructor(message: string, readonly code: IOSLibErrorCode) {
super(message);
}
}
6 changes: 6 additions & 0 deletions src/ios/lib/protocol/lockdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as Debug from 'debug';
import type * as net from 'net';
import * as plist from 'plist';

import { IOSLibError } from '../lib-errors';

import type { ProtocolWriter } from './protocol';
import {
PlistProtocolReader,
Expand Down Expand Up @@ -70,6 +72,10 @@ export class LockdownProtocolReader extends PlistProtocolReader {
const resp = super.parseBody(data);
debug(`Response: ${JSON.stringify(resp)}`);
if (isLockdownErrorResponse(resp)) {
if (resp.Error === 'DeviceLocked') {
throw new IOSLibError('Device is currently locked.', 'DeviceLocked');
}

throw new Error(resp.Error);
}
return resp;
Expand Down
131 changes: 103 additions & 28 deletions src/ios/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,109 @@ import * as path from 'path';
import {
CLIException,
ERR_BAD_INPUT,
ERR_DEVICE_LOCKED,
ERR_TARGET_NOT_FOUND,
IOSRunException,
} from '../errors';
import { getOptionValue } from '../utils/cli';
import { wait } from '../utils/process';

import type { DeviceValues } from './lib';
import { IOSLibError } from './lib/lib-errors';
import { getBundleId, unzipIPA } from './utils/app';
import { getConnectedDevices, runOnDevice } from './utils/device';
import type { SimulatorResult } from './utils/simulator';
import { getSimulators, runOnSimulator } from './utils/simulator';

const debug = Debug('native-run:ios:run');

interface IOSRunConfig {
udid?: string;
devices: DeviceValues[];
simulators: SimulatorResult[];
appPath: string;
bundleId: string;
waitForApp: boolean;
preferSimulator: boolean;
}

async function runIpaOrAppFile({
udid,
devices,
simulators,
appPath,
bundleId,
waitForApp,
preferSimulator,
}: IOSRunConfig): Promise<void> {
if (udid) {
if (devices.find(d => d.UniqueDeviceID === udid)) {
await runOnDevice(udid, appPath, bundleId, waitForApp);
} else if (simulators.find(s => s.udid === udid)) {
await runOnSimulator(udid, appPath, bundleId, waitForApp);
} else {
throw new IOSRunException(
`No device or simulator with UDID "${udid}" found`,
ERR_TARGET_NOT_FOUND,
);
}
} else if (devices.length && !preferSimulator) {
// no udid, use first connected device
await runOnDevice(devices[0].UniqueDeviceID, appPath, bundleId, waitForApp);
} else {
// use default sim
await runOnSimulator(
simulators[simulators.length - 1].udid,
appPath,
bundleId,
waitForApp,
);
}
}

async function runIpaOrAppFileOnInterval(config: IOSRunConfig): Promise<void> {
const maxRetryCount = 12; // 1 minute
const retryInterval = 5000; // 5 seconds
let error: Error | undefined;
let retryCount = 0;

const retry = async () => {
process.stderr.write('Please unlock your device. Waiting 5 seconds...\n');
retryCount++;
await wait(retryInterval);
await run();
};

const run = async () => {
try {
await runIpaOrAppFile(config);
} catch (err) {
if (
err instanceof IOSLibError &&
err.code == 'DeviceLocked' &&
retryCount < maxRetryCount
) {
await retry();
} else {
if (maxRetryCount >= retryCount) {
error = new IOSRunException(
`Device still locked after 1 minute. Aborting.`,
ERR_DEVICE_LOCKED,
);
} else {
error = err;
}
}
}
};

await run();

if (error) {
throw error;
}
}

export async function run(args: readonly string[]): Promise<void> {
let appPath = getOptionValue(args, '--app');
if (!appPath) {
Expand Down Expand Up @@ -47,34 +139,17 @@ export async function run(args: readonly string[]): Promise<void> {
getSimulators(),
]);
// try to run on device or simulator with udid
if (udid) {
if (devices.find(d => d.UniqueDeviceID === udid)) {
await runOnDevice(udid, appPath, bundleId, waitForApp);
} else if (simulators.find(s => s.udid === udid)) {
await runOnSimulator(udid, appPath, bundleId, waitForApp);
} else {
throw new IOSRunException(
`No device or simulator with UDID "${udid}" found`,
ERR_TARGET_NOT_FOUND,
);
}
} else if (devices.length && !preferSimulator) {
// no udid, use first connected device
await runOnDevice(
devices[0].UniqueDeviceID,
appPath,
bundleId,
waitForApp,
);
} else {
// use default sim
await runOnSimulator(
simulators[simulators.length - 1].udid,
appPath,
bundleId,
waitForApp,
);
}
const config: IOSRunConfig = {
udid,
devices,
simulators,
appPath,
bundleId,
waitForApp,
preferSimulator,
};

await runIpaOrAppFileOnInterval(config);
} finally {
if (isIPA) {
try {
Expand Down
4 changes: 1 addition & 3 deletions src/ios/utils/device.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as Debug from 'debug';
import { readFileSync } from 'fs';
import * as path from 'path';
import { promisify } from 'util';

import { Exception } from '../../errors';
import { onBeforeExit } from '../../utils/process';
import { onBeforeExit, wait } from '../../utils/process';
import type { DeviceValues, IPLookupResult } from '../lib';
import {
AFCError,
Expand All @@ -17,7 +16,6 @@ import {
import { getDeveloperDiskImagePath } from './xcode';

const debug = Debug('native-run:ios:utils:device');
const wait = promisify(setTimeout);

export async function getConnectedDevices() {
const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket());
Expand Down
6 changes: 5 additions & 1 deletion src/ios/utils/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ interface SimCtlOutput {
readonly devicetypes: SimCtlType[];
}

export async function getSimulators() {
export interface SimulatorResult extends Simulator {
runtime: SimCtlRuntime;
}

export async function getSimulators(): Promise<SimulatorResult[]> {
const simctl = spawnSync('xcrun', ['simctl', 'list', '--json'], {
encoding: 'utf8',
});
Expand Down
1 change: 1 addition & 0 deletions src/utils/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const debug = Debug('native-run:utils:process');

export const exec = util.promisify(cp.exec);
export const execFile = util.promisify(cp.execFile);
export const wait = util.promisify(setTimeout);

export type ExitQueueFn = () => Promise<void>;

Expand Down

0 comments on commit f451e46

Please sign in to comment.