diff --git a/package.json b/package.json index 8547998..75c9c08 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build": "npm run clean && tsc", "watch": "tsc -w", "test": "jest --maxWorkers=4", - "lint": "true", + "lint": "npm run prettier -- --check", + "prettier": "prettier \"**/*.ts\"", "publish:ci": "semantic-release", "publish:testing": "npm version prerelease --preid=testing --no-git-tag-version && npm publish --tag=testing && git stash", "prepublishOnly": "npm run build" @@ -46,6 +47,7 @@ "yauzl": "^2.10.0" }, "devDependencies": { + "@ionic/prettier-config": "^1.0.0", "@semantic-release/changelog": "^5.0.0", "@semantic-release/git": "^9.0.0", "@types/debug": "4.1.5", @@ -60,10 +62,12 @@ "@types/yauzl": "^2.9.1", "husky": "^4.0.9", "jest": "^26.4.2", + "prettier": "^2.1.1", "semantic-release": "^17.1.1", "ts-jest": "^26.3.0", "typescript": "~4.0.2" }, + "prettier": "@ionic/prettier-config", "release": { "branches": "stable", "plugins": [ diff --git a/src/android/index.ts b/src/android/index.ts index 527e81f..9bdd007 100644 --- a/src/android/index.ts +++ b/src/android/index.ts @@ -9,12 +9,12 @@ export async function run(args: readonly string[]): Promise { } if (args.includes('--list')) { - cmd = await import ('./list'); + cmd = await import('./list'); return cmd.run(args); } if (args.includes('--sdk-info')) { - cmd = await import ('./sdk-info'); + cmd = await import('./sdk-info'); return cmd.run(args); } diff --git a/src/android/list.ts b/src/android/list.ts index 445fdef..f410030 100644 --- a/src/android/list.ts +++ b/src/android/list.ts @@ -13,7 +13,7 @@ export async function list(args: readonly string[]): Promise { const sdk = await getSDK(); const errors: Exception[] = []; - const [ devices, virtualDevices ] = await Promise.all([ + const [devices, virtualDevices] = await Promise.all([ (async () => { try { return await getDeviceTargets(sdk); diff --git a/src/android/run.ts b/src/android/run.ts index 612c029..262abe9 100644 --- a/src/android/run.ts +++ b/src/android/run.ts @@ -1,14 +1,38 @@ import * as Debug from 'debug'; -import { AVDException, AndroidRunException, CLIException, ERR_BAD_INPUT, ERR_NO_DEVICE, ERR_NO_TARGET, ERR_TARGET_NOT_FOUND, ERR_UNSUITABLE_API_INSTALLATION } from '../errors'; +import { + AVDException, + AndroidRunException, + CLIException, + ERR_BAD_INPUT, + ERR_NO_DEVICE, + ERR_NO_TARGET, + ERR_TARGET_NOT_FOUND, + ERR_UNSUITABLE_API_INSTALLATION, +} from '../errors'; import { getOptionValue, getOptionValues } from '../utils/cli'; import { log } from '../utils/log'; import { onBeforeExit } from '../utils/process'; -import { Device, Ports, closeApp, forwardPorts, getDevices, startActivity, unforwardPorts, waitForBoot, waitForClose } from './utils/adb'; +import { + Device, + Ports, + closeApp, + forwardPorts, + getDevices, + startActivity, + unforwardPorts, + waitForBoot, + waitForClose, +} from './utils/adb'; import { getApkInfo } from './utils/apk'; import { getInstalledAVDs } from './utils/avd'; -import { installApkToDevice, selectDeviceByTarget, selectHardwareDevice, selectVirtualDevice } from './utils/run'; +import { + installApkToDevice, + selectDeviceByTarget, + selectHardwareDevice, + selectVirtualDevice, +} from './utils/run'; import { SDK, getSDK } from './utils/sdk'; const modulePrefix = 'native-run:android:run'; @@ -22,10 +46,12 @@ export async function run(args: readonly string[]): Promise { if (forwardedPorts && forwardedPorts.length > 0) { forwardedPorts.forEach((port: string) => { - const [ device, host ] = port.split(':'); + const [device, host] = port.split(':'); if (!device || !host) { - throw new CLIException(`Invalid --forward value "${port}": expecting , e.g. 8080:8080`); + throw new CLIException( + `Invalid --forward value "${port}": expecting , e.g. 8080:8080`, + ); } ports.push({ device, host }); @@ -38,16 +64,22 @@ export async function run(args: readonly string[]): Promise { const device = await selectDevice(sdk, args); - log(`Selected ${device.type === 'hardware' ? 'hardware device' : 'emulator'} ${device.serial}\n`); + log( + `Selected ${device.type === 'hardware' ? 'hardware device' : 'emulator'} ${ + device.serial + }\n`, + ); const { appId, activityName } = await getApkInfo(apkPath); await waitForBoot(sdk, device); if (ports) { - await Promise.all(ports.map(async (port: Ports) => { - await forwardPorts(sdk, device, port); - log(`Forwarded device port ${port.device} to host port ${port.host}\n`); - })); + await Promise.all( + ports.map(async (port: Ports) => { + await forwardPorts(sdk, device, port); + log(`Forwarded device port ${port.device} to host port ${port.host}\n`); + }), + ); } await installApkToDevice(sdk, device, apkPath, appId); @@ -59,9 +91,11 @@ export async function run(args: readonly string[]): Promise { onBeforeExit(async () => { if (ports) { - await Promise.all(ports.map(async (port: Ports) => { - await unforwardPorts(sdk, device, port); - })); + await Promise.all( + ports.map(async (port: Ports) => { + await unforwardPorts(sdk, device, port); + }), + ); } }); @@ -75,7 +109,10 @@ export async function run(args: readonly string[]): Promise { } } -export async function selectDevice(sdk: SDK, args: readonly string[]): Promise { +export async function selectDevice( + sdk: SDK, + args: readonly string[], +): Promise { const debug = Debug(`${modulePrefix}:${selectDevice.name}`); const devices = await getDevices(sdk); @@ -90,7 +127,10 @@ export async function selectDevice(sdk: SDK, args: readonly string[]): Promise { function formatSDKInfo(sdk: SDKInfo): string { return ` SDK Location: ${sdk.root} -AVD Home${sdk.avdHome ? `: ${sdk.avdHome}` : ` (!): not found`} +AVD Home${ + sdk.avdHome ? `: ${sdk.avdHome}` : ` (!): not found` + } ${sdk.platforms.map(platform => `${formatPlatform(platform)}\n\n`).join('\n')} Tools: @@ -51,11 +53,25 @@ ${sdk.tools.map(tool => formatPackage(tool)).join('\n')} function formatPlatform(platform: Platform): string { return ` API Level: ${platform.apiLevel} -Packages: ${platform.packages.map(p => formatPackage(p)).join('\n' + ' '.repeat(22))} -${platform.missingPackages.length > 0 ? `(!) Missing Packages: ${platform.missingPackages.map(p => formatPackage(p)).join('\n' + ' '.repeat(22))}` : ''} +Packages: ${platform.packages + .map(p => formatPackage(p)) + .join('\n' + ' '.repeat(22))} +${ + platform.missingPackages.length > 0 + ? `(!) Missing Packages: ${platform.missingPackages + .map(p => formatPackage(p)) + .join('\n' + ' '.repeat(22))}` + : '' +} `.trim(); } -function formatPackage(p: { name: string; path: string; version?: string | RegExp; }): string { - return `${p.name} ${p.path} ${typeof p.version === 'string' ? p.version : ''}`; +function formatPackage(p: { + name: string; + path: string; + version?: string | RegExp; +}): string { + return `${p.name} ${p.path} ${ + typeof p.version === 'string' ? p.version : '' + }`; } diff --git a/src/android/utils/__tests__/adb.ts b/src/android/utils/__tests__/adb.ts index 1eba894..abd5ec3 100644 --- a/src/android/utils/__tests__/adb.ts +++ b/src/android/utils/__tests__/adb.ts @@ -1,9 +1,7 @@ import * as os from 'os'; describe('android/utils/adb', () => { - describe('parseAdbDevices', () => { - let adbUtils: typeof import('../adb'); beforeEach(async () => { @@ -143,7 +141,6 @@ List of devices attached }); describe('windows', () => { - let adbUtils: typeof import('../adb'); beforeEach(async () => { @@ -173,9 +170,6 @@ List of devices attached }, ]); }); - }); - }); - }); diff --git a/src/android/utils/__tests__/avd.ts b/src/android/utils/__tests__/avd.ts index 7ed70df..7c9a9ab 100644 --- a/src/android/utils/__tests__/avd.ts +++ b/src/android/utils/__tests__/avd.ts @@ -4,11 +4,12 @@ import * as iniUtils from '../../../utils/ini'; import * as avdUtils from '../avd'; describe('android/utils/avd', () => { - describe('getAVDFromINI', () => { - it('should properly parse Pixel_2_API_28', async () => { - const inipath = path.resolve(__dirname, './fixtures/avd/Pixel_2_API_28.ini'); + const inipath = path.resolve( + __dirname, + './fixtures/avd/Pixel_2_API_28.ini', + ); const ini: any = await iniUtils.readINI(inipath); ini.path = path.resolve(__dirname, './fixtures/avd/Pixel_2_API_28.avd'); // patch path @@ -27,9 +28,15 @@ describe('android/utils/avd', () => { }); it('should properly parse Pixel_2_XL_API_28', async () => { - const inipath = path.resolve(__dirname, './fixtures/avd/Pixel_2_XL_API_28.ini'); + const inipath = path.resolve( + __dirname, + './fixtures/avd/Pixel_2_XL_API_28.ini', + ); const ini: any = await iniUtils.readINI(inipath); - ini.path = path.resolve(__dirname, './fixtures/avd/Pixel_2_XL_API_28.avd'); // patch path + ini.path = path.resolve( + __dirname, + './fixtures/avd/Pixel_2_XL_API_28.avd', + ); // patch path const expected = { id: 'Pixel_2_XL_API_28', @@ -46,7 +53,10 @@ describe('android/utils/avd', () => { }); it('should properly parse Pixel_API_25', async () => { - const inipath = path.resolve(__dirname, './fixtures/avd/Pixel_API_25.ini'); + const inipath = path.resolve( + __dirname, + './fixtures/avd/Pixel_API_25.ini', + ); const ini: any = await iniUtils.readINI(inipath); ini.path = path.resolve(__dirname, './fixtures/avd/Pixel_API_25.avd'); // patch path @@ -65,7 +75,10 @@ describe('android/utils/avd', () => { }); it('should properly parse Nexus_5X_API_24', async () => { - const inipath = path.resolve(__dirname, './fixtures/avd/Nexus_5X_API_24.ini'); + const inipath = path.resolve( + __dirname, + './fixtures/avd/Nexus_5X_API_24.ini', + ); const ini: any = await iniUtils.readINI(inipath); ini.path = path.resolve(__dirname, './fixtures/avd/Nexus_5X_API_24.avd'); // patch path @@ -84,7 +97,10 @@ describe('android/utils/avd', () => { }); it('should properly parse avdmanager_1', async () => { - const inipath = path.resolve(__dirname, './fixtures/avd/avdmanager_1.ini'); + const inipath = path.resolve( + __dirname, + './fixtures/avd/avdmanager_1.ini', + ); const ini: any = await iniUtils.readINI(inipath); ini.path = path.resolve(__dirname, './fixtures/avd/avdmanager_1.avd'); // patch path @@ -101,7 +117,5 @@ describe('android/utils/avd', () => { const avd = await avdUtils.getAVDFromINI(inipath, ini); expect(avd).toEqual(expected); }); - }); - }); diff --git a/src/android/utils/__tests__/emulator.ts b/src/android/utils/__tests__/emulator.ts index 244cf92..0c7506a 100644 --- a/src/android/utils/__tests__/emulator.ts +++ b/src/android/utils/__tests__/emulator.ts @@ -8,9 +8,7 @@ OK `; describe('android/utils/emulator', () => { - describe('parseAndroidConsoleResponse', () => { - it('should not parse an event from whitespace', () => { for (const output of ['', '\n', ' \n']) { const event = parseAndroidConsoleResponse(output); @@ -29,11 +27,10 @@ describe('android/utils/emulator', () => { }); it('should parse response from output', () => { - const expected = authRequiredOutput.split('\n').slice(0, -2).join('\n') + '\n'; + const expected = + authRequiredOutput.split('\n').slice(0, -2).join('\n') + '\n'; const event = parseAndroidConsoleResponse(authRequiredOutput); expect(event).toBe(expected); }); - }); - }); diff --git a/src/android/utils/adb.ts b/src/android/utils/adb.ts index 8cbde87..4430dba 100644 --- a/src/android/utils/adb.ts +++ b/src/android/utils/adb.ts @@ -5,7 +5,13 @@ import * as path from 'path'; import * as split2 from 'split2'; import * as through2 from 'through2'; -import { ADBException, ERR_INCOMPATIBLE_UPDATE, ERR_MIN_SDK_VERSION, ERR_NO_CERTIFICATES, ERR_VERSION_DOWNGRADE } from '../../errors'; +import { + ADBException, + ERR_INCOMPATIBLE_UPDATE, + ERR_MIN_SDK_VERSION, + ERR_NO_CERTIFICATES, + ERR_VERSION_DOWNGRADE, +} from '../../errors'; import { execFile } from '../../utils/process'; import { SDK, getSDKPackage, supplementProcessEnv } from './sdk'; @@ -36,7 +42,10 @@ export interface Device extends MappedDeviceProps { properties: DeviceProperties; } -const ADB_GETPROP_MAP: ReadonlyMap = new Map([ +const ADB_GETPROP_MAP: ReadonlyMap = new Map< + string, + keyof MappedDeviceProps +>([ ['ro.product.manufacturer', 'manufacturer'], ['ro.product.model', 'model'], ['ro.product.name', 'product'], @@ -45,61 +54,82 @@ const ADB_GETPROP_MAP: ReadonlyMap = new Map { const debug = Debug(`${modulePrefix}:${getDevices.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['devices', '-l']; debug('Invoking adb: %O %O', adbBin, args); - const { stdout } = await execFile(adbBin, args, { env: supplementProcessEnv(sdk) }); + const { stdout } = await execFile(adbBin, args, { + env: supplementProcessEnv(sdk), + }); const devices = parseAdbDevices(stdout); - await Promise.all(devices.map(async device => { - const properties = await getDeviceProperties(sdk, device); + await Promise.all( + devices.map(async device => { + const properties = await getDeviceProperties(sdk, device); - for (const [ prop, deviceProp ] of ADB_GETPROP_MAP.entries()) { - const value = properties[prop]; + for (const [prop, deviceProp] of ADB_GETPROP_MAP.entries()) { + const value = properties[prop]; - if (value) { - device[deviceProp] = value; + if (value) { + device[deviceProp] = value; + } } - } - })); + }), + ); debug('Found adb devices: %O', devices); return devices; } -export async function getDeviceProperty(sdk: SDK, device: Device, property: string): Promise { +export async function getDeviceProperty( + sdk: SDK, + device: Device, + property: string, +): Promise { const debug = Debug(`${modulePrefix}:${getDeviceProperty.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['-s', device.serial, 'shell', 'getprop', property]; debug('Invoking adb: %O %O', adbBin, args); - const { stdout } = await execFile(adbBin, args, { env: supplementProcessEnv(sdk) }); + const { stdout } = await execFile(adbBin, args, { + env: supplementProcessEnv(sdk), + }); return stdout.trim(); } -export async function getDeviceProperties(sdk: SDK, device: Device): Promise { +export async function getDeviceProperties( + sdk: SDK, + device: Device, +): Promise { const debug = Debug(`${modulePrefix}:${getDeviceProperties.name}`); const re = /^\[([a-z0-9\.]+)\]: \[(.*)\]$/; - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['-s', device.serial, 'shell', 'getprop']; debug('Invoking adb: %O %O', adbBin, args); const propAllowList = [...ADB_GETPROP_MAP.keys()]; - const { stdout } = await execFile(adbBin, args, { env: supplementProcessEnv(sdk) }); + const { stdout } = await execFile(adbBin, args, { + env: supplementProcessEnv(sdk), + }); const properties: DeviceProperties = {}; for (const line of stdout.split(os.EOL)) { const m = line.match(re); if (m) { - const [ , key, value ] = m; + const [, key, value] = m; if (propAllowList.includes(key)) { properties[key] = value; @@ -112,7 +142,9 @@ export async function getDeviceProperties(sdk: SDK, device: Device): Promise { const debug = Debug(`${modulePrefix}:${waitForDevice.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['-s', serial, 'wait-for-any-device']; debug('Invoking adb: %O %O', adbBin, args); @@ -138,9 +170,15 @@ export async function waitForBoot(sdk: SDK, device: Device): Promise { }); } -export async function waitForClose(sdk: SDK, device: Device, app: string): Promise { +export async function waitForClose( + sdk: SDK, + device: Device, + app: string, +): Promise { const debug = Debug(`${modulePrefix}:${waitForClose.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['-s', device.serial, 'shell', `ps | grep ${app}`]; @@ -152,7 +190,11 @@ export async function waitForClose(sdk: SDK, device: Device, app: string): Promi await execFile(adbBin, args, { env: supplementProcessEnv(sdk) }); } catch (e) { debug('Error received from adb: %O', e); - debug('App %s no longer found in process list for %s', app, device.serial); + debug( + 'App %s no longer found in process list for %s', + app, + device.serial, + ); clearInterval(interval); resolve(); } @@ -160,14 +202,23 @@ export async function waitForClose(sdk: SDK, device: Device, app: string): Promi }); } -export async function installApk(sdk: SDK, device: Device, apk: string): Promise { +export async function installApk( + sdk: SDK, + device: Device, + apk: string, +): Promise { const debug = Debug(`${modulePrefix}:${installApk.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['-s', device.serial, 'install', '-r', '-t', apk]; debug('Invoking adb: %O %O', adbBin, args); - const p = spawn(adbBin, args, { stdio: 'pipe', env: supplementProcessEnv(sdk) }); + const p = spawn(adbBin, args, { + stdio: 'pipe', + env: supplementProcessEnv(sdk), + }); return new Promise((resolve, reject) => { p.on('close', code => { @@ -183,30 +234,58 @@ export async function installApk(sdk: SDK, device: Device, apk: string): Promise reject(err); }); - p.stderr.pipe(split2()).pipe(through2((chunk, enc, cb) => { - const line = chunk.toString(); - - debug('adb install: %O', line); - const event = parseAdbInstallOutput(line); - - if (event === ADBEvent.IncompatibleUpdateFailure) { - reject(new ADBException(`Encountered adb error: ${ADBEvent[event]}.`, ERR_INCOMPATIBLE_UPDATE)); - } else if (event === ADBEvent.NewerVersionOnDeviceFailure) { - reject(new ADBException(`Encountered adb error: ${ADBEvent[event]}.`, ERR_VERSION_DOWNGRADE)); - } else if (event === ADBEvent.NewerSdkRequiredOnDeviceFailure) { - reject(new ADBException(`Encountered adb error: ${ADBEvent[event]}.`, ERR_MIN_SDK_VERSION)); - } else if (event === ADBEvent.NoCertificates) { - reject(new ADBException(`Encountered adb error: ${ADBEvent[event]}.`, ERR_NO_CERTIFICATES)); - } - - cb(); - })); + p.stderr.pipe(split2()).pipe( + through2((chunk, enc, cb) => { + const line = chunk.toString(); + + debug('adb install: %O', line); + const event = parseAdbInstallOutput(line); + + if (event === ADBEvent.IncompatibleUpdateFailure) { + reject( + new ADBException( + `Encountered adb error: ${ADBEvent[event]}.`, + ERR_INCOMPATIBLE_UPDATE, + ), + ); + } else if (event === ADBEvent.NewerVersionOnDeviceFailure) { + reject( + new ADBException( + `Encountered adb error: ${ADBEvent[event]}.`, + ERR_VERSION_DOWNGRADE, + ), + ); + } else if (event === ADBEvent.NewerSdkRequiredOnDeviceFailure) { + reject( + new ADBException( + `Encountered adb error: ${ADBEvent[event]}.`, + ERR_MIN_SDK_VERSION, + ), + ); + } else if (event === ADBEvent.NoCertificates) { + reject( + new ADBException( + `Encountered adb error: ${ADBEvent[event]}.`, + ERR_NO_CERTIFICATES, + ), + ); + } + + cb(); + }), + ); }); } -export async function closeApp(sdk: SDK, device: Device, app: string): Promise { +export async function closeApp( + sdk: SDK, + device: Device, + app: string, +): Promise { const debug = Debug(`${modulePrefix}:${closeApp.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['-s', device.serial, 'shell', 'am', 'force-stop', app]; debug('Invoking adb: %O %O', adbBin, args); @@ -214,9 +293,15 @@ export async function closeApp(sdk: SDK, device: Device, app: string): Promise { +export async function uninstallApp( + sdk: SDK, + device: Device, + app: string, +): Promise { const debug = Debug(`${modulePrefix}:${uninstallApp.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); const args = ['-s', device.serial, 'uninstall', app]; debug('Invoking adb: %O %O', adbBin, args); @@ -252,11 +337,27 @@ export function parseAdbInstallOutput(line: string): ADBEvent | undefined { return event; } -export async function startActivity(sdk: SDK, device: Device, packageName: string, activityName: string): Promise { +export async function startActivity( + sdk: SDK, + device: Device, + packageName: string, + activityName: string, +): Promise { const debug = Debug(`${modulePrefix}:${startActivity.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); - const args = ['-s', device.serial, 'shell', 'am', 'start', '-W', '-n', `${packageName}/${activityName}`]; + const args = [ + '-s', + device.serial, + 'shell', + 'am', + 'start', + '-W', + '-n', + `${packageName}/${activityName}`, + ]; debug('Invoking adb: %O %O', adbBin, args); await execFile(adbBin, args, { env: supplementProcessEnv(sdk) }); @@ -277,22 +378,33 @@ export function parseAdbDevices(output: string): Device[] { const m = line.match(re); if (m) { - const [ , serial, state, description ] = m; + const [, serial, state, description] = m; const properties = description .split(/\s+/) - .map(prop => prop.includes(':') ? prop.split(':') : undefined) - .filter((kv): kv is [string, string] => typeof kv !== 'undefined' && kv.length >= 2) - .reduce((acc, [ k, v ]) => { + .map(prop => (prop.includes(':') ? prop.split(':') : undefined)) + .filter( + (kv): kv is [string, string] => + typeof kv !== 'undefined' && kv.length >= 2, + ) + .reduce((acc, [k, v]) => { if (k && v) { acc[k.trim()] = v.trim(); } return acc; - }, {} as { [key: string]: string | undefined; }); + }, {} as { [key: string]: string | undefined }); const isIP = !!serial.match(ipRe); - const isGenericDevice = (properties['device'] || '').startsWith('generic'); - const type = 'usb' in properties || isIP || !serial.startsWith('emulator') || !isGenericDevice ? 'hardware' : 'emulator'; + const isGenericDevice = (properties['device'] || '').startsWith( + 'generic', + ); + const type = + 'usb' in properties || + isIP || + !serial.startsWith('emulator') || + !isGenericDevice + ? 'hardware' + : 'emulator'; const connection = 'usb' in properties ? 'usb' : isIP ? 'tcpip' : null; devices.push({ @@ -308,7 +420,10 @@ export function parseAdbDevices(output: string): Device[] { sdkVersion: '', }); } else { - debug('adb devices output line does not match expected regex: %O', line); + debug( + 'adb devices output line does not match expected regex: %O', + line, + ); } } } @@ -316,21 +431,45 @@ export function parseAdbDevices(output: string): Device[] { return devices; } -export async function forwardPorts(sdk: SDK, device: Device, ports: Ports): Promise { +export async function forwardPorts( + sdk: SDK, + device: Device, + ports: Ports, +): Promise { const debug = Debug(`${modulePrefix}:${forwardPorts.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); - const args = ['-s', device.serial, 'reverse', `tcp:${ports.device}`, `tcp:${ports.host}`]; + const args = [ + '-s', + device.serial, + 'reverse', + `tcp:${ports.device}`, + `tcp:${ports.host}`, + ]; debug('Invoking adb: %O %O', adbBin, args); await execFile(adbBin, args, { env: supplementProcessEnv(sdk) }); } -export async function unforwardPorts(sdk: SDK, device: Device, ports: Ports): Promise { +export async function unforwardPorts( + sdk: SDK, + device: Device, + ports: Ports, +): Promise { const debug = Debug(`${modulePrefix}:${forwardPorts.name}`); - const platformTools = await getSDKPackage(path.join(sdk.root, 'platform-tools')); + const platformTools = await getSDKPackage( + path.join(sdk.root, 'platform-tools'), + ); const adbBin = path.join(platformTools.location, 'adb'); - const args = ['-s', device.serial, 'reverse', '--remove', `tcp:${ports.device}`]; + const args = [ + '-s', + device.serial, + 'reverse', + '--remove', + `tcp:${ports.device}`, + ]; debug('Invoking adb: %O %O', adbBin, args); await execFile(adbBin, args, { env: supplementProcessEnv(sdk) }); diff --git a/src/android/utils/apk.ts b/src/android/utils/apk.ts index 4c2bf28..a98d905 100644 --- a/src/android/utils/apk.ts +++ b/src/android/utils/apk.ts @@ -9,7 +9,7 @@ export async function readAndroidManifest(apkPath: string) { await unzip(apkPath, async (entry, zipfile, openReadStream) => { if (entry.fileName === 'AndroidManifest.xml') { const readStream = await openReadStream(entry); - readStream.on('error', (err: Error) => error = err); + readStream.on('error', (err: Error) => (error = err)); readStream.on('data', (chunk: Buffer) => chunks.push(chunk)); readStream.on('end', () => zipfile.close()); } else { @@ -30,8 +30,13 @@ export async function readAndroidManifest(apkPath: string) { export async function getApkInfo(apkPath: string) { const doc = await readAndroidManifest(apkPath); const appId = doc.attributes.find((a: any) => a.name === 'package').value; - const application = doc.childNodes.find((n: any) => n.nodeName === 'application'); - const activity = application.childNodes.find((n: any) => n.nodeName === 'activity'); - const activityName = activity.attributes.find((a: any) => a.name === 'name').value; + const application = doc.childNodes.find( + (n: any) => n.nodeName === 'application', + ); + const activity = application.childNodes.find( + (n: any) => n.nodeName === 'activity', + ); + const activityName = activity.attributes.find((a: any) => a.name === 'name') + .value; return { appId, activityName }; } diff --git a/src/android/utils/avd.ts b/src/android/utils/avd.ts index 41e7812..40afb1a 100644 --- a/src/android/utils/avd.ts +++ b/src/android/utils/avd.ts @@ -3,12 +3,26 @@ import * as Debug from 'debug'; import * as pathlib from 'path'; import { ASSETS_PATH } from '../../constants'; -import { AVDException, ERR_INVALID_SKIN, ERR_INVALID_SYSTEM_IMAGE, ERR_MISSING_SYSTEM_IMAGE, ERR_SDK_UNSATISFIED_PACKAGES, ERR_UNSUITABLE_API_INSTALLATION, ERR_UNSUPPORTED_API_LEVEL } from '../../errors'; +import { + AVDException, + ERR_INVALID_SKIN, + ERR_INVALID_SYSTEM_IMAGE, + ERR_MISSING_SYSTEM_IMAGE, + ERR_SDK_UNSATISFIED_PACKAGES, + ERR_UNSUITABLE_API_INSTALLATION, + ERR_UNSUPPORTED_API_LEVEL, +} from '../../errors'; import { readINI, writeINI } from '../../utils/ini'; import { sort } from '../../utils/object'; import { SDK, SDKPackage, findAllSDKPackages } from './sdk'; -import { APILevel, API_LEVEL_SCHEMAS, PartialAVDSchematic, findPackageBySchemaPath, getAPILevels } from './sdk/api'; +import { + APILevel, + API_LEVEL_SCHEMAS, + PartialAVDSchematic, + findPackageBySchemaPath, + getAPILevels, +} from './sdk/api'; const modulePrefix = 'native-run:android:utils:avd'; @@ -30,13 +44,13 @@ export interface AVDSchematic { export interface AVDINI { readonly 'avd.ini.encoding': string; - readonly 'path': string; + readonly path: string; readonly 'path.rel': string; - readonly 'target': string; + readonly target: string; } export interface AVDConfigINI { - readonly 'AvdId'?: string; + readonly AvdId?: string; readonly 'abi.type'?: string; readonly 'avd.ini.displayname'?: string; readonly 'avd.ini.encoding'?: string; @@ -64,7 +78,7 @@ export interface AVDConfigINI { readonly 'hw.sensors.proximity'?: string; readonly 'image.sysdir.1'?: string; readonly 'sdcard.size'?: string; - readonly 'showDeviceFrame'?: string; + readonly showDeviceFrame?: string; readonly 'skin.dynamic'?: string; readonly 'skin.name'?: string; readonly 'skin.path'?: string; @@ -72,18 +86,25 @@ export interface AVDConfigINI { readonly 'tag.id'?: string; } -export const isAVDINI = (o: any): o is AVDINI => o - && typeof o['avd.ini.encoding'] === 'string' - && typeof o['path'] === 'string' - && typeof o['path.rel'] === 'string' - && typeof o['target'] === 'string'; - -export const isAVDConfigINI = (o: any): o is AVDConfigINI => o - && (typeof o['avd.ini.displayname'] === 'undefined' || typeof o['avd.ini.displayname'] === 'string') - && (typeof o['hw.lcd.density'] === 'undefined' || typeof o['hw.lcd.density'] === 'string') - && (typeof o['hw.lcd.height'] === 'undefined' || typeof o['hw.lcd.height'] === 'string') - && (typeof o['hw.lcd.width'] === 'undefined' || typeof o['hw.lcd.width'] === 'string') - && (typeof o['image.sysdir.1'] === 'undefined' || typeof o['image.sysdir.1'] === 'string'); +export const isAVDINI = (o: any): o is AVDINI => + o && + typeof o['avd.ini.encoding'] === 'string' && + typeof o['path'] === 'string' && + typeof o['path.rel'] === 'string' && + typeof o['target'] === 'string'; + +export const isAVDConfigINI = (o: any): o is AVDConfigINI => + o && + (typeof o['avd.ini.displayname'] === 'undefined' || + typeof o['avd.ini.displayname'] === 'string') && + (typeof o['hw.lcd.density'] === 'undefined' || + typeof o['hw.lcd.density'] === 'string') && + (typeof o['hw.lcd.height'] === 'undefined' || + typeof o['hw.lcd.height'] === 'string') && + (typeof o['hw.lcd.width'] === 'undefined' || + typeof o['hw.lcd.width'] === 'string') && + (typeof o['image.sysdir.1'] === 'undefined' || + typeof o['image.sysdir.1'] === 'string'); export async function getAVDINIs(sdk: SDK): Promise<[string, AVDINI][]> { const debug = Debug(`${modulePrefix}:${getAVDINIs.name}`); @@ -97,24 +118,43 @@ export async function getAVDINIs(sdk: SDK): Promise<[string, AVDINI][]> { debug('Discovered AVD ini files: %O', iniFilePaths); const iniFiles = await Promise.all( - iniFilePaths.map(async (f): Promise<[string, AVDINI | undefined]> => [f, await readINI(f, isAVDINI)]) + iniFilePaths.map( + async (f): Promise<[string, AVDINI | undefined]> => [ + f, + await readINI(f, isAVDINI), + ], + ), ); - const avdInis = iniFiles - .filter((c): c is [string, AVDINI] => typeof c[1] !== 'undefined'); + const avdInis = iniFiles.filter( + (c): c is [string, AVDINI] => typeof c[1] !== 'undefined', + ); return avdInis; } -export function getAVDFromConfigINI(inipath: string, ini: AVDINI, configini: AVDConfigINI): AVD { +export function getAVDFromConfigINI( + inipath: string, + ini: AVDINI, + configini: AVDConfigINI, +): AVD { const inibasename = pathlib.basename(inipath); - const id = inibasename.substring(0, inibasename.length - pathlib.extname(inibasename).length); + const id = inibasename.substring( + 0, + inibasename.length - pathlib.extname(inibasename).length, + ); const name = configini['avd.ini.displayname'] ? String(configini['avd.ini.displayname']) : id.replace(/_/g, ' '); - const screenDPI = configini['hw.lcd.density'] ? Number(configini['hw.lcd.density']) : null; - const screenWidth = configini['hw.lcd.width'] ? Number(configini['hw.lcd.width']) : null; - const screenHeight = configini['hw.lcd.height'] ? Number(configini['hw.lcd.height']) : null; + const screenDPI = configini['hw.lcd.density'] + ? Number(configini['hw.lcd.density']) + : null; + const screenWidth = configini['hw.lcd.width'] + ? Number(configini['hw.lcd.width']) + : null; + const screenHeight = configini['hw.lcd.height'] + ? Number(configini['hw.lcd.height']) + : null; return { id, @@ -131,8 +171,14 @@ export function getSDKVersionFromTarget(target: string): string { return target.replace(/^android-(\d+)/, '$1'); } -export async function getAVDFromINI(inipath: string, ini: AVDINI): Promise { - const configini = await readINI(pathlib.resolve(ini.path, 'config.ini'), isAVDConfigINI); +export async function getAVDFromINI( + inipath: string, + ini: AVDINI, +): Promise { + const configini = await readINI( + pathlib.resolve(ini.path, 'config.ini'), + isAVDConfigINI, + ); if (configini) { return getAVDFromConfigINI(inipath, ini, configini); @@ -141,8 +187,12 @@ export async function getAVDFromINI(inipath: string, ini: AVDINI): Promise { const avdInis = await getAVDINIs(sdk); - const possibleAvds = await Promise.all(avdInis.map(([inipath, ini]) => getAVDFromINI(inipath, ini))); - const avds = possibleAvds.filter((avd): avd is AVD => typeof avd !== 'undefined'); + const possibleAvds = await Promise.all( + avdInis.map(([inipath, ini]) => getAVDFromINI(inipath, ini)), + ); + const avds = possibleAvds.filter( + (avd): avd is AVD => typeof avd !== 'undefined', + ); return avds; } @@ -167,26 +217,46 @@ export async function getDefaultAVDSchematic(sdk: SDK): Promise { } } - throw new AVDException('No suitable API installation found.', ERR_UNSUITABLE_API_INSTALLATION, 1); + throw new AVDException( + 'No suitable API installation found.', + ERR_UNSUITABLE_API_INSTALLATION, + 1, + ); } -export async function getAVDSchematicFromAPILevel(sdk: SDK, packages: readonly SDKPackage[], api: APILevel): Promise { +export async function getAVDSchematicFromAPILevel( + sdk: SDK, + packages: readonly SDKPackage[], + api: APILevel, +): Promise { const schema = API_LEVEL_SCHEMAS.find(s => s.apiLevel === api.apiLevel); if (!schema) { - throw new AVDException(`Unsupported API level: ${api.apiLevel}`, ERR_UNSUPPORTED_API_LEVEL); + throw new AVDException( + `Unsupported API level: ${api.apiLevel}`, + ERR_UNSUPPORTED_API_LEVEL, + ); } const missingPackages = schema.validate(packages); if (missingPackages.length > 0) { - throw new AVDException(`Unsatisfied packages within API ${api.apiLevel}: ${missingPackages.map(pkg => pkg.path).join(', ')}`, ERR_SDK_UNSATISFIED_PACKAGES, 1); + throw new AVDException( + `Unsatisfied packages within API ${api.apiLevel}: ${missingPackages + .map(pkg => pkg.path) + .join(', ')}`, + ERR_SDK_UNSATISFIED_PACKAGES, + 1, + ); } return createAVDSchematic(sdk, await schema.loadPartialAVDSchematic()); } -export async function getDefaultAVD(sdk: SDK, avds: readonly AVD[]): Promise { +export async function getDefaultAVD( + sdk: SDK, + avds: readonly AVD[], +): Promise { const defaultAvdSchematic = await getDefaultAVDSchematic(sdk); const defaultAvd = avds.find(avd => avd.id === defaultAvdSchematic.id); @@ -197,7 +267,10 @@ export async function getDefaultAVD(sdk: SDK, avds: readonly AVD[]): Promise { +export async function createAVD( + sdk: SDK, + schematic: AVDSchematic, +): Promise { const { id, ini, configini } = schematic; await mkdirp(pathlib.join(sdk.avdHome, `${id}.avd`)); @@ -207,26 +280,42 @@ export async function createAVD(sdk: SDK, schematic: AVDSchematic): Promise writeINI(pathlib.join(sdk.avdHome, `${id}.avd`, 'config.ini'), configini), ]); - return getAVDFromConfigINI(pathlib.join(sdk.avdHome, `${id}.ini`), ini, configini); + return getAVDFromConfigINI( + pathlib.join(sdk.avdHome, `${id}.ini`), + ini, + configini, + ); } -export async function createAVDSchematic(sdk: SDK, partialSchematic: PartialAVDSchematic): Promise { - const sysimage = findPackageBySchemaPath(sdk.packages || [], new RegExp(`^system-images;${partialSchematic.ini.target}`)); +export async function createAVDSchematic( + sdk: SDK, + partialSchematic: PartialAVDSchematic, +): Promise { + const sysimage = findPackageBySchemaPath( + sdk.packages || [], + new RegExp(`^system-images;${partialSchematic.ini.target}`), + ); if (!sysimage) { - throw new AVDException(`Cannot create AVD schematic for ${partialSchematic.id}: missing system image.`, ERR_MISSING_SYSTEM_IMAGE); + throw new AVDException( + `Cannot create AVD schematic for ${partialSchematic.id}: missing system image.`, + ERR_MISSING_SYSTEM_IMAGE, + ); } const avdpath = pathlib.join(sdk.avdHome, `${partialSchematic.id}.avd`); - const skinpath = getSkinPathByName(sdk, partialSchematic.configini['skin.name']); + const skinpath = getSkinPathByName( + sdk, + partialSchematic.configini['skin.name'], + ); const sysdir = pathlib.relative(sdk.root, sysimage.location); - const [ , , tagid ] = sysimage.path.split(';'); + const [, , tagid] = sysimage.path.split(';'); const schematic: AVDSchematic = { id: partialSchematic.id, ini: sort({ ...partialSchematic.ini, - 'path': avdpath, + path: avdpath, 'path.rel': `avd/${partialSchematic.id}.avd`, }), configini: sort({ @@ -242,25 +331,38 @@ export async function createAVDSchematic(sdk: SDK, partialSchematic: PartialAVDS return schematic; } -export async function validateAVDSchematic(sdk: SDK, schematic: AVDSchematic): Promise { +export async function validateAVDSchematic( + sdk: SDK, + schematic: AVDSchematic, +): Promise { const { configini } = schematic; const skin = configini['skin.name']; const skinpath = configini['skin.path']; const sysdir = configini['image.sysdir.1']; if (!skinpath) { - throw new AVDException(`${schematic.id} does not have a skin defined.`, ERR_INVALID_SKIN); + throw new AVDException( + `${schematic.id} does not have a skin defined.`, + ERR_INVALID_SKIN, + ); } if (!sysdir) { - throw new AVDException(`${schematic.id} does not have a system image defined.`, ERR_INVALID_SYSTEM_IMAGE); + throw new AVDException( + `${schematic.id} does not have a system image defined.`, + ERR_INVALID_SYSTEM_IMAGE, + ); } await validateSkin(sdk, skin, skinpath); await validateSystemImagePath(sdk, sysdir); } -export async function validateSkin(sdk: SDK, skin: string, skinpath: string): Promise { +export async function validateSkin( + sdk: SDK, + skin: string, + skinpath: string, +): Promise { const debug = Debug(`${modulePrefix}:${validateSkin.name}`); const p = pathlib.join(skinpath, 'layout'); @@ -275,7 +377,11 @@ export async function validateSkin(sdk: SDK, skin: string, skinpath: string): Pr await copySkin(sdk, skin, skinpath); } -export async function copySkin(sdk: SDK, skin: string, skinpath: string): Promise { +export async function copySkin( + sdk: SDK, + skin: string, + skinpath: string, +): Promise { const debug = Debug(`${modulePrefix}:${copySkin.name}`); const skinsrc = pathlib.resolve(ASSETS_PATH, 'android', 'skins', skin); @@ -294,7 +400,10 @@ export async function copySkin(sdk: SDK, skin: string, skinpath: string): Promis throw new AVDException(`${skinpath} is an invalid skin.`, ERR_INVALID_SKIN); } -export async function validateSystemImagePath(sdk: SDK, sysdir: string): Promise { +export async function validateSystemImagePath( + sdk: SDK, + sysdir: string, +): Promise { const debug = Debug(`${modulePrefix}:${validateSystemImagePath.name}`); const p = pathlib.join(sdk.root, sysdir, 'package.xml'); @@ -303,7 +412,10 @@ export async function validateSystemImagePath(sdk: SDK, sysdir: string): Promise const stat = await statSafe(p); if (!stat || !stat.isFile()) { - throw new AVDException(`${p} is an invalid system image package.`, ERR_INVALID_SYSTEM_IMAGE); + throw new AVDException( + `${p} is an invalid system image package.`, + ERR_INVALID_SYSTEM_IMAGE, + ); } } diff --git a/src/android/utils/binary-xml-parser.ts b/src/android/utils/binary-xml-parser.ts index 9ec29de..264f8cd 100644 --- a/src/android/utils/binary-xml-parser.ts +++ b/src/android/utils/binary-xml-parser.ts @@ -180,24 +180,24 @@ export class BinaryXmlParser { dimension.rawUnit = unit; switch (unit) { - case TypedValue.COMPLEX_UNIT_MM: - dimension.unit = 'mm'; - break; - case TypedValue.COMPLEX_UNIT_PX: - dimension.unit = 'px'; - break; - case TypedValue.COMPLEX_UNIT_DIP: - dimension.unit = 'dp'; - break; - case TypedValue.COMPLEX_UNIT_SP: - dimension.unit = 'sp'; - break; - case TypedValue.COMPLEX_UNIT_PT: - dimension.unit = 'pt'; - break; - case TypedValue.COMPLEX_UNIT_IN: - dimension.unit = 'in'; - break; + case TypedValue.COMPLEX_UNIT_MM: + dimension.unit = 'mm'; + break; + case TypedValue.COMPLEX_UNIT_PX: + dimension.unit = 'px'; + break; + case TypedValue.COMPLEX_UNIT_DIP: + dimension.unit = 'dp'; + break; + case TypedValue.COMPLEX_UNIT_SP: + dimension.unit = 'sp'; + break; + case TypedValue.COMPLEX_UNIT_PT: + dimension.unit = 'pt'; + break; + case TypedValue.COMPLEX_UNIT_IN: + dimension.unit = 'in'; + break; } return dimension; @@ -219,12 +219,12 @@ export class BinaryXmlParser { fraction.rawType = type; switch (type) { - case TypedValue.COMPLEX_UNIT_FRACTION: - fraction.type = '%'; - break; - case TypedValue.COMPLEX_UNIT_FRACTION_PARENT: - fraction.type = '%p'; - break; + case TypedValue.COMPLEX_UNIT_FRACTION: + fraction.type = '%'; + break; + case TypedValue.COMPLEX_UNIT_FRACTION_PARENT: + fraction.type = '%p'; + break; } return fraction; @@ -265,63 +265,63 @@ export class BinaryXmlParser { typedValue.rawType = dataType; switch (dataType) { - case TypedValue.TYPE_INT_DEC: - typedValue.value = this.readS32(); - typedValue.type = 'int_dec'; - break; - case TypedValue.TYPE_INT_HEX: - typedValue.value = this.readS32(); - typedValue.type = 'int_hex'; - break; - case TypedValue.TYPE_STRING: - const ref = this.readS32(); - typedValue.value = ref > 0 ? this.strings[ref] : ''; - typedValue.type = 'string'; - break; - case TypedValue.TYPE_REFERENCE: - const id = this.readU32(); - typedValue.value = `resourceId:0x${id.toString(16)}`; - typedValue.type = 'reference'; - break; - case TypedValue.TYPE_INT_BOOLEAN: - typedValue.value = this.readS32() !== 0; - typedValue.type = 'boolean'; - break; - case TypedValue.TYPE_NULL: - this.readU32(); - typedValue.value = null; - typedValue.type = 'null'; - break; - case TypedValue.TYPE_INT_COLOR_RGB8: - typedValue.value = this.readHex24(); - typedValue.type = 'rgb8'; - break; - case TypedValue.TYPE_INT_COLOR_RGB4: - typedValue.value = this.readHex24(); - typedValue.type = 'rgb4'; - break; - case TypedValue.TYPE_INT_COLOR_ARGB8: - typedValue.value = this.readHex32(); - typedValue.type = 'argb8'; - break; - case TypedValue.TYPE_INT_COLOR_ARGB4: - typedValue.value = this.readHex32(); - typedValue.type = 'argb4'; - break; - case TypedValue.TYPE_DIMENSION: - typedValue.value = this.readDimension(); - typedValue.type = 'dimension'; - break; - case TypedValue.TYPE_FRACTION: - typedValue.value = this.readFraction(); - typedValue.type = 'fraction'; - break; - default: { - // const type = dataType.toString(16); - // debug(`Not sure what to do with typed value of type 0x${type}, falling back to reading an uint32.`); - typedValue.value = this.readU32(); - typedValue.type = 'unknown'; - } + case TypedValue.TYPE_INT_DEC: + typedValue.value = this.readS32(); + typedValue.type = 'int_dec'; + break; + case TypedValue.TYPE_INT_HEX: + typedValue.value = this.readS32(); + typedValue.type = 'int_hex'; + break; + case TypedValue.TYPE_STRING: + const ref = this.readS32(); + typedValue.value = ref > 0 ? this.strings[ref] : ''; + typedValue.type = 'string'; + break; + case TypedValue.TYPE_REFERENCE: + const id = this.readU32(); + typedValue.value = `resourceId:0x${id.toString(16)}`; + typedValue.type = 'reference'; + break; + case TypedValue.TYPE_INT_BOOLEAN: + typedValue.value = this.readS32() !== 0; + typedValue.type = 'boolean'; + break; + case TypedValue.TYPE_NULL: + this.readU32(); + typedValue.value = null; + typedValue.type = 'null'; + break; + case TypedValue.TYPE_INT_COLOR_RGB8: + typedValue.value = this.readHex24(); + typedValue.type = 'rgb8'; + break; + case TypedValue.TYPE_INT_COLOR_RGB4: + typedValue.value = this.readHex24(); + typedValue.type = 'rgb4'; + break; + case TypedValue.TYPE_INT_COLOR_ARGB8: + typedValue.value = this.readHex32(); + typedValue.type = 'argb8'; + break; + case TypedValue.TYPE_INT_COLOR_ARGB4: + typedValue.value = this.readHex32(); + typedValue.type = 'argb4'; + break; + case TypedValue.TYPE_DIMENSION: + typedValue.value = this.readDimension(); + typedValue.type = 'dimension'; + break; + case TypedValue.TYPE_FRACTION: + typedValue.value = this.readFraction(); + typedValue.type = 'fraction'; + break; + default: { + // const type = dataType.toString(16); + // debug(`Not sure what to do with typed value of type 0x${type}, falling back to reading an uint32.`); + typedValue.value = this.readU32(); + typedValue.type = 'unknown'; + } } // Ensure we consume the whole value @@ -329,9 +329,9 @@ export class BinaryXmlParser { if (this.cursor !== end) { // const type = dataType.toString(16); // const diff = end - this.cursor; -// debug(`Cursor is off by ${diff} bytes at ${this.cursor} at supposed end \ -// of typed value of type 0x${type}. The typed value started at offset ${start} \ -// and is supposed to end at offset ${end}. Ignoring the rest of the value.`); + // debug(`Cursor is off by ${diff} bytes at ${this.cursor} at supposed end \ + // of typed value of type 0x${type}. The typed value started at offset ${start} \ + // and is supposed to end at offset ${end}. Ignoring the rest of the value.`); this.cursor = end; } @@ -341,8 +341,8 @@ export class BinaryXmlParser { // https://twitter.com/kawasima/status/427730289201139712 convertIntToFloat(int: number) { const buf = new ArrayBuffer(4); - (new Int32Array(buf))[0] = int; - return (new Float32Array(buf))[0]; + new Int32Array(buf)[0] = int; + return new Float32Array(buf)[0]; } readString(encoding: string) { @@ -351,26 +351,34 @@ export class BinaryXmlParser { let byteLength; let value; switch (encoding) { - case 'utf-8': - stringLength = this.readLength8(); - // debug('stringLength:', stringLength); - byteLength = this.readLength8(); - // debug('byteLength:', byteLength); - value = this.buffer.toString(encoding, this.cursor, (this.cursor += byteLength)); - // debug('value:', value); - assert.equal(this.readU8(), 0, 'String must end with trailing zero'); - return value; - case 'ucs2': - stringLength = this.readLength16(); - // debug('stringLength:', stringLength); - byteLength = stringLength * 2; - // debug('byteLength:', byteLength); - value = this.buffer.toString(encoding, this.cursor, (this.cursor += byteLength)); - // debug('value:', value); - assert.equal(this.readU16(), 0, 'String must end with trailing zero'); - return value; - default: - throw new Exception(`Unsupported encoding '${encoding}'`); + case 'utf-8': + stringLength = this.readLength8(); + // debug('stringLength:', stringLength); + byteLength = this.readLength8(); + // debug('byteLength:', byteLength); + value = this.buffer.toString( + encoding, + this.cursor, + (this.cursor += byteLength), + ); + // debug('value:', value); + assert.equal(this.readU8(), 0, 'String must end with trailing zero'); + return value; + case 'ucs2': + stringLength = this.readLength16(); + // debug('stringLength:', stringLength); + byteLength = stringLength * 2; + // debug('byteLength:', byteLength); + value = this.buffer.toString( + encoding, + this.cursor, + (this.cursor += byteLength), + ); + // debug('value:', value); + assert.equal(this.readU16(), 0, 'String must end with trailing zero'); + return value; + default: + throw new Exception(`Unsupported encoding '${encoding}'`); } } @@ -415,9 +423,8 @@ export class BinaryXmlParser { // const sorted = (header.flags & StringFlags.SORTED) === StringFlags.SORTED; // debug('sorted:', sorted); - const encoding = (header.flags & StringFlags.UTF8) === StringFlags.UTF8 - ? 'utf-8' - : 'ucs2'; + const encoding = + (header.flags & StringFlags.UTF8) === StringFlags.UTF8 ? 'utf-8' : 'ucs2'; // debug('encoding:', encoding); const stringsStart = header.startOffset + header.stringsStart; @@ -532,7 +539,7 @@ export class BinaryXmlParser { this.parent.childNodes.push(node); this.parent = node; } else { - this.document = (this.parent = node); + this.document = this.parent = node; } this.stack.push(node); @@ -636,32 +643,32 @@ export class BinaryXmlParser { const start = this.cursor; const header = this.readChunkHeader(); switch (header.chunkType) { - case ChunkType.STRING_POOL: - this.readStringPool(header); - break; - case ChunkType.XML_RESOURCE_MAP: - this.readResourceMap(header); - break; - case ChunkType.XML_START_NAMESPACE: - this.readXmlNamespaceStart(); - break; - case ChunkType.XML_END_NAMESPACE: - this.readXmlNamespaceEnd(); - break; - case ChunkType.XML_START_ELEMENT: - this.readXmlElementStart(); - break; - case ChunkType.XML_END_ELEMENT: - this.readXmlElementEnd(); - break; - case ChunkType.XML_CDATA: - this.readXmlCData(); - break; - case ChunkType.NULL: - this.readNull(header); - break; - default: - throw new Exception(`Unsupported chunk type '${header.chunkType}'`); + case ChunkType.STRING_POOL: + this.readStringPool(header); + break; + case ChunkType.XML_RESOURCE_MAP: + this.readResourceMap(header); + break; + case ChunkType.XML_START_NAMESPACE: + this.readXmlNamespaceStart(); + break; + case ChunkType.XML_END_NAMESPACE: + this.readXmlNamespaceEnd(); + break; + case ChunkType.XML_START_ELEMENT: + this.readXmlElementStart(); + break; + case ChunkType.XML_END_ELEMENT: + this.readXmlElementEnd(); + break; + case ChunkType.XML_CDATA: + this.readXmlCData(); + break; + case ChunkType.NULL: + this.readNull(header); + break; + default: + throw new Exception(`Unsupported chunk type '${header.chunkType}'`); } // Ensure we consume the whole chunk @@ -670,9 +677,9 @@ export class BinaryXmlParser { // const diff = end - this.cursor; // const type = header.chunkType.toString(16); // debug(`Cursor is off by ${diff} bytes at ${this.cursor} at supposed \ -// end of chunk of type 0x${type}. The chunk started at offset ${start} and is \ -// supposed to end at offset ${end}. Ignoring the rest of the chunk.`); -// this.cursor = end; + // end of chunk of type 0x${type}. The chunk started at offset ${start} and is \ + // supposed to end at offset ${end}. Ignoring the rest of the chunk.`); + // this.cursor = end; } } diff --git a/src/android/utils/emulator.ts b/src/android/utils/emulator.ts index 5b25378..b12db79 100644 --- a/src/android/utils/emulator.ts +++ b/src/android/utils/emulator.ts @@ -7,7 +7,13 @@ import * as path from 'path'; import * as split2 from 'split2'; import * as through2 from 'through2'; -import { ERR_ALREADY_RUNNING, ERR_AVD_HOME_NOT_FOUND, ERR_NON_ZERO_EXIT, ERR_UNKNOWN_AVD, EmulatorException } from '../../errors'; +import { + ERR_ALREADY_RUNNING, + ERR_AVD_HOME_NOT_FOUND, + ERR_NON_ZERO_EXIT, + ERR_UNKNOWN_AVD, + EmulatorException, +} from '../../errors'; import { once } from '../../utils/fn'; import { Device, getDevices, waitForDevice } from './adb'; @@ -19,7 +25,11 @@ const modulePrefix = 'native-run:android:utils:emulator'; /** * Resolves when emulator is ready and running with the specified AVD. */ -export async function runEmulator(sdk: SDK, avd: AVD, port: number): Promise { +export async function runEmulator( + sdk: SDK, + avd: AVD, + port: number, +): Promise { try { await spawnEmulator(sdk, avd, port); } catch (e) { @@ -39,21 +49,38 @@ export async function runEmulator(sdk: SDK, avd: AVD, port: number): Promise { +export async function spawnEmulator( + sdk: SDK, + avd: AVD, + port: number, +): Promise { const debug = Debug(`${modulePrefix}:${spawnEmulator.name}`); const emulator = await getSDKPackage(path.join(sdk.root, 'emulator')); const emulatorBin = path.join(emulator.location, 'emulator'); const args = ['-avd', avd.id, '-port', port.toString(), '-verbose']; debug('Invoking emulator: %O %O', emulatorBin, args); - const p = spawn(emulatorBin, args, { detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: supplementProcessEnv(sdk) }); + const p = spawn(emulatorBin, args, { + detached: true, + stdio: ['ignore', 'pipe', 'pipe'], + env: supplementProcessEnv(sdk), + }); p.unref(); return new Promise((_resolve, _reject) => { - const resolve: typeof _resolve = once(() => { _resolve(); cleanup(); }); - const reject: typeof _reject = once(err => { _reject(err); cleanup(); }); + const resolve: typeof _resolve = once(() => { + _resolve(); + cleanup(); + }); + const reject: typeof _reject = once(err => { + _reject(err); + cleanup(); + }); - waitForDevice(sdk, `emulator-${port}`).then(() => resolve(), err => reject(err)); + waitForDevice(sdk, `emulator-${port}`).then( + () => resolve(), + err => reject(err), + ); const eventParser = through2((chunk: string, enc, cb) => { const line = chunk.toString(); @@ -62,11 +89,26 @@ export async function spawnEmulator(sdk: SDK, avd: AVD, port: number): Promise 0) { - reject(new EmulatorException(`Non-zero exit code from Emulator: ${code}`, ERR_NON_ZERO_EXIT)); + reject( + new EmulatorException( + `Non-zero exit code from Emulator: ${code}`, + ERR_NON_ZERO_EXIT, + ), + ); } }); @@ -111,7 +158,9 @@ export function parseEmulatorOutput(line: string): EmulatorEvent | undefined { if (line.includes('Unknown AVD name')) { event = EmulatorEvent.UnknownAVD; - } else if (line.includes('another emulator instance running with the current AVD')) { + } else if ( + line.includes('another emulator instance running with the current AVD') + ) { event = EmulatorEvent.AlreadyRunning; } else if (line.includes('Cannot find AVD system path')) { event = EmulatorEvent.AVDHomeNotFound; @@ -124,13 +173,18 @@ export function parseEmulatorOutput(line: string): EmulatorEvent | undefined { return event; } -export async function getAVDFromEmulator(emulator: Device, avds: readonly AVD[]): Promise { +export async function getAVDFromEmulator( + emulator: Device, + avds: readonly AVD[], +): Promise { const debug = Debug(`${modulePrefix}:${getAVDFromEmulator.name}`); const emulatorPortRegex = /^emulator-(\d+)$/; const m = emulator.serial.match(emulatorPortRegex); if (!m) { - throw new EmulatorException(`Emulator ${emulator.serial} does not match expected emulator serial format`); + throw new EmulatorException( + `Emulator ${emulator.serial} does not match expected emulator serial format`, + ); } const port = Number.parseInt(m[1], 10); @@ -142,8 +196,12 @@ export async function getAVDFromEmulator(emulator: Device, avds: readonly AVD[]) const readAuthFile = new Promise((resolve, reject) => { sock.on('connect', () => { debug('Connected to %s:%d', host, port); - readFile(path.resolve(os.homedir(), '.emulator_console_auth_token'), { encoding: 'utf8' }) - .then(contents => resolve(contents.trim()), err => reject(err)); + readFile(path.resolve(os.homedir(), '.emulator_console_auth_token'), { + encoding: 'utf8', + }).then( + contents => resolve(contents.trim()), + err => reject(err), + ); }); }); @@ -160,7 +218,11 @@ export async function getAVDFromEmulator(emulator: Device, avds: readonly AVD[]) const timer = setTimeout(() => { if (stage !== Stage.Complete) { - reject(new EmulatorException(`Took too long to get AVD name from Android Emulator Console, something went wrong.`)); + reject( + new EmulatorException( + `Took too long to get AVD name from Android Emulator Console, something went wrong.`, + ), + ); } }, 3000); @@ -174,44 +236,59 @@ export async function getAVDFromEmulator(emulator: Device, avds: readonly AVD[]) cleanup(); }); - sock.pipe(split2()).pipe(through2((chunk: string, enc, cb) => { - const line = chunk.toString(); - - debug('Android Console: %O', line); - - if (stage === Stage.Initial && line.includes('Authentication required')) { - stage = Stage.Auth; - } else if (stage === Stage.Auth && line.trim() === 'OK') { - readAuthFile.then(token => sock.write(`auth ${token}\n`, 'utf8'), err => reject(err)); - stage = Stage.AuthSuccess; - } else if (stage === Stage.AuthSuccess && line.trim() === 'OK') { - sock.write('avd name\n', 'utf8'); - stage = Stage.Response; - } else if (stage === Stage.Response) { - const avdId = line.trim(); - const avd = avds.find(avd => avd.id === avdId); - - if (avd) { - resolve(avd); - } else { - reject(new EmulatorException(`Unknown AVD name [${avdId}]`, ERR_UNKNOWN_AVD)); + sock.pipe(split2()).pipe( + through2((chunk: string, enc, cb) => { + const line = chunk.toString(); + + debug('Android Console: %O', line); + + if ( + stage === Stage.Initial && + line.includes('Authentication required') + ) { + stage = Stage.Auth; + } else if (stage === Stage.Auth && line.trim() === 'OK') { + readAuthFile.then( + token => sock.write(`auth ${token}\n`, 'utf8'), + err => reject(err), + ); + stage = Stage.AuthSuccess; + } else if (stage === Stage.AuthSuccess && line.trim() === 'OK') { + sock.write('avd name\n', 'utf8'); + stage = Stage.Response; + } else if (stage === Stage.Response) { + const avdId = line.trim(); + const avd = avds.find(avd => avd.id === avdId); + + if (avd) { + resolve(avd); + } else { + reject( + new EmulatorException( + `Unknown AVD name [${avdId}]`, + ERR_UNKNOWN_AVD, + ), + ); + } + + stage = Stage.Complete; + cleanup(); } - stage = Stage.Complete; - cleanup(); - } - - cb(); - })); + cb(); + }), + ); }); } -export function parseAndroidConsoleResponse(output: string): string | undefined { +export function parseAndroidConsoleResponse( + output: string, +): string | undefined { const debug = Debug(`${modulePrefix}:${parseAndroidConsoleResponse.name}`); const m = /([\s\S]+)OK\r?\n/g.exec(output); if (m) { - const [ , response ] = m; + const [, response] = m; debug('Parsed response data from Android Console output: %O', response); return response; } diff --git a/src/android/utils/run.ts b/src/android/utils/run.ts index 55c7091..7f8a9e0 100644 --- a/src/android/utils/run.ts +++ b/src/android/utils/run.ts @@ -1,6 +1,10 @@ import * as Debug from 'debug'; -import { ADBException, ERR_INCOMPATIBLE_UPDATE, ERR_VERSION_DOWNGRADE } from '../../errors'; +import { + ADBException, + ERR_INCOMPATIBLE_UPDATE, + ERR_VERSION_DOWNGRADE, +} from '../../errors'; import { Device, installApk, uninstallApp } from './adb'; import { AVD, getDefaultAVD } from './avd'; @@ -9,7 +13,12 @@ import { SDK } from './sdk'; const modulePrefix = 'native-run:android:utils:run'; -export async function selectDeviceByTarget(sdk: SDK, devices: readonly Device[], avds: readonly AVD[], target: string): Promise { +export async function selectDeviceByTarget( + sdk: SDK, + devices: readonly Device[], + avds: readonly AVD[], + target: string, +): Promise { const debug = Debug(`${modulePrefix}:${selectDeviceByTarget.name}`); debug('--target %s detected', target); @@ -23,7 +32,9 @@ export async function selectDeviceByTarget(sdk: SDK, devices: readonly Device[], const emulatorDevices = devices.filter(d => d.type === 'emulator'); - const pairAVD = async (emulator: Device): Promise<[Device, AVD | undefined]> => { + const pairAVD = async ( + emulator: Device, + ): Promise<[Device, AVD | undefined]> => { let avd: AVD | undefined; try { @@ -33,16 +44,24 @@ export async function selectDeviceByTarget(sdk: SDK, devices: readonly Device[], debug('Error with emulator %s: %O', emulator.serial, e); } - return [ emulator, avd ]; + return [emulator, avd]; }; - debug('Checking if any of %d running emulators are using AVD by ID: %s', emulatorDevices.length, target); - const emulatorsAndAVDs = await Promise.all(emulatorDevices.map(emulator => pairAVD(emulator))); - const emulators = emulatorsAndAVDs.filter((t): t is [Device, AVD] => typeof t[1] !== 'undefined'); - const emulator = emulators.find(([ , avd ]) => avd.id === target); + debug( + 'Checking if any of %d running emulators are using AVD by ID: %s', + emulatorDevices.length, + target, + ); + const emulatorsAndAVDs = await Promise.all( + emulatorDevices.map(emulator => pairAVD(emulator)), + ); + const emulators = emulatorsAndAVDs.filter( + (t): t is [Device, AVD] => typeof t[1] !== 'undefined', + ); + const emulator = emulators.find(([, avd]) => avd.id === target); if (emulator) { - const [ device, avd ] = emulator; + const [device, avd] = emulator; debug('Emulator %s found by AVD: %s', device.serial, avd.id); return device; } @@ -59,7 +78,9 @@ export async function selectDeviceByTarget(sdk: SDK, devices: readonly Device[], } } -export async function selectHardwareDevice(devices: readonly Device[]): Promise { +export async function selectHardwareDevice( + devices: readonly Device[], +): Promise { const hardwareDevices = devices.filter(d => d.type === 'hardware'); // If a hardware device is found, we prefer launching to it instead of in an emulator. @@ -68,13 +89,17 @@ export async function selectHardwareDevice(devices: readonly Device[]): Promise< } } -export async function selectVirtualDevice(sdk: SDK, devices: readonly Device[], avds: readonly AVD[]): Promise { +export async function selectVirtualDevice( + sdk: SDK, + devices: readonly Device[], + avds: readonly AVD[], +): Promise { const debug = Debug(`${modulePrefix}:${selectVirtualDevice.name}`); const emulators = devices.filter(d => d.type === 'emulator'); // If an emulator is running, use it. if (emulators.length > 0) { - const [ emulator ] = emulators; + const [emulator] = emulators; debug('Found running emulator: %s', emulator.serial); return emulator; } @@ -87,14 +112,22 @@ export async function selectVirtualDevice(sdk: SDK, devices: readonly Device[], return device; } -export async function installApkToDevice(sdk: SDK, device: Device, apk: string, appId: string): Promise { +export async function installApkToDevice( + sdk: SDK, + device: Device, + apk: string, + appId: string, +): Promise { process.stdout.write(`Installing ${apk}...\n`); try { await installApk(sdk, device, apk); } catch (e) { if (e instanceof ADBException) { - if (e.code === ERR_INCOMPATIBLE_UPDATE || e.code === ERR_VERSION_DOWNGRADE) { + if ( + e.code === ERR_INCOMPATIBLE_UPDATE || + e.code === ERR_VERSION_DOWNGRADE + ) { process.stdout.write(`${e.message} Uninstalling and trying again...\n`); await uninstallApp(sdk, device, appId); await installApk(sdk, device, apk); diff --git a/src/android/utils/sdk/__tests__/api.ts b/src/android/utils/sdk/__tests__/api.ts index 82e2c0c..8a4b2a4 100644 --- a/src/android/utils/sdk/__tests__/api.ts +++ b/src/android/utils/sdk/__tests__/api.ts @@ -1,7 +1,10 @@ -import { APISchemaPackage, findPackageBySchema, findUnsatisfiedPackages } from '../api'; +import { + APISchemaPackage, + findPackageBySchema, + findUnsatisfiedPackages, +} from '../api'; describe('android/utils/sdk/api', () => { - const FooPackage = { path: 'foo', location: '/Users/me/Android/sdk/foo', @@ -24,11 +27,17 @@ describe('android/utils/sdk/api', () => { }; const FooPackageSchema = { name: 'Foo', path: 'foo', version: '1' }; - const BarPackageSchema = { name: 'Bar', path: 'bar', version: /^1\.\d+\.\d+$/ }; + const BarPackageSchema = { + name: 'Bar', + path: 'bar', + version: /^1\.\d+\.\d+$/, + }; describe('findUnsatisfiedPackages', () => { - - const schemaPackages: APISchemaPackage[] = [FooPackageSchema, BarPackageSchema]; + const schemaPackages: APISchemaPackage[] = [ + FooPackageSchema, + BarPackageSchema, + ]; it('should return all package schemas for empty packages', () => { const result = findUnsatisfiedPackages([], schemaPackages); @@ -52,26 +61,28 @@ describe('android/utils/sdk/api', () => { const result = findUnsatisfiedPackages(api, schemaPackages); expect(result).toEqual([]); }); - }); describe('findPackageBySchema', () => { - it('should not find package in empty api', () => { const pkg = findPackageBySchema([], FooPackageSchema); expect(pkg).toBeUndefined(); }); it('should not find package for invalid version', () => { - const pkg = findPackageBySchema([FooPackage, BarPackageInvalidVersion], BarPackageSchema); + const pkg = findPackageBySchema( + [FooPackage, BarPackageInvalidVersion], + BarPackageSchema, + ); expect(pkg).toBeUndefined(); }); it('should find foo package by schema', () => { - const pkg = findPackageBySchema([FooPackage, BarPackage], FooPackageSchema); + const pkg = findPackageBySchema( + [FooPackage, BarPackage], + FooPackageSchema, + ); expect(pkg).toBe(FooPackage); }); - }); - }); diff --git a/src/android/utils/sdk/__tests__/index.ts b/src/android/utils/sdk/__tests__/index.ts index 1162126..644812d 100644 --- a/src/android/utils/sdk/__tests__/index.ts +++ b/src/android/utils/sdk/__tests__/index.ts @@ -1,7 +1,6 @@ import * as path from 'path'; describe('android/utils/sdk', () => { - let sdkUtils: typeof import('../'); let mockIsDir: jest.Mock; let mockHomedir: jest.Mock; @@ -26,9 +25,7 @@ describe('android/utils/sdk', () => { }); describe('SDK_DIRECTORIES', () => { - describe('windows', () => { - beforeEach(() => { jest.mock('path', () => path.win32); mockHomedir = jest.fn().mockReturnValue('C:\\Users\\me'); @@ -38,28 +35,45 @@ describe('android/utils/sdk', () => { it('should default to windows 10 local app data directory', async () => { Object.defineProperty(process, 'env', { value: {} }); sdkUtils = await import('../'); - expect(sdkUtils.SDK_DIRECTORIES.get('win32')).toEqual([path.win32.join('C:\\Users\\me\\AppData\\Local\\Android\\Sdk')]); + expect(sdkUtils.SDK_DIRECTORIES.get('win32')).toEqual([ + path.win32.join('C:\\Users\\me\\AppData\\Local\\Android\\Sdk'), + ]); }); it('should use LOCALAPPDATA environment variable if present', async () => { - Object.defineProperty(process, 'env', { value: { LOCALAPPDATA: path.win32.join('C:\\', 'Documents and Settings', 'me', 'Application Data') } }); + Object.defineProperty(process, 'env', { + value: { + LOCALAPPDATA: path.win32.join( + 'C:\\', + 'Documents and Settings', + 'me', + 'Application Data', + ), + }, + }); sdkUtils = await import('../'); - expect(sdkUtils.SDK_DIRECTORIES.get('win32')).toEqual([path.win32.join('C:\\Documents and Settings\\me\\Application Data\\Android\\Sdk')]); + expect(sdkUtils.SDK_DIRECTORIES.get('win32')).toEqual([ + path.win32.join( + 'C:\\Documents and Settings\\me\\Application Data\\Android\\Sdk', + ), + ]); }); - }); - }); describe('resolveSDKRoot', () => { - beforeEach(async () => { sdkUtils = await import('../'); }); it('should resolve with ANDROID_HOME if in environment', async () => { mockIsDir.mockResolvedValueOnce(true); - Object.defineProperty(process, 'env', { value: { ANDROID_HOME: '/some/dir', ANDROID_SDK_ROOT: '/some/other/dir' } }); + Object.defineProperty(process, 'env', { + value: { + ANDROID_HOME: '/some/dir', + ANDROID_SDK_ROOT: '/some/other/dir', + }, + }); await expect(sdkUtils.resolveSDKRoot()).resolves.toEqual('/some/dir'); expect(mockIsDir).toHaveBeenCalledTimes(1); expect(mockIsDir).toHaveBeenCalledWith('/some/dir'); @@ -67,8 +81,12 @@ describe('android/utils/sdk', () => { it('should resolve with ANDROID_SDK_ROOT if in environment', async () => { mockIsDir.mockResolvedValueOnce(true); - Object.defineProperty(process, 'env', { value: { ANDROID_SDK_ROOT: '/some/other/dir' } }); - await expect(sdkUtils.resolveSDKRoot()).resolves.toEqual('/some/other/dir'); + Object.defineProperty(process, 'env', { + value: { ANDROID_SDK_ROOT: '/some/other/dir' }, + }); + await expect(sdkUtils.resolveSDKRoot()).resolves.toEqual( + '/some/other/dir', + ); expect(mockIsDir).toHaveBeenCalledTimes(1); expect(mockIsDir).toHaveBeenCalledWith('/some/other/dir'); }); @@ -77,7 +95,9 @@ describe('android/utils/sdk', () => { mockIsDir.mockResolvedValueOnce(true); Object.defineProperty(process, 'env', { value: {} }); Object.defineProperty(process, 'platform', { value: 'linux' }); - await expect(sdkUtils.resolveSDKRoot()).resolves.toEqual('/home/me/Android/sdk'); + await expect(sdkUtils.resolveSDKRoot()).resolves.toEqual( + '/home/me/Android/sdk', + ); expect(mockIsDir).toHaveBeenCalledTimes(1); expect(mockIsDir).toHaveBeenCalledWith('/home/me/Android/sdk'); }); @@ -86,7 +106,9 @@ describe('android/utils/sdk', () => { mockIsDir.mockResolvedValueOnce(true); Object.defineProperty(process, 'env', { value: {} }); Object.defineProperty(process, 'platform', { value: 'darwin' }); - await expect(sdkUtils.resolveSDKRoot()).resolves.toEqual('/home/me/Library/Android/sdk'); + await expect(sdkUtils.resolveSDKRoot()).resolves.toEqual( + '/home/me/Library/Android/sdk', + ); expect(mockIsDir).toHaveBeenCalledTimes(1); expect(mockIsDir).toHaveBeenCalledWith('/home/me/Library/Android/sdk'); }); @@ -95,11 +117,11 @@ describe('android/utils/sdk', () => { mockIsDir.mockResolvedValueOnce(false); Object.defineProperty(process, 'env', { value: {} }); Object.defineProperty(process, 'platform', { value: 'darwin' }); - await expect(sdkUtils.resolveSDKRoot()).rejects.toThrowError('No valid Android SDK root found.'); + await expect(sdkUtils.resolveSDKRoot()).rejects.toThrowError( + 'No valid Android SDK root found.', + ); expect(mockIsDir).toHaveBeenCalledTimes(1); expect(mockIsDir).toHaveBeenCalledWith('/home/me/Library/Android/sdk'); }); - }); - }); diff --git a/src/android/utils/sdk/api.ts b/src/android/utils/sdk/api.ts index 169406a..6c85e43 100644 --- a/src/android/utils/sdk/api.ts +++ b/src/android/utils/sdk/api.ts @@ -10,31 +10,44 @@ export interface APILevel { readonly missingPackages?: APISchemaPackage[]; } -export async function getAPILevels(packages: SDKPackage[]): Promise { +export async function getAPILevels( + packages: SDKPackage[], +): Promise { const debug = Debug(`${modulePrefix}:${getAPILevels.name}`); const levels = [ ...new Set( packages .map(pkg => pkg.apiLevel) - .filter((apiLevel): apiLevel is string => typeof apiLevel !== 'undefined') + .filter( + (apiLevel): apiLevel is string => typeof apiLevel !== 'undefined', + ), ), - ].sort((a, b) => a <= b ? 1 : -1); + ].sort((a, b) => (a <= b ? 1 : -1)); const apis = levels.map(apiLevel => ({ apiLevel, packages: packages.filter(pkg => pkg.apiLevel === apiLevel), })); - debug('Discovered installed API Levels: %O', apis.map(api => ({ ...api, packages: api.packages.map(pkg => pkg.path) }))); + debug( + 'Discovered installed API Levels: %O', + apis.map(api => ({ ...api, packages: api.packages.map(pkg => pkg.path) })), + ); return apis; } -export function findUnsatisfiedPackages(packages: readonly SDKPackage[], schemas: readonly APISchemaPackage[]): APISchemaPackage[] { +export function findUnsatisfiedPackages( + packages: readonly SDKPackage[], + schemas: readonly APISchemaPackage[], +): APISchemaPackage[] { return schemas.filter(pkg => !findPackageBySchema(packages, pkg)); } -export function findPackageBySchema(packages: readonly SDKPackage[], pkg: APISchemaPackage): SDKPackage | undefined { +export function findPackageBySchema( + packages: readonly SDKPackage[], + pkg: APISchemaPackage, +): SDKPackage | undefined { const apiPkg = findPackageBySchemaPath(packages, pkg.path); if (apiPkg) { @@ -50,7 +63,10 @@ export function findPackageBySchema(packages: readonly SDKPackage[], pkg: APISch } } -export function findPackageBySchemaPath(packages: readonly SDKPackage[], path: string | RegExp): SDKPackage | undefined { +export function findPackageBySchemaPath( + packages: readonly SDKPackage[], + path: string | RegExp, +): SDKPackage | undefined { return packages.find(pkg => { if (typeof path !== 'string') { return !!pkg.path.match(path); @@ -60,14 +76,13 @@ export function findPackageBySchemaPath(packages: readonly SDKPackage[], path: s }); } -export type PartialAVDSchematic = ( - typeof import('../../data/avds/Pixel_3_API_29.json') | - typeof import('../../data/avds/Pixel_2_API_28.json') | - typeof import('../../data/avds/Pixel_2_API_27.json') | - typeof import('../../data/avds/Pixel_2_API_26.json') | - typeof import('../../data/avds/Pixel_API_25.json') | - typeof import('../../data/avds/Nexus_5X_API_24.json') -); +export type PartialAVDSchematic = + | typeof import('../../data/avds/Pixel_3_API_29.json') + | typeof import('../../data/avds/Pixel_2_API_28.json') + | typeof import('../../data/avds/Pixel_2_API_27.json') + | typeof import('../../data/avds/Pixel_2_API_26.json') + | typeof import('../../data/avds/Pixel_API_25.json') + | typeof import('../../data/avds/Nexus_5X_API_24.json'); export interface APISchemaPackage { readonly name: string; @@ -86,7 +101,11 @@ export const API_LEVEL_29: APISchema = Object.freeze({ validate: (packages: readonly SDKPackage[]) => { const schemas: APISchemaPackage[] = [ { name: 'Android Emulator', path: 'emulator', version: /.+/ }, - { name: 'Android SDK Platform 29', path: 'platforms;android-29', version: /.+/ }, + { + name: 'Android SDK Platform 29', + path: 'platforms;android-29', + version: /.+/, + }, ]; const missingPackages = findUnsatisfiedPackages(packages, schemas); @@ -101,7 +120,8 @@ export const API_LEVEL_29: APISchema = Object.freeze({ return missingPackages; }, - loadPartialAVDSchematic: async () => import('../../data/avds/Pixel_3_API_29.json'), + loadPartialAVDSchematic: async () => + import('../../data/avds/Pixel_3_API_29.json'), }); export const API_LEVEL_28: APISchema = Object.freeze({ @@ -109,7 +129,11 @@ export const API_LEVEL_28: APISchema = Object.freeze({ validate: (packages: readonly SDKPackage[]) => { const schemas: APISchemaPackage[] = [ { name: 'Android Emulator', path: 'emulator', version: /.+/ }, - { name: 'Android SDK Platform 28', path: 'platforms;android-28', version: /.+/ }, + { + name: 'Android SDK Platform 28', + path: 'platforms;android-28', + version: /.+/, + }, ]; const missingPackages = findUnsatisfiedPackages(packages, schemas); @@ -124,7 +148,8 @@ export const API_LEVEL_28: APISchema = Object.freeze({ return missingPackages; }, - loadPartialAVDSchematic: async () => import('../../data/avds/Pixel_2_API_28.json'), + loadPartialAVDSchematic: async () => + import('../../data/avds/Pixel_2_API_28.json'), }); export const API_LEVEL_27: APISchema = Object.freeze({ @@ -132,7 +157,11 @@ export const API_LEVEL_27: APISchema = Object.freeze({ validate: (packages: readonly SDKPackage[]) => { const schemas: APISchemaPackage[] = [ { name: 'Android Emulator', path: 'emulator', version: /.+/ }, - { name: 'Android SDK Platform 27', path: 'platforms;android-27', version: /.+/ }, + { + name: 'Android SDK Platform 27', + path: 'platforms;android-27', + version: /.+/, + }, ]; const missingPackages = findUnsatisfiedPackages(packages, schemas); @@ -147,7 +176,8 @@ export const API_LEVEL_27: APISchema = Object.freeze({ return missingPackages; }, - loadPartialAVDSchematic: async () => import('../../data/avds/Pixel_2_API_27.json'), + loadPartialAVDSchematic: async () => + import('../../data/avds/Pixel_2_API_27.json'), }); export const API_LEVEL_26: APISchema = Object.freeze({ @@ -155,7 +185,11 @@ export const API_LEVEL_26: APISchema = Object.freeze({ validate: (packages: readonly SDKPackage[]) => { const schemas: APISchemaPackage[] = [ { name: 'Android Emulator', path: 'emulator', version: /.+/ }, - { name: 'Android SDK Platform 26', path: 'platforms;android-26', version: /.+/ }, + { + name: 'Android SDK Platform 26', + path: 'platforms;android-26', + version: /.+/, + }, ]; const missingPackages = findUnsatisfiedPackages(packages, schemas); @@ -170,7 +204,8 @@ export const API_LEVEL_26: APISchema = Object.freeze({ return missingPackages; }, - loadPartialAVDSchematic: async () => import('../../data/avds/Pixel_2_API_26.json'), + loadPartialAVDSchematic: async () => + import('../../data/avds/Pixel_2_API_26.json'), }); export const API_LEVEL_25: APISchema = Object.freeze({ @@ -178,7 +213,11 @@ export const API_LEVEL_25: APISchema = Object.freeze({ validate: (packages: readonly SDKPackage[]) => { const schemas: APISchemaPackage[] = [ { name: 'Android Emulator', path: 'emulator', version: /.+/ }, - { name: 'Android SDK Platform 25', path: 'platforms;android-25', version: /.+/ }, + { + name: 'Android SDK Platform 25', + path: 'platforms;android-25', + version: /.+/, + }, ]; const missingPackages = findUnsatisfiedPackages(packages, schemas); @@ -193,7 +232,8 @@ export const API_LEVEL_25: APISchema = Object.freeze({ return missingPackages; }, - loadPartialAVDSchematic: async () => import('../../data/avds/Pixel_API_25.json'), + loadPartialAVDSchematic: async () => + import('../../data/avds/Pixel_API_25.json'), }); export const API_LEVEL_24: APISchema = Object.freeze({ @@ -201,7 +241,11 @@ export const API_LEVEL_24: APISchema = Object.freeze({ validate: (packages: readonly SDKPackage[]) => { const schemas: APISchemaPackage[] = [ { name: 'Android Emulator', path: 'emulator', version: /.+/ }, - { name: 'Android SDK Platform 24', path: 'platforms;android-24', version: /.+/ }, + { + name: 'Android SDK Platform 24', + path: 'platforms;android-24', + version: /.+/, + }, ]; const missingPackages = findUnsatisfiedPackages(packages, schemas); @@ -216,7 +260,8 @@ export const API_LEVEL_24: APISchema = Object.freeze({ return missingPackages; }, - loadPartialAVDSchematic: async () => import('../../data/avds/Nexus_5X_API_24.json'), + loadPartialAVDSchematic: async () => + import('../../data/avds/Nexus_5X_API_24.json'), }); export const API_LEVEL_SCHEMAS: readonly APISchema[] = [ diff --git a/src/android/utils/sdk/index.ts b/src/android/utils/sdk/index.ts index 075d7bb..c9683a9 100644 --- a/src/android/utils/sdk/index.ts +++ b/src/android/utils/sdk/index.ts @@ -3,18 +3,41 @@ import * as Debug from 'debug'; import * as os from 'os'; import * as pathlib from 'path'; -import { ERR_EMULATOR_HOME_NOT_FOUND, ERR_SDK_NOT_FOUND, ERR_SDK_PACKAGE_NOT_FOUND, SDKException } from '../../../errors'; +import { + ERR_EMULATOR_HOME_NOT_FOUND, + ERR_SDK_NOT_FOUND, + ERR_SDK_PACKAGE_NOT_FOUND, + SDKException, +} from '../../../errors'; import { isDir } from '../../../utils/fs'; -import { getAPILevelFromPackageXml, getNameFromPackageXml, getPathFromPackageXml, getVersionFromPackageXml, readPackageXml } from './xml'; +import { + getAPILevelFromPackageXml, + getNameFromPackageXml, + getPathFromPackageXml, + getVersionFromPackageXml, + readPackageXml, +} from './xml'; const modulePrefix = 'native-run:android:utils:sdk'; const homedir = os.homedir(); -export const SDK_DIRECTORIES: ReadonlyMap = new Map([ +export const SDK_DIRECTORIES: ReadonlyMap< + NodeJS.Platform, + string[] | undefined +> = new Map([ ['darwin', [pathlib.join(homedir, 'Library', 'Android', 'sdk')]], ['linux', [pathlib.join(homedir, 'Android', 'sdk')]], - ['win32', [pathlib.join(process.env.LOCALAPPDATA || pathlib.join(homedir, 'AppData', 'Local'), 'Android', 'Sdk')]], + [ + 'win32', + [ + pathlib.join( + process.env.LOCALAPPDATA || pathlib.join(homedir, 'AppData', 'Local'), + 'Android', + 'Sdk', + ), + ], + ], ]); export interface SDK { @@ -56,21 +79,23 @@ export async function findAllSDKPackages(sdk: SDK): Promise { onError: err => debug('Error while walking SDK: %O', err), walkerOptions: { pathFilter: p => { - if ([ - 'bin', - 'bin64', - 'lib', - 'lib64', - 'include', - 'clang-include', - 'skins', - 'data', - 'examples', - 'resources', - 'systrace', - 'extras', - // 'm2repository', - ].includes(pathlib.basename(p))) { + if ( + [ + 'bin', + 'bin64', + 'lib', + 'lib64', + 'include', + 'clang-include', + 'skins', + 'data', + 'examples', + 'resources', + 'systrace', + 'extras', + // 'm2repository', + ].includes(pathlib.basename(p)) + ) { return false; } @@ -84,12 +109,10 @@ export async function findAllSDKPackages(sdk: SDK): Promise { }); sdk.packages = await Promise.all( - contents - .map(p => pathlib.dirname(p)) - .map(p => getSDKPackage(p)) + contents.map(p => pathlib.dirname(p)).map(p => getSDKPackage(p)), ); - sdk.packages.sort((a, b) => a.name >= b.name ? 1 : -1); + sdk.packages.sort((a, b) => (a.name >= b.name ? 1 : -1)); return sdk.packages; } @@ -120,7 +143,10 @@ export async function getSDKPackage(location: string): Promise { debug('Encountered error with %s: %O', packageXmlPath, e); if (e.code === 'ENOENT') { - throw new SDKException(`SDK package not found by location: ${location}.`, ERR_SDK_PACKAGE_NOT_FOUND); + throw new SDKException( + `SDK package not found by location: ${location}.`, + ERR_SDK_PACKAGE_NOT_FOUND, + ); } throw e; @@ -138,7 +164,7 @@ export async function resolveSDKRoot(): Promise { // $ANDROID_HOME is deprecated, but still overrides $ANDROID_SDK_ROOT if // defined and valid. - if (process.env.ANDROID_HOME && await isDir(process.env.ANDROID_HOME)) { + if (process.env.ANDROID_HOME && (await isDir(process.env.ANDROID_HOME))) { debug('Using $ANDROID_HOME at %s', process.env.ANDROID_HOME); return process.env.ANDROID_HOME; } @@ -146,7 +172,10 @@ export async function resolveSDKRoot(): Promise { debug('Looking for $ANDROID_SDK_ROOT'); // No valid $ANDROID_HOME, try $ANDROID_SDK_ROOT. - if (process.env.ANDROID_SDK_ROOT && await isDir(process.env.ANDROID_SDK_ROOT)) { + if ( + process.env.ANDROID_SDK_ROOT && + (await isDir(process.env.ANDROID_SDK_ROOT)) + ) { debug('Using $ANDROID_SDK_ROOT at %s', process.env.ANDROID_SDK_ROOT); return process.env.ANDROID_SDK_ROOT; } @@ -173,8 +202,14 @@ export async function resolveEmulatorHome(): Promise { const debug = Debug(`${modulePrefix}:${resolveEmulatorHome.name}`); debug('Looking for $ANDROID_EMULATOR_HOME'); - if (process.env.ANDROID_EMULATOR_HOME && await isDir(process.env.ANDROID_EMULATOR_HOME)) { - debug('Using $ANDROID_EMULATOR_HOME at %s', process.env.$ANDROID_EMULATOR_HOME); + if ( + process.env.ANDROID_EMULATOR_HOME && + (await isDir(process.env.ANDROID_EMULATOR_HOME)) + ) { + debug( + 'Using $ANDROID_EMULATOR_HOME at %s', + process.env.$ANDROID_EMULATOR_HOME, + ); return process.env.ANDROID_EMULATOR_HOME; } @@ -187,7 +222,10 @@ export async function resolveEmulatorHome(): Promise { return homeEmulatorHome; } - throw new SDKException(`No valid Android Emulator home found.`, ERR_EMULATOR_HOME_NOT_FOUND); + throw new SDKException( + `No valid Android Emulator home found.`, + ERR_EMULATOR_HOME_NOT_FOUND, + ); } export async function resolveAVDHome(): Promise { @@ -195,7 +233,10 @@ export async function resolveAVDHome(): Promise { debug('Looking for $ANDROID_AVD_HOME'); - if (process.env.ANDROID_AVD_HOME && await isDir(process.env.ANDROID_AVD_HOME)) { + if ( + process.env.ANDROID_AVD_HOME && + (await isDir(process.env.ANDROID_AVD_HOME)) + ) { debug('Using $ANDROID_AVD_HOME at %s', process.env.$ANDROID_AVD_HOME); return process.env.ANDROID_AVD_HOME; } diff --git a/src/android/utils/sdk/xml.ts b/src/android/utils/sdk/xml.ts index be17eaa..cf765c1 100644 --- a/src/android/utils/sdk/xml.ts +++ b/src/android/utils/sdk/xml.ts @@ -2,13 +2,17 @@ import { readFile } from '@ionic/utils-fs'; import { ERR_INVALID_SDK_PACKAGE, SDKException } from '../../../errors'; -export function getAPILevelFromPackageXml(packageXml: import('elementtree').ElementTree): string | undefined { +export function getAPILevelFromPackageXml( + packageXml: import('elementtree').ElementTree, +): string | undefined { const apiLevel = packageXml.find('./localPackage/type-details/api-level'); return apiLevel && apiLevel.text ? apiLevel.text.toString() : undefined; } -export async function readPackageXml(path: string): Promise { +export async function readPackageXml( + path: string, +): Promise { const et = await import('elementtree'); const contents = await readFile(path, { encoding: 'utf8' }); const etree = et.parse(contents); @@ -16,7 +20,9 @@ export async function readPackageXml(path: string): Promise e && e.text ? e.text.toString() : ''; + const textFromElement = (e: import('elementtree').Element | null): string => + e && e.text ? e.text.toString() : ''; const versions: string[] = []; for (const version of versionElements.map(textFromElement)) { @@ -61,7 +78,10 @@ export function getVersionFromPackageXml(packageXml: import('elementtree').Eleme } if (versions.length === 0) { - throw new SDKException(`Invalid SDK package version.`, ERR_INVALID_SDK_PACKAGE); + throw new SDKException( + `Invalid SDK package version.`, + ERR_INVALID_SDK_PACKAGE, + ); } return versions.join('.'); diff --git a/src/errors.ts b/src/errors.ts index af9f51e..3f3b8cf 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,8 +4,15 @@ export const enum ExitCode { GENERAL = 1, } -export class Exception extends Error implements NodeJS.ErrnoException { - constructor(readonly message: string, readonly code?: T, readonly exitCode = ExitCode.GENERAL, readonly data?: D) { +export class Exception + extends Error + implements NodeJS.ErrnoException { + constructor( + readonly message: string, + readonly code?: T, + readonly exitCode = ExitCode.GENERAL, + readonly data?: D, + ) { super(message); } @@ -22,7 +29,10 @@ export class Exception extends Error implements No } } -export class AndroidException extends Exception { +export class AndroidException extends Exception< + T, + D +> { serialize() { return ( `${super.serialize()}\n\n` + @@ -47,7 +57,8 @@ export const ERR_INVALID_SYSTEM_IMAGE = 'ERR_INVALID_SYSTEM_IMAGE'; export const ERR_NON_ZERO_EXIT = 'ERR_NON_ZERO_EXIT'; export const ERR_NO_AVDS_FOUND = 'ERR_NO_AVDS_FOUND'; export const ERR_MISSING_SYSTEM_IMAGE = 'ERR_MISSING_SYSTEM_IMAGE'; -export const ERR_UNSUITABLE_API_INSTALLATION = 'ERR_UNSUITABLE_API_INSTALLATION'; +export const ERR_UNSUITABLE_API_INSTALLATION = + 'ERR_UNSUITABLE_API_INSTALLATION'; export const ERR_SDK_NOT_FOUND = 'ERR_SDK_NOT_FOUND'; export const ERR_SDK_PACKAGE_NOT_FOUND = 'ERR_SDK_PACKAGE_NOT_FOUND'; export const ERR_SDK_UNSATISFIED_PACKAGES = 'ERR_SDK_UNSATISFIED_PACKAGES'; @@ -57,66 +68,62 @@ export const ERR_NO_TARGET = 'ERR_NO_TARGET'; export const ERR_UNKNOWN_AVD = 'ERR_UNKNOWN_AVD'; export const ERR_UNSUPPORTED_API_LEVEL = 'ERR_UNSUPPORTED_API_LEVEL'; -export type CLIExceptionCode = ( - typeof ERR_BAD_INPUT -); +export type CLIExceptionCode = typeof ERR_BAD_INPUT; export class CLIException extends Exception {} -export type ADBExceptionCode = ( - typeof ERR_INCOMPATIBLE_UPDATE | - typeof ERR_VERSION_DOWNGRADE | - typeof ERR_MIN_SDK_VERSION | - typeof ERR_NO_CERTIFICATES | - typeof ERR_NON_ZERO_EXIT -); +export type ADBExceptionCode = + | typeof ERR_INCOMPATIBLE_UPDATE + | typeof ERR_VERSION_DOWNGRADE + | typeof ERR_MIN_SDK_VERSION + | typeof ERR_NO_CERTIFICATES + | typeof ERR_NON_ZERO_EXIT; export class ADBException extends AndroidException {} -export type AVDExceptionCode = ( - typeof ERR_INVALID_SKIN | - typeof ERR_INVALID_SYSTEM_IMAGE | - typeof ERR_UNSUITABLE_API_INSTALLATION | - typeof ERR_UNSUPPORTED_API_LEVEL | - typeof ERR_SDK_UNSATISFIED_PACKAGES | - typeof ERR_MISSING_SYSTEM_IMAGE -); +export type AVDExceptionCode = + | typeof ERR_INVALID_SKIN + | typeof ERR_INVALID_SYSTEM_IMAGE + | typeof ERR_UNSUITABLE_API_INSTALLATION + | typeof ERR_UNSUPPORTED_API_LEVEL + | typeof ERR_SDK_UNSATISFIED_PACKAGES + | typeof ERR_MISSING_SYSTEM_IMAGE; export class AVDException extends AndroidException {} -export type EmulatorExceptionCode = ( - typeof ERR_ALREADY_RUNNING | - typeof ERR_AVD_HOME_NOT_FOUND | - typeof ERR_INVALID_SERIAL | - typeof ERR_NON_ZERO_EXIT | - typeof ERR_UNKNOWN_AVD -); - -export class EmulatorException extends AndroidException {} - -export type AndroidRunExceptionCode = ( - typeof ERR_NO_AVDS_FOUND | - typeof ERR_TARGET_NOT_FOUND | - typeof ERR_NO_DEVICE | - typeof ERR_NO_TARGET -); - -export class AndroidRunException extends AndroidException {} - -export type SDKExceptionCode = ( - typeof ERR_EMULATOR_HOME_NOT_FOUND | - typeof ERR_INVALID_SDK_PACKAGE | - typeof ERR_SDK_NOT_FOUND | - typeof ERR_SDK_PACKAGE_NOT_FOUND -); +export type EmulatorExceptionCode = + | typeof ERR_ALREADY_RUNNING + | typeof ERR_AVD_HOME_NOT_FOUND + | typeof ERR_INVALID_SERIAL + | typeof ERR_NON_ZERO_EXIT + | typeof ERR_UNKNOWN_AVD; + +export class EmulatorException extends AndroidException< + EmulatorExceptionCode +> {} + +export type AndroidRunExceptionCode = + | typeof ERR_NO_AVDS_FOUND + | typeof ERR_TARGET_NOT_FOUND + | typeof ERR_NO_DEVICE + | typeof ERR_NO_TARGET; + +export class AndroidRunException extends AndroidException< + AndroidRunExceptionCode +> {} + +export type SDKExceptionCode = + | typeof ERR_EMULATOR_HOME_NOT_FOUND + | typeof ERR_INVALID_SDK_PACKAGE + | typeof ERR_SDK_NOT_FOUND + | typeof ERR_SDK_PACKAGE_NOT_FOUND; export class SDKException extends AndroidException {} -export type IOSRunExceptionCode = ( - typeof ERR_TARGET_NOT_FOUND | - typeof ERR_NO_DEVICE | - typeof ERR_NO_TARGET -); +export type IOSRunExceptionCode = + | typeof ERR_TARGET_NOT_FOUND + | typeof ERR_NO_DEVICE + | typeof ERR_NO_TARGET; export class IOSRunException extends Exception {} diff --git a/src/index.ts b/src/index.ts index 1c7f6be..223f46f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,13 @@ import * as Debug from 'debug'; import * as path from 'path'; -import { CLIException, ERR_BAD_INPUT, Exception, ExitCode, serializeError } from './errors'; +import { + CLIException, + ERR_BAD_INPUT, + Exception, + ExitCode, + serializeError, +} from './errors'; const debug = Debug('native-run'); @@ -19,7 +25,7 @@ export async function run(): Promise { } let cmd: Command; - const [ platform, ...platformArgs ] = args; + const [platform, ...platformArgs] = args; try { if (platform === 'android') { @@ -32,12 +38,21 @@ export async function run(): Promise { cmd = await import('./list'); await cmd.run(args); } else { - if (!platform || platform === 'help' || args.includes('--help') || args.includes('-h') || platform.startsWith('-')) { + if ( + !platform || + platform === 'help' || + args.includes('--help') || + args.includes('-h') || + platform.startsWith('-') + ) { cmd = await import('./help'); return cmd.run(args); } - throw new CLIException(`Unsupported platform: "${platform}"`, ERR_BAD_INPUT); + throw new CLIException( + `Unsupported platform: "${platform}"`, + ERR_BAD_INPUT, + ); } } catch (e) { debug('Caught fatal error: %O', e); diff --git a/src/ios/lib/client/afc.ts b/src/ios/lib/client/afc.ts index fc0b567..3474654 100644 --- a/src/ios/lib/client/afc.ts +++ b/src/ios/lib/client/afc.ts @@ -4,7 +4,13 @@ import * as net from 'net'; import * as path from 'path'; import { promisify } from 'util'; -import { AFCError, AFCProtocolClient, AFC_FILE_OPEN_FLAGS, AFC_OPS, AFC_STATUS } from '../protocol/afc'; +import { + AFCError, + AFCProtocolClient, + AFC_FILE_OPEN_FLAGS, + AFC_OPS, + AFC_STATUS, +} from '../protocol/afc'; import { ServiceClient } from './client'; @@ -57,11 +63,20 @@ export class AFCClient extends ServiceClient { // then path to file toCString(path).copy(data, 8); - const resp = await this.protocolClient.sendMessage({ operation: AFC_OPS.FILE_OPEN, data }); + const resp = await this.protocolClient.sendMessage({ + operation: AFC_OPS.FILE_OPEN, + data, + }); + if (resp.operation === AFC_OPS.FILE_OPEN_RES) { return resp.data; } - throw new Error(`There was an unknown error opening file ${path}, response: ${Array.prototype.toString.call(resp.data)}`); + + throw new Error( + `There was an unknown error opening file ${path}, response: ${Array.prototype.toString.call( + resp.data, + )}`, + ); } async closeFile(fd: Buffer) { @@ -76,7 +91,7 @@ export class AFCClient extends ServiceClient { debug(`uploadFile: ${srcPath}`); // read local file and get fd of destination - const [ srcFile, destFile ] = await Promise.all([ + const [srcFile, destFile] = await Promise.all([ await promisify(fs.readFile)(srcPath), await this.openFile(destPath), ]); @@ -114,10 +129,13 @@ export class AFCClient extends ServiceClient { const promises: Promise[] = []; for (const file of fs.readdirSync(dirPath)) { const filePath = path.join(dirPath, file); - const remotePath = path.join(destPath, path.relative(srcPath, filePath)); + const remotePath = path.join( + destPath, + path.relative(srcPath, filePath), + ); if (fs.lstatSync(filePath).isDirectory()) { promises.push( - _this.makeDirectory(remotePath).then(() => uploadDir(filePath)) + _this.makeDirectory(remotePath).then(() => uploadDir(filePath)), ); } else { // Create promise to add to promises array @@ -133,7 +151,8 @@ export class AFCClient extends ServiceClient { // wrap upload in a function in case we need to save it for later const uploadFile = (tries = 0) => { numOpenFiles++; - _this.uploadFile(filePath, remotePath) + _this + .uploadFile(filePath, remotePath) .then(() => { resolve(); numOpenFiles--; @@ -146,7 +165,9 @@ export class AFCClient extends ServiceClient { // Couldn't get fd for whatever reason, try again // # of retries is arbitrary and can be adjusted if (err.status === AFC_STATUS.NO_RESOURCES && tries < 10) { - debug(`Received NO_RESOURCES from AFC, retrying ${filePath} upload. ${tries}`); + debug( + `Received NO_RESOURCES from AFC, retrying ${filePath} upload. ${tries}`, + ); uploadFile(tries++); } else { numOpenFiles--; @@ -158,7 +179,9 @@ export class AFCClient extends ServiceClient { if (numOpenFiles < MAX_OPEN_FILES) { uploadFile(); } else { - debug(`numOpenFiles >= ${MAX_OPEN_FILES}, adding to pending queue. Length: ${pendingFileUploads.length}`); + debug( + `numOpenFiles >= ${MAX_OPEN_FILES}, adding to pending queue. Length: ${pendingFileUploads.length}`, + ); pendingFileUploads.push(uploadFile); } } @@ -166,7 +189,6 @@ export class AFCClient extends ServiceClient { await Promise.all(promises); } } - } function toCString(s: string) { diff --git a/src/ios/lib/client/debugserver.ts b/src/ios/lib/client/debugserver.ts index f616556..5ffa733 100644 --- a/src/ios/lib/client/debugserver.ts +++ b/src/ios/lib/client/debugserver.ts @@ -42,16 +42,19 @@ export class DebugserverClient extends ServiceClient { async kill() { const msg: any = { cmd: 'k', args: [] }; - return this.protocolClient.sendMessage(msg, (resp: string, resolve: any, reject: any) => { - this.protocolClient.socket.write('+'); - const parts = resp.split(';'); - for (const part of parts) { - if (part.includes('description')) { - // description:{hex encoded message like: "Terminated with signal 9"} - resolve(Buffer.from(part.split(':')[1], 'hex').toString('ascii')); + return this.protocolClient.sendMessage( + msg, + (resp: string, resolve: any, reject: any) => { + this.protocolClient.socket.write('+'); + const parts = resp.split(';'); + for (const part of parts) { + if (part.includes('description')) { + // description:{hex encoded message like: "Terminated with signal 9"} + resolve(Buffer.from(part.split(':')[1], 'hex').toString('ascii')); + } } - } - }); + }, + ); } // TODO support app args @@ -72,5 +75,4 @@ export class DebugserverClient extends ServiceClient { this.protocolClient.socket.write('+'); return resp; } - } diff --git a/src/ios/lib/client/installation_proxy.ts b/src/ios/lib/client/installation_proxy.ts index 964e8a3..e5d9582 100644 --- a/src/ios/lib/client/installation_proxy.ts +++ b/src/ios/lib/client/installation_proxy.ts @@ -1,18 +1,27 @@ import * as Debug from 'debug'; import * as net from 'net'; -import { LockdownCommand, LockdownProtocolClient, LockdownResponse } from '../protocol/lockdown'; +import { + LockdownCommand, + LockdownProtocolClient, + LockdownResponse, +} from '../protocol/lockdown'; import { ResponseError, ServiceClient } from './client'; const debug = Debug('native-run:ios:lib:client:installation_proxy'); interface IPOptions { - 'ApplicationsType'?: 'Any'; - 'PackageType'?: 'Developer'; - 'CFBundleIdentifier'?: string; - 'ReturnAttributes'?: ('CFBundleIdentifier' | 'CFBundleExecutable' | 'Container' | 'Path')[]; - 'BundleIDs'?: string[]; + ApplicationsType?: 'Any'; + PackageType?: 'Developer'; + CFBundleIdentifier?: string; + ReturnAttributes?: ( + | 'CFBundleIdentifier' + | 'CFBundleExecutable' + | 'Container' + | 'Path' + )[]; + BundleIDs?: string[]; } interface IPInstallPercentCompleteResponseItem extends LockdownResponse { @@ -54,7 +63,8 @@ interface IPLookupResponseItem extends LockdownResponse { type IPLookupResponse = IPLookupResponseItem[]; export interface IPLookupResult { - [key: string]: { // BundleId + // BundleId + [key: string]: { Container: string; CFBundleIdentifier: string; CFBundleExecutable: string; @@ -66,33 +76,49 @@ function isIPLookupResponse(resp: any): resp is IPLookupResponse { return resp.length && resp[0].LookupResult !== undefined; } -function isIPInstallPercentCompleteResponse(resp: any): resp is IPInstallPercentCompleteResponse { +function isIPInstallPercentCompleteResponse( + resp: any, +): resp is IPInstallPercentCompleteResponse { return resp.length && resp[0].PercentComplete !== undefined; } -function isIPInstallCFBundleIdentifierResponse(resp: any): resp is IPInstallCFBundleIdentifierResponse { +function isIPInstallCFBundleIdentifierResponse( + resp: any, +): resp is IPInstallCFBundleIdentifierResponse { return resp.length && resp[0].CFBundleIdentifier !== undefined; } -function isIPInstallCompleteResponse(resp: any): resp is IPInstallCompleteResponse { +function isIPInstallCompleteResponse( + resp: any, +): resp is IPInstallCompleteResponse { return resp.length && resp[0].Status === 'Complete'; } -export class InstallationProxyClient extends ServiceClient> { +export class InstallationProxyClient extends ServiceClient< + LockdownProtocolClient +> { constructor(public socket: net.Socket) { super(socket, new LockdownProtocolClient(socket)); } - async lookupApp(bundleIds: string[], options: IPOptions = { - 'ReturnAttributes': ['Path', 'Container', 'CFBundleExecutable', 'CFBundleIdentifier'], - 'ApplicationsType': 'Any', - }) { + async lookupApp( + bundleIds: string[], + options: IPOptions = { + ReturnAttributes: [ + 'Path', + 'Container', + 'CFBundleExecutable', + 'CFBundleIdentifier', + ], + ApplicationsType: 'Any', + }, + ) { debug(`lookupApp, options: ${JSON.stringify(options)}`); const resp = await this.protocolClient.sendMessage({ - 'Command': 'Lookup', - 'ClientOptions': { - 'BundleIDs': bundleIds, + Command: 'Lookup', + ClientOptions: { + BundleIDs: bundleIds, ...options, }, }); @@ -103,29 +129,38 @@ export class InstallationProxyClient extends ServiceClient { - if (isIPInstallCompleteResponse(resp)) { - resolve(); - } else if (isIPInstallPercentCompleteResponse(resp)) { - debug(`Installation status: ${resp[0].Status}, %${resp[0].PercentComplete}`); - } else if (isIPInstallCFBundleIdentifierResponse(resp)) { - debug(`Installed app: ${resp[0].CFBundleIdentifier}`); - } else { - reject(new ResponseError('There was an error installing app', resp)); - } - }); + (resp: any, resolve, reject) => { + if (isIPInstallCompleteResponse(resp)) { + resolve(); + } else if (isIPInstallPercentCompleteResponse(resp)) { + debug( + `Installation status: ${resp[0].Status}, %${resp[0].PercentComplete}`, + ); + } else if (isIPInstallCFBundleIdentifierResponse(resp)) { + debug(`Installed app: ${resp[0].CFBundleIdentifier}`); + } else { + reject(new ResponseError('There was an error installing app', resp)); + } + }, + ); } } diff --git a/src/ios/lib/client/lockdownd.ts b/src/ios/lib/client/lockdownd.ts index b640aa0..bf54b16 100644 --- a/src/ios/lib/client/lockdownd.ts +++ b/src/ios/lib/client/lockdownd.ts @@ -68,23 +68,39 @@ interface LockdowndQueryTypeResponse { Type: string; } -function isLockdowndServiceResponse(resp: any): resp is LockdowndServiceResponse { - return resp.Request === 'StartService' && resp.Service !== undefined && resp.Port !== undefined; +function isLockdowndServiceResponse( + resp: any, +): resp is LockdowndServiceResponse { + return ( + resp.Request === 'StartService' && + resp.Service !== undefined && + resp.Port !== undefined + ); } -function isLockdowndSessionResponse(resp: any): resp is LockdowndSessionResponse { +function isLockdowndSessionResponse( + resp: any, +): resp is LockdowndSessionResponse { return resp.Request === 'StartSession'; } -function isLockdowndAllValuesResponse(resp: any): resp is LockdowndAllValuesResponse { +function isLockdowndAllValuesResponse( + resp: any, +): resp is LockdowndAllValuesResponse { return resp.Request === 'GetValue' && resp.Value !== undefined; } function isLockdowndValueResponse(resp: any): resp is LockdowndValueResponse { - return resp.Request === 'GetValue' && resp.Key !== undefined && typeof resp.Value === 'string'; + return ( + resp.Request === 'GetValue' && + resp.Key !== undefined && + typeof resp.Value === 'string' + ); } -function isLockdowndQueryTypeResponse(resp: any): resp is LockdowndQueryTypeResponse { +function isLockdowndQueryTypeResponse( + resp: any, +): resp is LockdowndQueryTypeResponse { return resp.Request === 'QueryType' && resp.Type !== undefined; } @@ -97,8 +113,8 @@ export class LockdowndClient extends ServiceClient { debug(`startService: ${name}`); const resp = await this.protocolClient.sendMessage({ - 'Request': 'StartService', - 'Service': name, + Request: 'StartService', + Service: name, }); if (isLockdowndServiceResponse(resp)) { @@ -112,20 +128,23 @@ export class LockdowndClient extends ServiceClient { debug(`startSession: ${pairRecord}`); const resp = await this.protocolClient.sendMessage({ - 'Request': 'StartSession', - 'HostID': pairRecord.HostID, - 'SystemBUID': pairRecord.SystemBUID, + Request: 'StartSession', + HostID: pairRecord.HostID, + SystemBUID: pairRecord.SystemBUID, }); if (isLockdowndSessionResponse(resp)) { if (resp.EnableSessionSSL) { - this.protocolClient.socket = new tls.TLSSocket(this.protocolClient.socket, { - secureContext: tls.createSecureContext({ - secureProtocol: 'TLSv1_method', - cert: pairRecord.RootCertificate, - key: pairRecord.RootPrivateKey, - }), - }); + this.protocolClient.socket = new tls.TLSSocket( + this.protocolClient.socket, + { + secureContext: tls.createSecureContext({ + secureProtocol: 'TLSv1_method', + cert: pairRecord.RootCertificate, + key: pairRecord.RootPrivateKey, + }), + }, + ); debug(`Socket upgraded to TLS connection`); } // TODO: save sessionID for StopSession? @@ -137,7 +156,7 @@ export class LockdowndClient extends ServiceClient { async getAllValues() { debug(`getAllValues`); - const resp = await this.protocolClient.sendMessage({ 'Request': 'GetValue' }); + const resp = await this.protocolClient.sendMessage({ Request: 'GetValue' }); if (isLockdowndAllValuesResponse(resp)) { return resp.Value; @@ -150,8 +169,8 @@ export class LockdowndClient extends ServiceClient { debug(`getValue: ${val}`); const resp = await this.protocolClient.sendMessage({ - 'Request': 'GetValue', - 'Key': val, + Request: 'GetValue', + Key: val, }); if (isLockdowndValueResponse(resp)) { @@ -164,7 +183,9 @@ export class LockdowndClient extends ServiceClient { async queryType() { debug('queryType'); - const resp = await this.protocolClient.sendMessage({ 'Request': 'QueryType' }); + const resp = await this.protocolClient.sendMessage({ + Request: 'QueryType', + }); if (isLockdowndQueryTypeResponse(resp)) { return resp.Type; @@ -183,5 +204,4 @@ export class LockdowndClient extends ServiceClient { // TODO: validate pair and pair await this.startSession(pairRecord); } - } diff --git a/src/ios/lib/client/mobile_image_mounter.ts b/src/ios/lib/client/mobile_image_mounter.ts index 1667adc..5207c85 100644 --- a/src/ios/lib/client/mobile_image_mounter.ts +++ b/src/ios/lib/client/mobile_image_mounter.ts @@ -2,7 +2,12 @@ import * as Debug from 'debug'; import * as fs from 'fs'; import * as net from 'net'; -import { LockdownCommand, LockdownProtocolClient, LockdownResponse, isLockdownResponse } from '../protocol/lockdown'; +import { + LockdownCommand, + LockdownProtocolClient, + LockdownResponse, + isLockdownResponse, +} from '../protocol/lockdown'; import { ResponseError, ServiceClient } from './client'; @@ -11,7 +16,7 @@ const debug = Debug('native-run:ios:lib:client:mobile_image_mounter'); export type MIMMountResponse = LockdownResponse; export interface MIMMessage extends LockdownCommand { - 'ImageType': string; + ImageType: string; } export interface MIMLookupResponse extends LockdownResponse { @@ -26,15 +31,21 @@ export interface MIMUploadReceiveBytesResponse extends LockdownResponse { Status: 'ReceiveBytesAck'; } -function isMIMUploadCompleteResponse(resp: any): resp is MIMUploadCompleteResponse { +function isMIMUploadCompleteResponse( + resp: any, +): resp is MIMUploadCompleteResponse { return resp.Status === 'Complete'; } -function isMIMUploadReceiveBytesResponse(resp: any): resp is MIMUploadReceiveBytesResponse { +function isMIMUploadReceiveBytesResponse( + resp: any, +): resp is MIMUploadReceiveBytesResponse { return resp.Status === 'ReceiveBytesAck'; } -export class MobileImageMounterClient extends ServiceClient> { +export class MobileImageMounterClient extends ServiceClient< + LockdownProtocolClient +> { constructor(socket: net.Socket) { super(socket, new LockdownProtocolClient(socket)); } @@ -43,14 +54,17 @@ export class MobileImageMounterClient extends ServiceClient { - if (isMIMUploadReceiveBytesResponse(resp)) { - const imageStream = fs.createReadStream(imagePath); - imageStream.pipe(this.protocolClient.socket, { end: false }); - imageStream.on('error', err => reject(err)); - } else if (isMIMUploadCompleteResponse(resp)) { - resolve(); - } else { - reject(new ResponseError(`There was an error uploading image ${imagePath} to the device`, resp)); - } - }); + return this.protocolClient.sendMessage( + { + Command: 'ReceiveBytes', + ImageSize: imageSize, + ImageSignature: imageSig, + ImageType: 'Developer', + }, + (resp: any, resolve, reject) => { + if (isMIMUploadReceiveBytesResponse(resp)) { + const imageStream = fs.createReadStream(imagePath); + imageStream.pipe(this.protocolClient.socket, { end: false }); + imageStream.on('error', err => reject(err)); + } else if (isMIMUploadCompleteResponse(resp)) { + resolve(); + } else { + reject( + new ResponseError( + `There was an error uploading image ${imagePath} to the device`, + resp, + ), + ); + } + }, + ); } async lookupImage() { debug('lookupImage'); return this.protocolClient.sendMessage({ - 'Command': 'LookupImage', - 'ImageType': 'Developer', + Command: 'LookupImage', + ImageType: 'Developer', }); } } diff --git a/src/ios/lib/client/usbmuxd.ts b/src/ios/lib/client/usbmuxd.ts index 312f33e..8161e29 100644 --- a/src/ios/lib/client/usbmuxd.ts +++ b/src/ios/lib/client/usbmuxd.ts @@ -56,7 +56,9 @@ function isUsbmuxdDeviceResponse(resp: any): resp is UsbmuxdDeviceResponse { return resp.DeviceList !== undefined; } -function isUsbmuxdPairRecordResponse(resp: any): resp is UsbmuxdPairRecordResponse { +function isUsbmuxdPairRecordResponse( + resp: any, +): resp is UsbmuxdPairRecordResponse { return resp.PairRecordData !== undefined; } @@ -80,22 +82,27 @@ export class UsbmuxdClient extends ServiceClient { const resp = await this.protocolClient.sendMessage({ messageType: 'Connect', extraFields: { - 'DeviceID': device.DeviceID, - 'PortNumber': htons(port), + DeviceID: device.DeviceID, + PortNumber: htons(port), }, }); if (isUsbmuxdConnectResponse(resp) && resp.Number === 0) { return this.protocolClient.socket; } else { - throw new ResponseError(`There was an error connecting to ${device.DeviceID} on port ${port}`, resp); + throw new ResponseError( + `There was an error connecting to ${device.DeviceID} on port ${port}`, + resp, + ); } } async getDevices() { debug('getDevices'); - const resp = await this.protocolClient.sendMessage({ messageType: 'ListDevices' }); + const resp = await this.protocolClient.sendMessage({ + messageType: 'ListDevices', + }); if (isUsbmuxdDeviceResponse(resp)) { return resp.DeviceList; @@ -130,7 +137,7 @@ export class UsbmuxdClient extends ServiceClient { const resp = await this.protocolClient.sendMessage({ messageType: 'ReadPairRecord', - extraFields: { 'PairRecordID': udid }, + extraFields: { PairRecordID: udid }, }); if (isUsbmuxdPairRecordResponse(resp)) { @@ -144,10 +151,12 @@ export class UsbmuxdClient extends ServiceClient { return plist.parse(resp.PairRecordData.toString()) as any; // TODO: type guard } } else { - throw new ResponseError(`There was an error reading pair record for udid: ${udid}`, resp); + throw new ResponseError( + `There was an error reading pair record for udid: ${udid}`, + resp, + ); } } - } function htons(n: number) { diff --git a/src/ios/lib/manager.ts b/src/ios/lib/manager.ts index 9958fc9..8fa5625 100644 --- a/src/ios/lib/manager.ts +++ b/src/ios/lib/manager.ts @@ -8,22 +8,30 @@ import { DebugserverClient } from './client/debugserver'; import { InstallationProxyClient } from './client/installation_proxy'; import { LockdowndClient } from './client/lockdownd'; import { MobileImageMounterClient } from './client/mobile_image_mounter'; -import { UsbmuxdClient, UsbmuxdDevice, UsbmuxdPairRecord } from './client/usbmuxd'; +import { + UsbmuxdClient, + UsbmuxdDevice, + UsbmuxdPairRecord, +} from './client/usbmuxd'; export class ClientManager { private connections: net.Socket[]; constructor( public pairRecord: UsbmuxdPairRecord, public device: UsbmuxdDevice, - private lockdowndClient: LockdowndClient + private lockdowndClient: LockdowndClient, ) { this.connections = [lockdowndClient.socket]; } static async create(udid?: string) { - const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); + const usbmuxClient = new UsbmuxdClient( + UsbmuxdClient.connectUsbmuxdSocket(), + ); const device = await usbmuxClient.getDevice(udid); - const pairRecord = await usbmuxClient.readPairRecord(device.Properties.SerialNumber); + const pairRecord = await usbmuxClient.readPairRecord( + device.Properties.SerialNumber, + ); const lockdownSocket = await usbmuxClient.connect(device, 62078); const lockdownClient = new LockdowndClient(lockdownSocket); await lockdownClient.doHandshake(pairRecord); @@ -31,13 +39,17 @@ export class ClientManager { } async getUsbmuxdClient() { - const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); + const usbmuxClient = new UsbmuxdClient( + UsbmuxdClient.connectUsbmuxdSocket(), + ); this.connections.push(usbmuxClient.socket); return usbmuxClient; } async getLockdowndClient() { - const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); + const usbmuxClient = new UsbmuxdClient( + UsbmuxdClient.connectUsbmuxdSocket(), + ); const lockdownSocket = await usbmuxClient.connect(this.device, 62078); const lockdownClient = new LockdowndClient(lockdownSocket); this.connections.push(lockdownClient.socket); @@ -55,20 +67,39 @@ export class ClientManager { } async getInstallationProxyClient() { - return this.getServiceClient('com.apple.mobile.installation_proxy', InstallationProxyClient); + return this.getServiceClient( + 'com.apple.mobile.installation_proxy', + InstallationProxyClient, + ); } async getMobileImageMounterClient() { - return this.getServiceClient('com.apple.mobile.mobile_image_mounter', MobileImageMounterClient); + return this.getServiceClient( + 'com.apple.mobile.mobile_image_mounter', + MobileImageMounterClient, + ); } async getDebugserverClient() { - return this.getServiceClient('com.apple.debugserver', DebugserverClient, true); + return this.getServiceClient( + 'com.apple.debugserver', + DebugserverClient, + true, + ); } - private async getServiceClient>(name: string, ServiceType: new(...args: any[]) => T, disableSSL = false) { - const { port: servicePort, enableServiceSSL } = await this.lockdowndClient.startService(name); - const usbmuxClient = new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()); + private async getServiceClient>( + name: string, + ServiceType: new (...args: any[]) => T, + disableSSL = false, + ) { + const { + port: servicePort, + enableServiceSSL, + } = await this.lockdowndClient.startService(name); + const usbmuxClient = new UsbmuxdClient( + UsbmuxdClient.connectUsbmuxdSocket(), + ); let usbmuxdSocket = await usbmuxClient.connect(this.device, servicePort); if (enableServiceSSL) { @@ -91,8 +122,10 @@ export class ClientManager { tlsOptions.socket = proxy; await new Promise((res, rej) => { - const timeoutId = setTimeout(() => { rej('The TLS handshake failed to complete after 5s.'); }, 5000); - tls.connect(tlsOptions, function(this: tls.TLSSocket) { + const timeoutId = setTimeout(() => { + rej('The TLS handshake failed to complete after 5s.'); + }, 5000); + tls.connect(tlsOptions, function (this: tls.TLSSocket) { clearTimeout(timeoutId); // After the handshake, we don't need TLS or the proxy anymore, // since we'll just pass in the naked usbmuxd socket to the service client @@ -100,7 +133,6 @@ export class ClientManager { res(); }); }); - } else { tlsOptions.socket = usbmuxdSocket; usbmuxdSocket = tls.connect(tlsOptions); diff --git a/src/ios/lib/protocol/afc.ts b/src/ios/lib/protocol/afc.ts index c612b8f..d2e8e30 100644 --- a/src/ios/lib/protocol/afc.ts +++ b/src/ios/lib/protocol/afc.ts @@ -4,7 +4,9 @@ import * as net from 'net'; import { ProtocolClient, ProtocolReader, - ProtocolReaderCallback, ProtocolReaderFactory, ProtocolWriter + ProtocolReaderCallback, + ProtocolReaderFactory, + ProtocolWriter, } from './protocol'; const debug = Debug('native-run:ios:lib:protocol:afc'); @@ -42,97 +44,289 @@ export interface AFCStatusResponse { * AFC Operations */ export enum AFC_OPS { - INVALID = 0x00000000, /* Invalid */ - STATUS = 0x00000001, /* Status */ - DATA = 0x00000002, /* Data */ - READ_DIR = 0x00000003, /* ReadDir */ - READ_FILE = 0x00000004, /* ReadFile */ - WRITE_FILE = 0x00000005, /* WriteFile */ - WRITE_PART = 0x00000006, /* WritePart */ - TRUNCATE = 0x00000007, /* TruncateFile */ - REMOVE_PATH = 0x00000008, /* RemovePath */ - MAKE_DIR = 0x00000009, /* MakeDir */ - GET_FILE_INFO = 0x0000000A, /* GetFileInfo */ - GET_DEVINFO = 0x0000000B, /* GetDeviceInfo */ - WRITE_FILE_ATOM = 0x0000000C, /* WriteFileAtomic (tmp file+rename) */ - FILE_OPEN = 0x0000000D, /* FileRefOpen */ - FILE_OPEN_RES = 0x0000000E, /* FileRefOpenResult */ - FILE_READ = 0x0000000F, /* FileRefRead */ - FILE_WRITE = 0x00000010, /* FileRefWrite */ - FILE_SEEK = 0x00000011, /* FileRefSeek */ - FILE_TELL = 0x00000012, /* FileRefTell */ - FILE_TELL_RES = 0x00000013, /* FileRefTellResult */ - FILE_CLOSE = 0x00000014, /* FileRefClose */ - FILE_SET_SIZE = 0x00000015, /* FileRefSetFileSize (ftruncate) */ - GET_CON_INFO = 0x00000016, /* GetConnectionInfo */ - SET_CON_OPTIONS = 0x00000017, /* SetConnectionOptions */ - RENAME_PATH = 0x00000018, /* RenamePath */ - SET_FS_BS = 0x00000019, /* SetFSBlockSize (0x800000) */ - SET_SOCKET_BS = 0x0000001A, /* SetSocketBlockSize (0x800000) */ - FILE_LOCK = 0x0000001B, /* FileRefLock */ - MAKE_LINK = 0x0000001C, /* MakeLink */ - GET_FILE_HASH = 0x0000001D, /* GetFileHash */ - SET_FILE_MOD_TIME = 0x0000001E, /* SetModTime */ - GET_FILE_HASH_RANGE = 0x0000001F, /* GetFileHashWithRange */ - /* iOS 6+ */ - FILE_SET_IMMUTABLE_HINT = 0x00000020, /* FileRefSetImmutableHint */ - GET_SIZE_OF_PATH_CONTENTS = 0x00000021, /* GetSizeOfPathContents */ - REMOVE_PATH_AND_CONTENTS = 0x00000022, /* RemovePathAndContents */ - DIR_OPEN = 0x00000023, /* DirectoryEnumeratorRefOpen */ - DIR_OPEN_RESULT = 0x00000024, /* DirectoryEnumeratorRefOpenResult */ - DIR_READ = 0x00000025, /* DirectoryEnumeratorRefRead */ - DIR_CLOSE = 0x00000026, /* DirectoryEnumeratorRefClose */ - /* iOS 7+ */ - FILE_READ_OFFSET = 0x00000027, /* FileRefReadWithOffset */ - FILE_WRITE_OFFSET = 0x00000028, /* FileRefWriteWithOffset */ + /** + * Invalid + */ + INVALID = 0x00000000, + + /** + * Status + */ + STATUS = 0x00000001, + + /** + * Data + */ + DATA = 0x00000002, + + /** + * ReadDir + */ + READ_DIR = 0x00000003, + + /** + * ReadFile + */ + READ_FILE = 0x00000004, + + /** + * WriteFile + */ + WRITE_FILE = 0x00000005, + + /** + * WritePart + */ + WRITE_PART = 0x00000006, + + /** + * TruncateFile + */ + TRUNCATE = 0x00000007, + + /** + * RemovePath + */ + REMOVE_PATH = 0x00000008, + + /** + * MakeDir + */ + MAKE_DIR = 0x00000009, + + /** + * GetFileInfo + */ + GET_FILE_INFO = 0x0000000a, + + /** + * GetDeviceInfo + */ + GET_DEVINFO = 0x0000000b, + + /** + * WriteFileAtomic (tmp file+rename) + */ + WRITE_FILE_ATOM = 0x0000000c, + + /** + * FileRefOpen + */ + FILE_OPEN = 0x0000000d, + + /** + * FileRefOpenResult + */ + FILE_OPEN_RES = 0x0000000e, + + /** + * FileRefRead + */ + FILE_READ = 0x0000000f, + + /** + * FileRefWrite + */ + FILE_WRITE = 0x00000010, + + /** + * FileRefSeek + */ + FILE_SEEK = 0x00000011, + + /** + * FileRefTell + */ + FILE_TELL = 0x00000012, + + /** + * FileRefTellResult + */ + FILE_TELL_RES = 0x00000013, + + /** + * FileRefClose + */ + FILE_CLOSE = 0x00000014, + + /** + * FileRefSetFileSize (ftruncate) + */ + FILE_SET_SIZE = 0x00000015, + + /** + * GetConnectionInfo + */ + GET_CON_INFO = 0x00000016, + + /** + * SetConnectionOptions + */ + SET_CON_OPTIONS = 0x00000017, + + /** + * RenamePath + */ + RENAME_PATH = 0x00000018, + + /** + * SetFSBlockSize (0x800000) + */ + SET_FS_BS = 0x00000019, + + /** + * SetSocketBlockSize (0x800000) + */ + SET_SOCKET_BS = 0x0000001a, + + /** + * FileRefLock + */ + FILE_LOCK = 0x0000001b, + + /** + * MakeLink + */ + MAKE_LINK = 0x0000001c, + + /** + * GetFileHash + */ + GET_FILE_HASH = 0x0000001d, + + /** + * SetModTime + */ + SET_FILE_MOD_TIME = 0x0000001e, + + /** + * GetFileHashWithRange + */ + GET_FILE_HASH_RANGE = 0x0000001f, + + // iOS 6+ + + /** + * FileRefSetImmutableHint + */ + FILE_SET_IMMUTABLE_HINT = 0x00000020, + + /** + * GetSizeOfPathContents + */ + GET_SIZE_OF_PATH_CONTENTS = 0x00000021, + + /** + * RemovePathAndContents + */ + REMOVE_PATH_AND_CONTENTS = 0x00000022, + + /** + * DirectoryEnumeratorRefOpen + */ + DIR_OPEN = 0x00000023, + + /** + * DirectoryEnumeratorRefOpenResult + */ + DIR_OPEN_RESULT = 0x00000024, + + /** + * DirectoryEnumeratorRefRead + */ + DIR_READ = 0x00000025, + + /** + * DirectoryEnumeratorRefClose + */ + DIR_CLOSE = 0x00000026, + + // iOS 7+ + + /** + * FileRefReadWithOffset + */ + FILE_READ_OFFSET = 0x00000027, + + /** + * FileRefWriteWithOffset + */ + FILE_WRITE_OFFSET = 0x00000028, } /** * Error Codes */ export enum AFC_STATUS { - SUCCESS = 0, - UNKNOWN_ERROR = 1, - OP_HEADER_INVALID = 2, - NO_RESOURCES = 3, - READ_ERROR = 4, - WRITE_ERROR = 5, - UNKNOWN_PACKET_TYPE = 6, - INVALID_ARG = 7, - OBJECT_NOT_FOUND = 8, - OBJECT_IS_DIR = 9, - PERM_DENIED = 10, + SUCCESS = 0, + UNKNOWN_ERROR = 1, + OP_HEADER_INVALID = 2, + NO_RESOURCES = 3, + READ_ERROR = 4, + WRITE_ERROR = 5, + UNKNOWN_PACKET_TYPE = 6, + INVALID_ARG = 7, + OBJECT_NOT_FOUND = 8, + OBJECT_IS_DIR = 9, + PERM_DENIED = 10, SERVICE_NOT_CONNECTED = 11, - OP_TIMEOUT = 12, - TOO_MUCH_DATA = 13, - END_OF_DATA = 14, - OP_NOT_SUPPORTED = 15, - OBJECT_EXISTS = 16, - OBJECT_BUSY = 17, - NO_SPACE_LEFT = 18, - OP_WOULD_BLOCK = 19, - IO_ERROR = 20, - OP_INTERRUPTED = 21, - OP_IN_PROGRESS = 22, - INTERNAL_ERROR = 23, - MUX_ERROR = 30, - NO_MEM = 31, - NOT_ENOUGH_DATA = 32, - DIR_NOT_EMPTY = 33, - FORCE_SIGNED_TYPE = -1, + OP_TIMEOUT = 12, + TOO_MUCH_DATA = 13, + END_OF_DATA = 14, + OP_NOT_SUPPORTED = 15, + OBJECT_EXISTS = 16, + OBJECT_BUSY = 17, + NO_SPACE_LEFT = 18, + OP_WOULD_BLOCK = 19, + IO_ERROR = 20, + OP_INTERRUPTED = 21, + OP_IN_PROGRESS = 22, + INTERNAL_ERROR = 23, + MUX_ERROR = 30, + NO_MEM = 31, + NOT_ENOUGH_DATA = 32, + DIR_NOT_EMPTY = 33, + FORCE_SIGNED_TYPE = -1, } export enum AFC_FILE_OPEN_FLAGS { - RDONLY = 0x00000001, // r O_RDONLY - RW = 0x00000002, // r+ O_RDWR | O_CREAT - WRONLY = 0x00000003, // w O_WRONLY | O_CREAT | O_TRUNC - WR = 0x00000004, // w+ O_RDWR | O_CREAT | O_TRUNC - APPEND = 0x00000005, // a O_WRONLY | O_APPEND | O_CREAT - RDAPPEND = 0x00000006, // a+ O_RDWR | O_APPEND | O_CREAT + /** + * r (O_RDONLY) + */ + RDONLY = 0x00000001, + + /** + * r+ (O_RDWR | O_CREAT) + */ + RW = 0x00000002, + + /** + * w (O_WRONLY | O_CREAT | O_TRUNC) + */ + WRONLY = 0x00000003, + + /** + * w+ (O_RDWR | O_CREAT | O_TRUNC) + */ + WR = 0x00000004, + + /** + * a (O_WRONLY | O_APPEND | O_CREAT) + */ + APPEND = 0x00000005, + + /** + * a+ (O_RDWR | O_APPEND | O_CREAT) + */ + RDAPPEND = 0x00000006, } function isAFCResponse(resp: any): resp is AFCResponse { - return AFC_OPS[resp.operation] !== undefined && resp.id !== undefined && resp.data !== undefined; + return ( + AFC_OPS[resp.operation] !== undefined && + resp.id !== undefined && + resp.data !== undefined + ); } function isStatusResponse(resp: any): resp is AFCStatusResponse { @@ -163,14 +357,17 @@ export class AFCProtocolClient extends ProtocolClient { super( socket, new ProtocolReaderFactory(AFCProtocolReader), - new AFCProtocolWriter() + new AFCProtocolWriter(), ); const reader = this.readerFactory.create((resp, err) => { if (err && err instanceof AFCInternalError) { this.requestCallbacks[err.requestId](resp, err); } else if (isErrorStatusResponse(resp)) { - this.requestCallbacks[resp.id](resp, new AFCError(AFC_STATUS[resp.data], resp.data)); + this.requestCallbacks[resp.id]( + resp, + new AFCError(AFC_STATUS[resp.data], resp.data), + ); } else { this.requestCallbacks[resp.id](resp); } @@ -195,7 +392,6 @@ export class AFCProtocolClient extends ProtocolClient { this.writer.write(this.socket, { ...msg, requestId }); }); } - } export class AFCProtocolReader extends ProtocolReader { @@ -208,7 +404,10 @@ export class AFCProtocolReader extends ProtocolReader { parseHeader(data: Buffer) { const magic = data.slice(0, 8).toString('ascii'); if (magic !== AFC_MAGIC) { - throw new AFCInternalError(`Invalid AFC packet received (magic != ${AFC_MAGIC})`, data.readUInt32LE(24)); + throw new AFCInternalError( + `Invalid AFC packet received (magic != ${AFC_MAGIC})`, + data.readUInt32LE(24), + ); } // technically these are uint64 this.header = { @@ -234,20 +433,28 @@ export class AFCProtocolReader extends ProtocolReader { }; if (isStatusResponse(body)) { const status = data.readUInt32LE(0); - debug(`${AFC_OPS[this.header.operation]} response: ${AFC_STATUS[status]}`); + debug( + `${AFC_OPS[this.header.operation]} response: ${AFC_STATUS[status]}`, + ); body.data = status; } else if (data.length <= 8) { - debug(`${AFC_OPS[this.header.operation]} response: ${Array.prototype.toString.call(body)}`); + debug( + `${ + AFC_OPS[this.header.operation] + } response: ${Array.prototype.toString.call(body)}`, + ); } else { - debug(`${AFC_OPS[this.header.operation]} response length: ${data.length} bytes`); + debug( + `${AFC_OPS[this.header.operation]} response length: ${ + data.length + } bytes`, + ); } return body; } - } export class AFCProtocolWriter implements ProtocolWriter { - write(socket: net.Socket, msg: AFCMessage & { requestId: number }) { const { data, payload, operation, requestId } = msg; @@ -264,13 +471,22 @@ export class AFCProtocolWriter implements ProtocolWriter { socket.write(header); socket.write(data); if (data.length <= 8) { - debug(`socket write, header: { requestId: ${requestId}, operation: ${AFC_OPS[operation]}}, body: ${Array.prototype.toString.call(data)}`); + debug( + `socket write, header: { requestId: ${requestId}, operation: ${ + AFC_OPS[operation] + }}, body: ${Array.prototype.toString.call(data)}`, + ); } else { - debug(`socket write, header: { requestId: ${requestId}, operation: ${AFC_OPS[operation]}}, body: ${data.length} bytes`); + debug( + `socket write, header: { requestId: ${requestId}, operation: ${AFC_OPS[operation]}}, body: ${data.length} bytes`, + ); } - debug(`socket write, bytes written ${header.length} (header), ${data.length} (body)`); - if (payload) { socket.write(payload); } + debug( + `socket write, bytes written ${header.length} (header), ${data.length} (body)`, + ); + if (payload) { + socket.write(payload); + } } - } diff --git a/src/ios/lib/protocol/gdb.ts b/src/ios/lib/protocol/gdb.ts index 32c9564..966b566 100644 --- a/src/ios/lib/protocol/gdb.ts +++ b/src/ios/lib/protocol/gdb.ts @@ -4,7 +4,9 @@ import * as net from 'net'; import { ProtocolClient, ProtocolReader, - ProtocolReaderCallback, ProtocolReaderFactory, ProtocolWriter + ProtocolReaderCallback, + ProtocolReaderFactory, + ProtocolWriter, } from './protocol'; const debug = Debug('native-run:ios:lib:protocol:gdb'); @@ -16,25 +18,24 @@ export interface GDBMessage { } export class GDBProtocolClient extends ProtocolClient { - constructor(socket: net.Socket) { super( socket, new ProtocolReaderFactory(GDBProtocolReader), - new GDBProtocolWriter() + new GDBProtocolWriter(), ); } - } export class GDBProtocolReader extends ProtocolReader { - constructor(callback: ProtocolReaderCallback) { super(1 /* "Header" is '+' or '-' */, callback); } parseHeader(data: Buffer) { - if (data[0] !== ACK_SUCCESS) { throw new Error('Unsuccessful debugserver response'); } // TODO: retry? + if (data[0] !== ACK_SUCCESS) { + throw new Error('Unsuccessful debugserver response'); + } // TODO: retry? return -1; } @@ -51,24 +52,24 @@ export class GDBProtocolReader extends ProtocolReader { throw new Error('Invalid checksum received from debugserver'); } } else { - throw new Error('Didn\'t receive checksum'); + throw new Error("Didn't receive checksum"); } } - } export class GDBProtocolWriter implements ProtocolWriter { - write(socket: net.Socket, msg: GDBMessage) { const { cmd, args } = msg; debug(`Socket write: ${cmd}, args: ${args}`); // hex encode and concat all args - const encodedArgs = args.map(arg => Buffer.from(arg).toString('hex')).join().toUpperCase(); + const encodedArgs = args + .map(arg => Buffer.from(arg).toString('hex')) + .join() + .toUpperCase(); const checksumStr = calculateChecksum(cmd + encodedArgs); const formattedCmd = `$${cmd}${encodedArgs}#${checksumStr}`; socket.write(formattedCmd); } - } // hex value of (sum of cmd chars mod 256) diff --git a/src/ios/lib/protocol/lockdown.ts b/src/ios/lib/protocol/lockdown.ts index 9a4b18f..3f62503 100644 --- a/src/ios/lib/protocol/lockdown.ts +++ b/src/ios/lib/protocol/lockdown.ts @@ -5,7 +5,8 @@ import * as plist from 'plist'; import { PlistProtocolReader, ProtocolClient, - ProtocolReaderFactory, ProtocolWriter + ProtocolReaderFactory, + ProtocolWriter, } from './protocol'; const debug = Debug('native-run:ios:lib:protocol:lockdown'); @@ -38,24 +39,25 @@ export function isLockdownResponse(resp: any): resp is LockdownResponse { return isDefined(resp.Status); } -export function isLockdownErrorResponse(resp: any): resp is LockdownErrorResponse { +export function isLockdownErrorResponse( + resp: any, +): resp is LockdownErrorResponse { return isDefined(resp.Error); } -export class LockdownProtocolClient extends ProtocolClient { - +export class LockdownProtocolClient< + MessageType extends LockdownRequest | LockdownCommand = LockdownRequest +> extends ProtocolClient { constructor(socket: net.Socket) { super( socket, new ProtocolReaderFactory(LockdownProtocolReader), - new LockdownProtocolWriter() + new LockdownProtocolWriter(), ); } - } export class LockdownProtocolReader extends PlistProtocolReader { - constructor(callback: (data: any) => any) { super(LOCKDOWN_HEADER_SIZE, callback); } @@ -75,7 +77,6 @@ export class LockdownProtocolReader extends PlistProtocolReader { } export class LockdownProtocolWriter implements ProtocolWriter { - write(socket: net.Socket, plistData: any) { debug(`socket write: ${JSON.stringify(plistData)}`); const plistMessage = plist.build(plistData); @@ -84,5 +85,4 @@ export class LockdownProtocolWriter implements ProtocolWriter { socket.write(header); socket.write(plistMessage); } - } diff --git a/src/ios/lib/protocol/protocol.ts b/src/ios/lib/protocol/protocol.ts index b6247d2..77c1cff 100644 --- a/src/ios/lib/protocol/protocol.ts +++ b/src/ios/lib/protocol/protocol.ts @@ -7,7 +7,9 @@ const BPLIST_MAGIC = Buffer.from('bplist00'); export type ProtocolReaderCallback = (resp: any, err?: Error) => void; export class ProtocolReaderFactory { - constructor(private ProtocolReader: new (callback: ProtocolReaderCallback) => T) {} + constructor( + private ProtocolReader: new (callback: ProtocolReaderCallback) => T, + ) {} create(callback: (resp: any, err?: Error) => void): T { return new this.ProtocolReader(callback); @@ -20,7 +22,7 @@ export abstract class ProtocolReader { private buffer = Buffer.alloc(0); constructor( private headerSize: number, - protected callback: ProtocolReaderCallback + protected callback: ProtocolReaderCallback, ) { this.onData = this.onData.bind(this); } @@ -63,7 +65,9 @@ export abstract class ProtocolReader { } this.buffer = this.buffer.slice(this.body.length); // There are multiple messages here, call parse again - if (this.buffer.length) { this.onData(); } + if (this.buffer.length) { + this.onData(); + } } } catch (err) { this.callback(null, err); @@ -89,25 +93,40 @@ export abstract class ProtocolClient { constructor( public socket: net.Socket, protected readerFactory: ProtocolReaderFactory, - protected writer: ProtocolWriter + protected writer: ProtocolWriter, ) {} sendMessage(msg: MessageType): Promise; - sendMessage(msg: MessageType, callback: (response: ResponseType, resolve: any, reject: any) => void): Promise; - sendMessage(msg: MessageType, callback?: (response: ResponseType, resolve: any, reject: any) => void): Promise { + sendMessage( + msg: MessageType, + callback: (response: ResponseType, resolve: any, reject: any) => void, + ): Promise; + sendMessage( + msg: MessageType, + callback?: (response: ResponseType, resolve: any, reject: any) => void, + ): Promise { return new Promise((resolve, reject) => { - const reader = this.readerFactory.create(async (resp: ResponseType, err?: Error) => { - if (err) { - reject(err); - return; - } - if (callback) { - callback(resp, (...args: any[]) => { this.socket.removeListener('data', reader.onData); resolve(...args); }, reject); - } else { - this.socket.removeListener('data', reader.onData); - resolve(resp); - } - }); + const reader = this.readerFactory.create( + async (resp: ResponseType, err?: Error) => { + if (err) { + reject(err); + return; + } + if (callback) { + callback( + resp, + (...args: any[]) => { + this.socket.removeListener('data', reader.onData); + resolve(...args); + }, + reject, + ); + } else { + this.socket.removeListener('data', reader.onData); + resolve(resp); + } + }, + ); this.socket.on('data', reader.onData); this.writer.write(this.socket, msg); }); diff --git a/src/ios/lib/protocol/usbmux.ts b/src/ios/lib/protocol/usbmux.ts index 2ec1f26..4229d45 100644 --- a/src/ios/lib/protocol/usbmux.ts +++ b/src/ios/lib/protocol/usbmux.ts @@ -5,7 +5,8 @@ import * as plist from 'plist'; import { PlistProtocolReader, ProtocolClient, - ProtocolReaderFactory, ProtocolWriter + ProtocolReaderFactory, + ProtocolWriter, } from './protocol'; const debug = Debug('native-run:ios:lib:protocol:usbmux'); @@ -18,19 +19,16 @@ export interface UsbmuxMessage { } export class UsbmuxProtocolClient extends ProtocolClient { - constructor(socket: net.Socket) { super( socket, new ProtocolReaderFactory(UsbmuxProtocolReader), - new UsbmuxProtocolWriter() + new UsbmuxProtocolWriter(), ); } - } export class UsbmuxProtocolReader extends PlistProtocolReader { - constructor(callback: (data: any) => any) { super(USBMUXD_HEADER_SIZE, callback); } @@ -44,21 +42,21 @@ export class UsbmuxProtocolReader extends PlistProtocolReader { debug(`Response: ${JSON.stringify(resp)}`); return resp; } - } export class UsbmuxProtocolWriter implements ProtocolWriter { private useTag = 0; - write(socket: net.Socket, msg: UsbmuxMessage) { // TODO Usbmux message type + write(socket: net.Socket, msg: UsbmuxMessage) { + // TODO Usbmux message type debug(`socket write: ${JSON.stringify(msg)}`); const { messageType, extraFields } = msg; const plistMessage = plist.build({ - 'BundleID': 'io.ionic.native-run', // TODO - 'ClientVersionString': 'usbmux.js', // TODO - 'MessageType': messageType, - 'ProgName': 'native-run', // TODO - 'kLibUSBMuxVersion': 3, + BundleID: 'io.ionic.native-run', // TODO + ClientVersionString: 'usbmux.js', // TODO + MessageType: messageType, + ProgName: 'native-run', // TODO + kLibUSBMuxVersion: 3, ...extraFields, }); @@ -74,5 +72,4 @@ export class UsbmuxProtocolWriter implements ProtocolWriter { socket.write(header); socket.write(plistMessage); } - } diff --git a/src/ios/list.ts b/src/ios/list.ts index e2c8813..8c26b21 100644 --- a/src/ios/list.ts +++ b/src/ios/list.ts @@ -12,7 +12,7 @@ export async function run(args: readonly string[]): Promise { export async function list(args: readonly string[]): Promise { const errors: Exception[] = []; - const [ devices, virtualDevices ] = await Promise.all([ + const [devices, virtualDevices] = await Promise.all([ (async () => { try { const devices = await getConnectedDevices(); diff --git a/src/ios/run.ts b/src/ios/run.ts index 8e65c2f..91fbc1c 100644 --- a/src/ios/run.ts +++ b/src/ios/run.ts @@ -3,7 +3,12 @@ import * as Debug from 'debug'; import { existsSync, mkdtempSync } from 'fs'; import * as path from 'path'; -import { CLIException, ERR_BAD_INPUT, ERR_TARGET_NOT_FOUND, IOSRunException } from '../errors'; +import { + CLIException, + ERR_BAD_INPUT, + ERR_TARGET_NOT_FOUND, + IOSRunException, +} from '../errors'; import { getOptionValue } from '../utils/cli'; import { getBundleId, unzipIPA } from './utils/app'; @@ -48,14 +53,27 @@ export async function run(args: readonly string[]): Promise { } 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); + 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); + await runOnDevice( + devices[0].UniqueDeviceID, + appPath, + bundleId, + waitForApp, + ); } else { // use default sim - await runOnSimulator(simulators[simulators.length - 1].udid, appPath, bundleId, waitForApp); + await runOnSimulator( + simulators[simulators.length - 1].udid, + appPath, + bundleId, + waitForApp, + ); } } finally { if (isIPA) { diff --git a/src/ios/utils/app.ts b/src/ios/utils/app.ts index 32e895c..4fd5766 100644 --- a/src/ios/utils/app.ts +++ b/src/ios/utils/app.ts @@ -13,9 +13,11 @@ const debug = Debug('native-run:ios:utils:app'); export async function getBundleId(appPath: string) { const plistPath = path.resolve(appPath, 'Info.plist'); try { - const { stdout } = await execFile('/usr/libexec/PlistBuddy', - ['-c', 'Print :CFBundleIdentifier', plistPath], - { encoding: 'utf8' }); + const { stdout } = await execFile( + '/usr/libexec/PlistBuddy', + ['-c', 'Print :CFBundleIdentifier', plistPath], + { encoding: 'utf8' }, + ); if (stdout) { return stdout.trim(); } @@ -41,8 +43,10 @@ export async function unzipIPA(ipaPath: string, destPath: string) { } else { await mkdirp(path.dirname(dest)); const readStream = await openReadStream(entry); - readStream.on('error', (err: Error) => error = err); - readStream.on('end', () => { zipfile.readEntry(); }); + readStream.on('error', (err: Error) => (error = err)); + readStream.on('end', () => { + zipfile.readEntry(); + }); readStream.pipe(createWriteStream(dest)); } }); diff --git a/src/ios/utils/device.ts b/src/ios/utils/device.ts index 4524854..04eb758 100644 --- a/src/ios/utils/device.ts +++ b/src/ios/utils/device.ts @@ -5,7 +5,15 @@ import { promisify } from 'util'; import { Exception } from '../../errors'; import { onBeforeExit } from '../../utils/process'; -import { AFCError, AFC_STATUS, ClientManager, DeviceValues, IPLookupResult, LockdowndClient, UsbmuxdClient } from '../lib'; +import { + AFCError, + AFC_STATUS, + ClientManager, + DeviceValues, + IPLookupResult, + LockdowndClient, + UsbmuxdClient, +} from '../lib'; import { getDeveloperDiskImagePath } from './xcode'; @@ -17,15 +25,26 @@ export async function getConnectedDevices() { const usbmuxDevices = await usbmuxClient.getDevices(); usbmuxClient.socket.end(); - return Promise.all(usbmuxDevices.map(async (d): Promise => { - const socket = await new UsbmuxdClient(UsbmuxdClient.connectUsbmuxdSocket()).connect(d, 62078); - const device = await new LockdowndClient(socket).getAllValues(); - socket.end(); - return device; - })); + return Promise.all( + usbmuxDevices.map( + async (d): Promise => { + const socket = await new UsbmuxdClient( + UsbmuxdClient.connectUsbmuxdSocket(), + ).connect(d, 62078); + const device = await new LockdowndClient(socket).getAllValues(); + socket.end(); + return device; + }, + ), + ); } -export async function runOnDevice(udid: string, appPath: string, bundleId: string, waitForApp: boolean) { +export async function runOnDevice( + udid: string, + appPath: string, + bundleId: string, + waitForApp: boolean, +) { const clientManager = await ClientManager.create(udid); try { @@ -70,15 +89,29 @@ async function mountDeveloperDiskImage(clientManager: ClientManager) { 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 version = await (await clientManager.getLockdowndClient()).getValue( + 'ProductVersion', + ); const developerDiskImagePath = await getDeveloperDiskImagePath(version); - const developerDiskImageSig = readFileSync(`${developerDiskImagePath}.signature`); - await imageMounter.uploadImage(developerDiskImagePath, developerDiskImageSig); - await imageMounter.mountImage(developerDiskImagePath, developerDiskImageSig); + const developerDiskImageSig = readFileSync( + `${developerDiskImagePath}.signature`, + ); + await imageMounter.uploadImage( + developerDiskImagePath, + developerDiskImageSig, + ); + await imageMounter.mountImage( + developerDiskImagePath, + developerDiskImageSig, + ); } } -async function uploadApp(clientManager: ClientManager, srcPath: string, destinationPath: string) { +async function uploadApp( + clientManager: ClientManager, + srcPath: string, + destinationPath: string, +) { const afcClient = await clientManager.getAFCClient(); try { await afcClient.getFileInfo('PublicStaging'); @@ -92,7 +125,10 @@ async function uploadApp(clientManager: ClientManager, srcPath: string, destinat await afcClient.uploadDirectory(srcPath, destinationPath); } -async function launchApp(clientManager: ClientManager, appInfo: IPLookupResult[string]) { +async function launchApp( + clientManager: ClientManager, + appInfo: IPLookupResult[string], +) { let tries = 0; while (tries < 3) { const debugServerClient = await clientManager.getDebugserverClient(); diff --git a/src/ios/utils/simulator.ts b/src/ios/utils/simulator.ts index af1f95e..c6e483f 100644 --- a/src/ios/utils/simulator.ts +++ b/src/ios/utils/simulator.ts @@ -26,7 +26,7 @@ interface SimCtlRuntime { } interface SimCtlType { - readonly name: string; // "iPhone 7" + readonly name: string; // "iPhone 7" readonly identifier: string; // "com.apple.CoreSimulator.SimDeviceType.iPhone-7" } @@ -39,7 +39,9 @@ interface SimCtlOutput { } export async function getSimulators() { - const simctl = spawnSync('xcrun', ['simctl', 'list', '--json'], { encoding: 'utf8' }); + const simctl = spawnSync('xcrun', ['simctl', 'list', '--json'], { + encoding: 'utf8', + }); if (simctl.status) { throw new Exception(`Unable to retrieve simulator list: ${simctl.stderr}`); } @@ -52,48 +54,94 @@ export async function getSimulators() { try { const output: SimCtlOutput = JSON.parse(simctl.stdout); return output.runtimes - .filter(runtime => runtime.name.indexOf('watch') === -1 && runtime.name.indexOf('tv') === -1) - .map(runtime => (output.devices[runtime.identifier] || output.devices[runtime.name]) - .filter(device => device.isAvailable) - .map(device => ({ ...device, runtime })) + .filter( + runtime => + runtime.name.indexOf('watch') === -1 && + runtime.name.indexOf('tv') === -1, + ) + .map(runtime => + (output.devices[runtime.identifier] || output.devices[runtime.name]) + .filter(device => device.isAvailable) + .map(device => ({ ...device, runtime })), ) .reduce((prev, next) => prev.concat(next)) // flatten - .sort((a, b) => a.name < b.name ? -1 : 1); + .sort((a, b) => (a.name < b.name ? -1 : 1)); } catch (err) { throw new Exception(`Unable to retrieve simulator list: ${err.message}`); } } -export async function runOnSimulator(udid: string, appPath: string, bundleId: string, waitForApp: boolean) { +export async function runOnSimulator( + udid: string, + appPath: string, + bundleId: string, + waitForApp: boolean, +) { debug(`Booting simulator ${udid}`); - const bootResult = spawnSync('xcrun', ['simctl', 'boot', udid], { encoding: 'utf8' }); + const bootResult = 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 Exception(`There was an error booting simulator: ${bootResult.stderr}`); + if ( + bootResult.status && + !bootResult.stderr.includes( + 'Unable to boot device in current state: Booted', + ) + ) { + throw new Exception( + `There was an error booting simulator: ${bootResult.stderr}`, + ); } debug(`Installing ${appPath} on ${udid}`); - const installResult = spawnSync('xcrun', ['simctl', 'install', udid, appPath], { encoding: 'utf8' }); + const installResult = spawnSync( + 'xcrun', + ['simctl', 'install', udid, appPath], + { encoding: 'utf8' }, + ); if (installResult.status) { - throw new Exception(`There was an error installing app on simulator: ${installResult.stderr}`); + throw new Exception( + `There was an error installing app on simulator: ${installResult.stderr}`, + ); } const xCodePath = await getXCodePath(); debug(`Running simulator ${udid}`); - const openResult = spawnSync('open', [`${xCodePath}/Applications/Simulator.app`, '--args', '-CurrentDeviceUDID', udid], { encoding: 'utf8' }); + const openResult = spawnSync( + 'open', + [ + `${xCodePath}/Applications/Simulator.app`, + '--args', + '-CurrentDeviceUDID', + udid, + ], + { encoding: 'utf8' }, + ); if (openResult.status) { - throw new Exception(`There was an error opening simulator: ${openResult.stderr}`); + throw new Exception( + `There was an error opening simulator: ${openResult.stderr}`, + ); } debug(`Launching ${appPath} on ${udid}`); - const launchResult = spawnSync('xcrun', ['simctl', 'launch', udid, bundleId], { encoding: 'utf8' }); + const launchResult = spawnSync( + 'xcrun', + ['simctl', 'launch', udid, bundleId], + { encoding: 'utf8' }, + ); if (launchResult.status) { - throw new Exception(`There was an error launching app on simulator: ${launchResult.stderr}`); + throw new Exception( + `There was an error launching app on simulator: ${launchResult.stderr}`, + ); } if (waitForApp) { onBeforeExit(async () => { - const terminateResult = spawnSync('xcrun', ['simctl', 'terminate', udid, bundleId], { encoding: 'utf8' }); + const terminateResult = spawnSync( + 'xcrun', + ['simctl', 'terminate', udid, bundleId], + { encoding: 'utf8' }, + ); if (terminateResult.status) { debug('Unable to terminate app on simulator'); } @@ -109,7 +157,11 @@ async function waitForSimulatorClose(udid: string, bundleId: string) { // poll service list for bundle id const interval = setInterval(async () => { try { - const data = spawnSync('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'list'], { encoding: 'utf8' }); + const data = 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); diff --git a/src/ios/utils/xcode.ts b/src/ios/utils/xcode.ts index a0c36f9..3a8aaf2 100644 --- a/src/ios/utils/xcode.ts +++ b/src/ios/utils/xcode.ts @@ -7,23 +7,34 @@ import { execFile } from '../../utils/process'; type XcodeVersion = string; type XcodeBuildVersion = string; -export function getXcodeVersionInfo(): readonly [XcodeVersion, XcodeBuildVersion] { - const xcodeVersionInfo = spawnSync('xcodebuild', ['-version'], { encoding: 'utf8' }); +export function getXcodeVersionInfo(): readonly [ + XcodeVersion, + XcodeBuildVersion, +] { + const xcodeVersionInfo = spawnSync('xcodebuild', ['-version'], { + encoding: 'utf8', + }); if (xcodeVersionInfo.error) { throw xcodeVersionInfo.error; } try { const trimmed = xcodeVersionInfo.stdout.trim().split('\n'); - return ['Xcode ', 'Build version'].map((s, i) => trimmed[i].replace(s, '')) as [string, string]; + return ['Xcode ', 'Build version'].map((s, i) => + trimmed[i].replace(s, ''), + ) as [string, string]; } catch (error) { - throw new Exception(`There was an error trying to retrieve the Xcode version: ${xcodeVersionInfo.stderr}`); + throw new Exception( + `There was an error trying to retrieve the Xcode version: ${xcodeVersionInfo.stderr}`, + ); } } export async function getXCodePath() { try { - const { stdout } = await execFile('xcode-select', ['-p'], { encoding: 'utf8' }); + const { stdout } = await execFile('xcode-select', ['-p'], { + encoding: 'utf8', + }); if (stdout) { return stdout.trim(); } @@ -35,7 +46,9 @@ export async function getXCodePath() { export async function getDeveloperDiskImagePath(version: string) { const xCodePath = await getXCodePath(); - const versionDirs = await readdir(`${xCodePath}/Platforms/iPhoneOS.platform/DeviceSupport/`); + const versionDirs = await readdir( + `${xCodePath}/Platforms/iPhoneOS.platform/DeviceSupport/`, + ); const versionPrefix = version.match(/\d+\.\d+/); if (versionPrefix === null) { throw new Exception(`Invalid iOS version: ${version}`); @@ -46,5 +59,7 @@ export async function getDeveloperDiskImagePath(version: string) { return `${xCodePath}/Platforms/iPhoneOS.platform/DeviceSupport/${dir}/DeveloperDiskImage.dmg`; } } - throw new Exception(`Unable to find Developer Disk Image path for SDK ${version}. Do you have the right version of Xcode?`); + throw new Exception( + `Unable to find Developer Disk Image path for SDK ${version}. Do you have the right version of Xcode?`, + ); } diff --git a/src/list.ts b/src/list.ts index 8a3db11..694f625 100644 --- a/src/list.ts +++ b/src/list.ts @@ -2,7 +2,7 @@ import { stringify } from './utils/json'; import { Targets, formatTargets } from './utils/list'; export async function run(args: readonly string[]): Promise { - const [ ios, android ] = await Promise.all([ + const [ios, android] = await Promise.all([ (async (): Promise => { const cmd = await import('./ios/list'); return cmd.list(args); diff --git a/src/utils/__tests__/fn.ts b/src/utils/__tests__/fn.ts index 6188201..1dc9142 100644 --- a/src/utils/__tests__/fn.ts +++ b/src/utils/__tests__/fn.ts @@ -1,9 +1,7 @@ import { once } from '../fn'; describe('utils/fn', () => { - describe('once', () => { - it('should call function once despite multiple calls', () => { const mock = jest.fn(); const fn = once(mock); @@ -28,7 +26,5 @@ describe('utils/fn', () => { expect(mock).toHaveBeenCalledTimes(1); expect(r).toBe(expected); }); - }); - }); diff --git a/src/utils/__tests__/object.ts b/src/utils/__tests__/object.ts index b05324a..8eaca34 100644 --- a/src/utils/__tests__/object.ts +++ b/src/utils/__tests__/object.ts @@ -1,9 +1,7 @@ import { sort } from '../object'; describe('utils/object', () => { - describe('sort', () => { - it('should return the same object', () => { const obj = { c: 3, b: 2, a: 1 }; const result = sort(obj); @@ -17,7 +15,5 @@ describe('utils/object', () => { sort(obj); expect(Object.keys(obj)).toEqual(['a', 'b', 'c']); }); - }); - }); diff --git a/src/utils/cli.ts b/src/utils/cli.ts index eef5ab4..e5c4521 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -1,6 +1,17 @@ -export function getOptionValue(args: readonly string[], arg: string): string | undefined; -export function getOptionValue(args: readonly string[], arg: string, defaultValue: string): string; -export function getOptionValue(args: readonly string[], arg: string, defaultValue?: string): string | undefined { +export function getOptionValue( + args: readonly string[], + arg: string, +): string | undefined; +export function getOptionValue( + args: readonly string[], + arg: string, + defaultValue: string, +): string; +export function getOptionValue( + args: readonly string[], + arg: string, + defaultValue?: string, +): string | undefined { const i = args.indexOf(arg); if (i >= 0) { @@ -10,7 +21,10 @@ export function getOptionValue(args: readonly string[], arg: string, defaultValu return defaultValue; } -export function getOptionValues(args: readonly string[], arg: string): string[] { +export function getOptionValues( + args: readonly string[], + arg: string, +): string[] { const returnVal: string[] = []; args.map((entry: string, idx: number) => { if (entry === arg) { diff --git a/src/utils/ini.ts b/src/utils/ini.ts index 343d755..3ce88dc 100644 --- a/src/utils/ini.ts +++ b/src/utils/ini.ts @@ -6,7 +6,10 @@ const debug = Debug('native-run:android:utils:ini'); export type INIGuard = (o: any) => o is T; -export async function readINI(p: string, guard: INIGuard = (o: any): o is T => true): Promise { +export async function readINI( + p: string, + guard: INIGuard = (o: any): o is T => true, +): Promise { const ini = await import('ini'); try { @@ -16,19 +19,22 @@ export async function readINI(p: string, guard: INIGuard = if (!guard(config)) { throw new Error( `Invalid ini configuration file: ${p}\n` + - `The following guard was used: ${guard.toString()}\n` + - `INI config parsed as: ${util.inspect(config)}` + `The following guard was used: ${guard.toString()}\n` + + `INI config parsed as: ${util.inspect(config)}`, ); } - return { __filename: p, ...config as any }; + return { __filename: p, ...(config as any) }; } catch (e) { debug(e); } } -export async function writeINI(p: string, o: T): Promise { - const ini = await import ('ini'); +export async function writeINI( + p: string, + o: T, +): Promise { + const ini = await import('ini'); const contents = ini.encode(o); await writeFile(p, contents, { encoding: 'utf8' }); diff --git a/src/utils/json.ts b/src/utils/json.ts index a7e815e..93c702a 100644 --- a/src/utils/json.ts +++ b/src/utils/json.ts @@ -1,3 +1,7 @@ export function stringify(obj: any): string { - return JSON.stringify(obj, (k, v) => v instanceof RegExp ? v.toString() : v, '\t'); + return JSON.stringify( + obj, + (k, v) => (v instanceof RegExp ? v.toString() : v), + '\t', + ); } diff --git a/src/utils/list.ts b/src/utils/list.ts index 6764a46..62e92bb 100644 --- a/src/utils/list.ts +++ b/src/utils/list.ts @@ -1,4 +1,9 @@ -import { CLIException, ERR_BAD_INPUT, Exception, serializeError } from '../errors'; +import { + CLIException, + ERR_BAD_INPUT, + Exception, + serializeError, +} from '../errors'; import { stringify } from './json'; @@ -17,14 +22,20 @@ export interface Target { readonly format: () => string; } -export function formatTargets(args: readonly string[], targets: Targets): string { +export function formatTargets( + args: readonly string[], + targets: Targets, +): string { const { devices, virtualDevices, errors } = targets; const virtualOnly = args.includes('--virtual'); const devicesOnly = args.includes('--device'); if (virtualOnly && devicesOnly) { - throw new CLIException('Only one of --device or --virtual may be specified', ERR_BAD_INPUT); + throw new CLIException( + 'Only one of --device or --virtual may be specified', + ERR_BAD_INPUT, + ); } if (args.includes('--json')) { @@ -62,7 +73,7 @@ function printTargets(name: string, targets: readonly Target[]) { output += ` No ${name.toLowerCase()}s found\n`; } else { for (const target of targets) { - output += ` ${(target.format())}\n`; + output += ` ${target.format()}\n`; } } return output; diff --git a/src/utils/object.ts b/src/utils/object.ts index 88f2802..0ef07b1 100644 --- a/src/utils/object.ts +++ b/src/utils/object.ts @@ -3,11 +3,11 @@ export function sort(obj: T): T { entries.sort(([k1], [k2]) => k1.localeCompare(k2)); - for (const [ key ] of entries) { + for (const [key] of entries) { delete obj[key]; } - for (const [ key, value ] of entries) { + for (const [key, value] of entries) { (obj as any)[key] = value; } diff --git a/src/utils/process.ts b/src/utils/process.ts index 0aaba01..ee84207 100644 --- a/src/utils/process.ts +++ b/src/utils/process.ts @@ -17,24 +17,36 @@ export function onBeforeExit(fn: ExitQueueFn): void { exitQueue.push(fn); } -const BEFORE_EXIT_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK']; - -const beforeExitHandlerWrapper = (signal: NodeJS.Signals) => once(async () => { - debug('onBeforeExit handler: %s received', signal); - debug('onBeforeExit handler: running %s queued functions', exitQueue.length); - - for (const [ i, fn ] of exitQueue.entries()) { - try { - await fn(); - } catch (e) { - debug('Error from function %d in exit queue: %O', i, e); +const BEFORE_EXIT_SIGNALS: NodeJS.Signals[] = [ + 'SIGINT', + 'SIGTERM', + 'SIGHUP', + 'SIGBREAK', +]; + +const beforeExitHandlerWrapper = (signal: NodeJS.Signals) => + once(async () => { + debug('onBeforeExit handler: %s received', signal); + debug( + 'onBeforeExit handler: running %s queued functions', + exitQueue.length, + ); + + for (const [i, fn] of exitQueue.entries()) { + try { + await fn(); + } catch (e) { + debug('Error from function %d in exit queue: %O', i, e); + } } - } - debug('onBeforeExit handler: exiting (exit code %s)', process.exitCode ? process.exitCode : 0); + debug( + 'onBeforeExit handler: exiting (exit code %s)', + process.exitCode ? process.exitCode : 0, + ); - process.exit(); -}); + process.exit(); + }); for (const signal of BEFORE_EXIT_SIGNALS) { process.on(signal, beforeExitHandlerWrapper(signal)); diff --git a/src/utils/unzip.ts b/src/utils/unzip.ts index 501d6c2..990cc32 100644 --- a/src/utils/unzip.ts +++ b/src/utils/unzip.ts @@ -3,9 +3,21 @@ import { promisify } from 'util'; import { Entry, Options, ZipFile, ZipFileOptions } from 'yauzl'; // Specify which of possible overloads is being promisified -type YauzlOpenReadStream = (entry: Entry, options?: ZipFileOptions, callback?: (err: Error, stream: Readable) => void) => void; -type YauzlOpen = (path: string, options?: Options, callback?: (err: Error, zipfile: ZipFile) => void) => void; -type UnzipOnEntry = (entry: Entry, zipfile: ZipFile, openReadStream: (arg1: Entry, arg2?: ZipFileOptions) => Promise) => void; +type YauzlOpenReadStream = ( + entry: Entry, + options?: ZipFileOptions, + callback?: (err: Error, stream: Readable) => void, +) => void; +type YauzlOpen = ( + path: string, + options?: Options, + callback?: (err: Error, zipfile: ZipFile) => void, +) => void; +type UnzipOnEntry = ( + entry: Entry, + zipfile: ZipFile, + openReadStream: (arg1: Entry, arg2?: ZipFileOptions) => Promise, +) => void; export async function unzip(srcPath: string, onEntry: UnzipOnEntry) { const yauzl = await import('yauzl'); @@ -14,7 +26,9 @@ export async function unzip(srcPath: string, onEntry: UnzipOnEntry) { return new Promise(async (resolve, reject) => { try { const zipfile = await open(srcPath, { lazyEntries: true }); - const openReadStream = promisify(zipfile.openReadStream.bind(zipfile) as YauzlOpenReadStream); + const openReadStream = promisify( + zipfile.openReadStream.bind(zipfile) as YauzlOpenReadStream, + ); zipfile.once('error', reject); // resolve when either one happens zipfile.once('close', resolve); // fd of zip closed diff --git a/types/bplist-parser.d.ts b/types/bplist-parser.d.ts index 23622dc..83c3800 100644 --- a/types/bplist-parser.d.ts +++ b/types/bplist-parser.d.ts @@ -1,7 +1,10 @@ -declare module "bplist-parser" { +declare module 'bplist-parser' { export const maxObjectSize = 100000000; // 100Meg export const maxObjectCount = 32768; export function UID(id: string): void; - export function parseFile(fileNameOrBuffer: string | Buffer, callback: (err: Error, result: any) => any): any; + export function parseFile( + fileNameOrBuffer: string | Buffer, + callback: (err: Error, result: any) => any, + ): any; export function parseBuffer(buffer: Buffer): any; }