Skip to content

Commit c9790eb

Browse files
jayuthymikee
authored andcommitted
feat: launch emulator during run-android (#676)
* feat: launch android emulator in run-android * test: fix tests due to changes * refactor: address CR comments & improvements * fix: refetch devices info after emualtor launch * fix: remove redundant async/await * fix: run emulator from android home path Co-Authored-By: Michał Pierzchała <[email protected]> * refactor: use interval to check if emulator is booted * fix emulator path * adjust wording * moar adjustments
1 parent b86313f commit c9790eb

File tree

5 files changed

+105
-17
lines changed

5 files changed

+105
-17
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default async () => true;

packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,35 @@ jest.mock('child_process', () => ({
1414
}));
1515

1616
jest.mock('../getAdbPath');
17+
jest.mock('../tryLaunchEmulator');
1718
const {execFileSync} = require('child_process');
1819

1920
describe('--appFolder', () => {
2021
beforeEach(() => {
2122
jest.clearAllMocks();
2223
});
2324

24-
it('uses task "install[Variant]" as default task', () => {
25+
it('uses task "install[Variant]" as default task', async () => {
2526
// @ts-ignore
26-
runOnAllDevices({
27+
await runOnAllDevices({
2728
variant: 'debug',
2829
});
29-
3030
expect(execFileSync.mock.calls[0][1]).toContain('installDebug');
3131
});
3232

33-
it('uses appFolder and default variant', () => {
33+
it('uses appFolder and default variant', async () => {
3434
// @ts-ignore
35-
runOnAllDevices({
35+
await runOnAllDevices({
3636
appFolder: 'someApp',
3737
variant: 'debug',
3838
});
3939

4040
expect(execFileSync.mock.calls[0][1]).toContain('someApp:installDebug');
4141
});
4242

43-
it('uses appFolder and custom variant', () => {
43+
it('uses appFolder and custom variant', async () => {
4444
// @ts-ignore
45-
runOnAllDevices({
45+
await runOnAllDevices({
4646
appFolder: 'anotherApp',
4747
variant: 'staging',
4848
});
@@ -52,19 +52,19 @@ describe('--appFolder', () => {
5252
);
5353
});
5454

55-
it('uses only task argument', () => {
55+
it('uses only task argument', async () => {
5656
// @ts-ignore
57-
runOnAllDevices({
57+
await runOnAllDevices({
5858
tasks: ['someTask'],
5959
variant: 'debug',
6060
});
6161

6262
expect(execFileSync.mock.calls[0][1]).toContain('someTask');
6363
});
6464

65-
it('uses appFolder and custom task argument', () => {
65+
it('uses appFolder and custom task argument', async () => {
6666
// @ts-ignore
67-
runOnAllDevices({
67+
await runOnAllDevices({
6868
appFolder: 'anotherApp',
6969
tasks: ['someTask'],
7070
variant: 'debug',
@@ -73,9 +73,9 @@ describe('--appFolder', () => {
7373
expect(execFileSync.mock.calls[0][1]).toContain('anotherApp:someTask');
7474
});
7575

76-
it('uses multiple tasks', () => {
76+
it('uses multiple tasks', async () => {
7777
// @ts-ignore
78-
runOnAllDevices({
78+
await runOnAllDevices({
7979
appFolder: 'app',
8080
tasks: ['clean', 'someTask'],
8181
});

packages/platform-android/src/commands/runAndroid/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
*/
8-
98
import path from 'path';
109
import execa from 'execa';
1110
import chalk from 'chalk';
@@ -135,7 +134,6 @@ function buildAndRun(args: Flags) {
135134
args.appIdSuffix,
136135
packageName,
137136
);
138-
139137
const adbPath = getAdbPath();
140138
if (args.deviceId) {
141139
return runOnSpecificDevice(

packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {logger, CLIError} from '@react-native-community/cli-tools';
1212
import adb from './adb';
1313
import tryRunAdbReverse from './tryRunAdbReverse';
1414
import tryLaunchAppOnDevice from './tryLaunchAppOnDevice';
15+
import tryLaunchEmulator from './tryLaunchEmulator';
1516
import {Flags} from '.';
1617

1718
function getTaskNames(
@@ -27,13 +28,30 @@ function toPascalCase(value: string) {
2728
return value[0].toUpperCase() + value.slice(1);
2829
}
2930

30-
function runOnAllDevices(
31+
async function runOnAllDevices(
3132
args: Flags,
3233
cmd: string,
3334
packageNameWithSuffix: string,
3435
packageName: string,
3536
adbPath: string,
3637
) {
38+
let devices = adb.getDevices(adbPath);
39+
if (devices.length === 0) {
40+
logger.info('Launching emulator...');
41+
const result = await tryLaunchEmulator(adbPath);
42+
if (result.success) {
43+
logger.info('Successfully launched emulator.');
44+
devices = adb.getDevices(adbPath);
45+
} else {
46+
logger.error(
47+
`Failed to launch emulator. Reason: ${chalk.dim(result.error || '')}.`,
48+
);
49+
logger.warn(
50+
'Please launch an emulator manually or connect a device. Otherwise app may fail to launch.',
51+
);
52+
}
53+
}
54+
3755
try {
3856
const tasks = args.tasks || ['install' + toPascalCase(args.variant)];
3957
const gradleArgs = getTaskNames(args.appFolder, tasks);
@@ -51,7 +69,6 @@ function runOnAllDevices(
5169
} catch (error) {
5270
throw createInstallError(error);
5371
}
54-
const devices = adb.getDevices(adbPath);
5572

5673
(devices.length > 0 ? devices : [undefined]).forEach(
5774
(device: string | void) => {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import execa from 'execa';
2+
import Adb from './adb';
3+
4+
const emulatorCommand = process.env.ANDROID_HOME
5+
? `${process.env.ANDROID_HOME}/emulator/emulator`
6+
: 'emulator';
7+
8+
const getEmulators = () => {
9+
try {
10+
const emulatorsOutput = execa.sync(emulatorCommand, ['-list-avds']).stdout;
11+
return emulatorsOutput.split('\n').filter(name => name !== '');
12+
} catch {
13+
return [];
14+
}
15+
};
16+
17+
const launchEmulator = async (emulatorName: string, adbPath: string) => {
18+
return new Promise((resolve, reject) => {
19+
const cp = execa(emulatorCommand, [`@${emulatorName}`], {
20+
detached: true,
21+
stdio: 'ignore',
22+
});
23+
cp.unref();
24+
const timeout = 30;
25+
26+
// Reject command after timeout
27+
const rejectTimeout = setTimeout(() => {
28+
cleanup();
29+
reject(`Could not start emulator within ${timeout} seconds.`);
30+
}, timeout * 1000);
31+
32+
const bootCheckInterval = setInterval(() => {
33+
if (Adb.getDevices(adbPath).length > 0) {
34+
cleanup();
35+
resolve();
36+
}
37+
}, 1000);
38+
39+
const cleanup = () => {
40+
clearTimeout(rejectTimeout);
41+
clearInterval(bootCheckInterval);
42+
};
43+
44+
cp.on('exit', () => {
45+
cleanup();
46+
reject('Emulator exited before boot.');
47+
});
48+
49+
cp.on('error', error => {
50+
cleanup();
51+
reject(error.message);
52+
});
53+
});
54+
};
55+
56+
export default async function tryLaunchEmulator(
57+
adbPath: string,
58+
): Promise<{success: boolean; error?: string}> {
59+
const emulators = getEmulators();
60+
if (emulators.length > 0) {
61+
try {
62+
await launchEmulator(emulators[0], adbPath);
63+
return {success: true};
64+
} catch (error) {
65+
return {success: false, error};
66+
}
67+
}
68+
return {
69+
success: false,
70+
error: 'No emulators found as an output of `emulator -list-avds`',
71+
};
72+
}

0 commit comments

Comments
 (0)