diff --git a/packages/platform-android/src/commands/runAndroid/__mocks__/tryLaunchEmulator.ts b/packages/platform-android/src/commands/runAndroid/__mocks__/tryLaunchEmulator.ts new file mode 100644 index 000000000..44746ea90 --- /dev/null +++ b/packages/platform-android/src/commands/runAndroid/__mocks__/tryLaunchEmulator.ts @@ -0,0 +1 @@ +export default async () => true; diff --git a/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts b/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts index daafc407b..830b2c56a 100644 --- a/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts +++ b/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts @@ -14,6 +14,7 @@ jest.mock('child_process', () => ({ })); jest.mock('../getAdbPath'); +jest.mock('../tryLaunchEmulator'); const {execFileSync} = require('child_process'); describe('--appFolder', () => { @@ -21,18 +22,17 @@ describe('--appFolder', () => { jest.clearAllMocks(); }); - it('uses task "install[Variant]" as default task', () => { + it('uses task "install[Variant]" as default task', async () => { // @ts-ignore - runOnAllDevices({ + await runOnAllDevices({ variant: 'debug', }); - expect(execFileSync.mock.calls[0][1]).toContain('installDebug'); }); - it('uses appFolder and default variant', () => { + it('uses appFolder and default variant', async () => { // @ts-ignore - runOnAllDevices({ + await runOnAllDevices({ appFolder: 'someApp', variant: 'debug', }); @@ -40,9 +40,9 @@ describe('--appFolder', () => { expect(execFileSync.mock.calls[0][1]).toContain('someApp:installDebug'); }); - it('uses appFolder and custom variant', () => { + it('uses appFolder and custom variant', async () => { // @ts-ignore - runOnAllDevices({ + await runOnAllDevices({ appFolder: 'anotherApp', variant: 'staging', }); @@ -52,9 +52,9 @@ describe('--appFolder', () => { ); }); - it('uses only task argument', () => { + it('uses only task argument', async () => { // @ts-ignore - runOnAllDevices({ + await runOnAllDevices({ tasks: ['someTask'], variant: 'debug', }); @@ -62,9 +62,9 @@ describe('--appFolder', () => { expect(execFileSync.mock.calls[0][1]).toContain('someTask'); }); - it('uses appFolder and custom task argument', () => { + it('uses appFolder and custom task argument', async () => { // @ts-ignore - runOnAllDevices({ + await runOnAllDevices({ appFolder: 'anotherApp', tasks: ['someTask'], variant: 'debug', @@ -73,9 +73,9 @@ describe('--appFolder', () => { expect(execFileSync.mock.calls[0][1]).toContain('anotherApp:someTask'); }); - it('uses multiple tasks', () => { + it('uses multiple tasks', async () => { // @ts-ignore - runOnAllDevices({ + await runOnAllDevices({ appFolder: 'app', tasks: ['clean', 'someTask'], }); diff --git a/packages/platform-android/src/commands/runAndroid/index.ts b/packages/platform-android/src/commands/runAndroid/index.ts index 373e582c7..a9fa9db00 100644 --- a/packages/platform-android/src/commands/runAndroid/index.ts +++ b/packages/platform-android/src/commands/runAndroid/index.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. * */ - import path from 'path'; import execa from 'execa'; import chalk from 'chalk'; @@ -135,7 +134,6 @@ function buildAndRun(args: Flags) { args.appIdSuffix, packageName, ); - const adbPath = getAdbPath(); if (args.deviceId) { return runOnSpecificDevice( diff --git a/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts b/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts index a36c0f408..e9779d7ba 100644 --- a/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts +++ b/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts @@ -12,6 +12,7 @@ import {logger, CLIError} from '@react-native-community/cli-tools'; import adb from './adb'; import tryRunAdbReverse from './tryRunAdbReverse'; import tryLaunchAppOnDevice from './tryLaunchAppOnDevice'; +import tryLaunchEmulator from './tryLaunchEmulator'; import {Flags} from '.'; function getTaskNames( @@ -27,13 +28,30 @@ function toPascalCase(value: string) { return value[0].toUpperCase() + value.slice(1); } -function runOnAllDevices( +async function runOnAllDevices( args: Flags, cmd: string, packageNameWithSuffix: string, packageName: string, adbPath: string, ) { + let devices = adb.getDevices(adbPath); + if (devices.length === 0) { + logger.info('Launching emulator...'); + const result = await tryLaunchEmulator(adbPath); + if (result.success) { + logger.info('Successfully launched emulator.'); + devices = adb.getDevices(adbPath); + } else { + logger.error( + `Failed to launch emulator. Reason: ${chalk.dim(result.error || '')}.`, + ); + logger.warn( + 'Please launch an emulator manually or connect a device. Otherwise app may fail to launch.', + ); + } + } + try { const tasks = args.tasks || ['install' + toPascalCase(args.variant)]; const gradleArgs = getTaskNames(args.appFolder, tasks); @@ -51,7 +69,6 @@ function runOnAllDevices( } catch (error) { throw createInstallError(error); } - const devices = adb.getDevices(adbPath); (devices.length > 0 ? devices : [undefined]).forEach( (device: string | void) => { diff --git a/packages/platform-android/src/commands/runAndroid/tryLaunchEmulator.ts b/packages/platform-android/src/commands/runAndroid/tryLaunchEmulator.ts new file mode 100644 index 000000000..9bdcf504c --- /dev/null +++ b/packages/platform-android/src/commands/runAndroid/tryLaunchEmulator.ts @@ -0,0 +1,72 @@ +import execa from 'execa'; +import Adb from './adb'; + +const emulatorCommand = process.env.ANDROID_HOME + ? `${process.env.ANDROID_HOME}/emulator/emulator` + : 'emulator'; + +const getEmulators = () => { + try { + const emulatorsOutput = execa.sync(emulatorCommand, ['-list-avds']).stdout; + return emulatorsOutput.split('\n').filter(name => name !== ''); + } catch { + return []; + } +}; + +const launchEmulator = async (emulatorName: string, adbPath: string) => { + return new Promise((resolve, reject) => { + const cp = execa(emulatorCommand, [`@${emulatorName}`], { + detached: true, + stdio: 'ignore', + }); + cp.unref(); + const timeout = 30; + + // Reject command after timeout + const rejectTimeout = setTimeout(() => { + cleanup(); + reject(`Could not start emulator within ${timeout} seconds.`); + }, timeout * 1000); + + const bootCheckInterval = setInterval(() => { + if (Adb.getDevices(adbPath).length > 0) { + cleanup(); + resolve(); + } + }, 1000); + + const cleanup = () => { + clearTimeout(rejectTimeout); + clearInterval(bootCheckInterval); + }; + + cp.on('exit', () => { + cleanup(); + reject('Emulator exited before boot.'); + }); + + cp.on('error', error => { + cleanup(); + reject(error.message); + }); + }); +}; + +export default async function tryLaunchEmulator( + adbPath: string, +): Promise<{success: boolean; error?: string}> { + const emulators = getEmulators(); + if (emulators.length > 0) { + try { + await launchEmulator(emulators[0], adbPath); + return {success: true}; + } catch (error) { + return {success: false, error}; + } + } + return { + success: false, + error: 'No emulators found as an output of `emulator -list-avds`', + }; +}