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