1import spawnAsync from '@expo/spawn-async'; 2import chalk from 'chalk'; 3import { spawn } from 'child_process'; 4import os from 'os'; 5 6import * as Log from '../../../log'; 7import { Device, getAttachedDevicesAsync, isBootAnimationCompleteAsync } from './adb'; 8 9export const EMULATOR_MAX_WAIT_TIMEOUT = 60 * 1000 * 3; 10 11export function whichEmulator(): string { 12 // https://developer.android.com/studio/command-line/variables 13 // TODO: Add ANDROID_SDK_ROOT support as well https://github.com/expo/expo/pull/16516#discussion_r820037917 14 if (process.env.ANDROID_HOME) { 15 return `${process.env.ANDROID_HOME}/emulator/emulator`; 16 } 17 return 'emulator'; 18} 19 20/** Returns a list of emulator names. */ 21export async function listAvdsAsync(): Promise<Device[]> { 22 try { 23 const { stdout } = await spawnAsync(whichEmulator(), ['-list-avds']); 24 return stdout 25 .split(os.EOL) 26 .filter(Boolean) 27 .map((name) => ({ 28 name, 29 type: 'emulator', 30 // unsure from this 31 isBooted: false, 32 isAuthorized: true, 33 })); 34 } catch { 35 return []; 36 } 37} 38 39/** Start an Android device and wait until it is booted. */ 40export async function startDeviceAsync( 41 device: Pick<Device, 'name'>, 42 { 43 timeout = EMULATOR_MAX_WAIT_TIMEOUT, 44 interval = 1000, 45 }: { 46 /** Time in milliseconds to wait before asserting a timeout error. */ 47 timeout?: number; 48 interval?: number; 49 } = {} 50): Promise<Device> { 51 Log.log(`\u203A Opening emulator ${chalk.bold(device.name)}`); 52 53 // Start a process to open an emulator 54 const emulatorProcess = spawn( 55 whichEmulator(), 56 [ 57 `@${device.name}`, 58 // disable animation for faster boot -- this might make it harder to detect if it mounted properly tho 59 //'-no-boot-anim' 60 ], 61 { 62 stdio: 'ignore', 63 detached: true, 64 } 65 ); 66 67 emulatorProcess.unref(); 68 69 return new Promise<Device>((resolve, reject) => { 70 const waitTimer = setInterval(async () => { 71 try { 72 const bootedDevices = await getAttachedDevicesAsync(); 73 const connected = bootedDevices.find(({ name }) => name === device.name); 74 if (connected) { 75 const isBooted = await isBootAnimationCompleteAsync(connected.pid); 76 if (isBooted) { 77 stopWaiting(); 78 resolve(connected); 79 } 80 } 81 } catch (error) { 82 stopWaiting(); 83 reject(error); 84 } 85 }, interval); 86 87 // Reject command after timeout 88 const maxTimer = setTimeout(() => { 89 const manualCommand = `${whichEmulator()} @${device.name}`; 90 stopWaitingAndReject( 91 `It took too long to start the Android emulator: ${device.name}. You can try starting the emulator manually from the terminal with: ${manualCommand}` 92 ); 93 }, timeout); 94 95 const stopWaiting = () => { 96 clearTimeout(maxTimer); 97 clearInterval(waitTimer); 98 }; 99 100 const stopWaitingAndReject = (message: string) => { 101 stopWaiting(); 102 reject(new Error(message)); 103 }; 104 105 emulatorProcess.on('error', ({ message }) => stopWaitingAndReject(message)); 106 107 emulatorProcess.on('exit', () => { 108 const manualCommand = `${whichEmulator()} @${device.name}`; 109 stopWaitingAndReject( 110 `The emulator (${device.name}) quit before it finished opening. You can try starting the emulator manually from the terminal with: ${manualCommand}` 111 ); 112 }); 113 }); 114} 115