18d307f52SEvan Baconimport spawnAsync from '@expo/spawn-async'; 28d307f52SEvan Baconimport chalk from 'chalk'; 38d307f52SEvan Baconimport { spawn } from 'child_process'; 48d307f52SEvan Baconimport os from 'os'; 58d307f52SEvan Bacon 6*8a424bebSJames Ideimport { Device, getAttachedDevicesAsync, isBootAnimationCompleteAsync } from './adb'; 78d307f52SEvan Baconimport * as Log from '../../../log'; 8c4dcb435SEvan Baconimport { AbortCommandError } from '../../../utils/errors'; 9c4dcb435SEvan Baconimport { installExitHooks } from '../../../utils/exit'; 108d307f52SEvan Bacon 118d307f52SEvan Baconexport const EMULATOR_MAX_WAIT_TIMEOUT = 60 * 1000 * 3; 128d307f52SEvan Bacon 138d307f52SEvan Baconexport function whichEmulator(): string { 148d307f52SEvan Bacon // https://developer.android.com/studio/command-line/variables 158d307f52SEvan Bacon // TODO: Add ANDROID_SDK_ROOT support as well https://github.com/expo/expo/pull/16516#discussion_r820037917 168d307f52SEvan Bacon if (process.env.ANDROID_HOME) { 178d307f52SEvan Bacon return `${process.env.ANDROID_HOME}/emulator/emulator`; 188d307f52SEvan Bacon } 198d307f52SEvan Bacon return 'emulator'; 208d307f52SEvan Bacon} 218d307f52SEvan Bacon 228d307f52SEvan Bacon/** Returns a list of emulator names. */ 238d307f52SEvan Baconexport async function listAvdsAsync(): Promise<Device[]> { 248d307f52SEvan Bacon try { 258d307f52SEvan Bacon const { stdout } = await spawnAsync(whichEmulator(), ['-list-avds']); 268d307f52SEvan Bacon return stdout 278d307f52SEvan Bacon .split(os.EOL) 288d307f52SEvan Bacon .filter(Boolean) 298d307f52SEvan Bacon .map((name) => ({ 308d307f52SEvan Bacon name, 318d307f52SEvan Bacon type: 'emulator', 328d307f52SEvan Bacon // unsure from this 338d307f52SEvan Bacon isBooted: false, 348d307f52SEvan Bacon isAuthorized: true, 358d307f52SEvan Bacon })); 368d307f52SEvan Bacon } catch { 378d307f52SEvan Bacon return []; 388d307f52SEvan Bacon } 398d307f52SEvan Bacon} 408d307f52SEvan Bacon 418d307f52SEvan Bacon/** Start an Android device and wait until it is booted. */ 428d307f52SEvan Baconexport async function startDeviceAsync( 438d307f52SEvan Bacon device: Pick<Device, 'name'>, 448d307f52SEvan Bacon { 458d307f52SEvan Bacon timeout = EMULATOR_MAX_WAIT_TIMEOUT, 468d307f52SEvan Bacon interval = 1000, 478d307f52SEvan Bacon }: { 488d307f52SEvan Bacon /** Time in milliseconds to wait before asserting a timeout error. */ 498d307f52SEvan Bacon timeout?: number; 508d307f52SEvan Bacon interval?: number; 518d307f52SEvan Bacon } = {} 528d307f52SEvan Bacon): Promise<Device> { 538d307f52SEvan Bacon Log.log(`\u203A Opening emulator ${chalk.bold(device.name)}`); 548d307f52SEvan Bacon 558d307f52SEvan Bacon // Start a process to open an emulator 568d307f52SEvan Bacon const emulatorProcess = spawn( 578d307f52SEvan Bacon whichEmulator(), 588d307f52SEvan Bacon [ 598d307f52SEvan Bacon `@${device.name}`, 608d307f52SEvan Bacon // disable animation for faster boot -- this might make it harder to detect if it mounted properly tho 618d307f52SEvan Bacon //'-no-boot-anim' 628d307f52SEvan Bacon ], 638d307f52SEvan Bacon { 648d307f52SEvan Bacon stdio: 'ignore', 658d307f52SEvan Bacon detached: true, 668d307f52SEvan Bacon } 678d307f52SEvan Bacon ); 688d307f52SEvan Bacon 698d307f52SEvan Bacon emulatorProcess.unref(); 708d307f52SEvan Bacon 718d307f52SEvan Bacon return new Promise<Device>((resolve, reject) => { 728d307f52SEvan Bacon const waitTimer = setInterval(async () => { 738d307f52SEvan Bacon try { 748d307f52SEvan Bacon const bootedDevices = await getAttachedDevicesAsync(); 758d307f52SEvan Bacon const connected = bootedDevices.find(({ name }) => name === device.name); 768d307f52SEvan Bacon if (connected) { 778d307f52SEvan Bacon const isBooted = await isBootAnimationCompleteAsync(connected.pid); 788d307f52SEvan Bacon if (isBooted) { 798d307f52SEvan Bacon stopWaiting(); 808d307f52SEvan Bacon resolve(connected); 818d307f52SEvan Bacon } 828d307f52SEvan Bacon } 838d307f52SEvan Bacon } catch (error) { 848d307f52SEvan Bacon stopWaiting(); 858d307f52SEvan Bacon reject(error); 868d307f52SEvan Bacon } 878d307f52SEvan Bacon }, interval); 888d307f52SEvan Bacon 898d307f52SEvan Bacon // Reject command after timeout 908d307f52SEvan Bacon const maxTimer = setTimeout(() => { 918d307f52SEvan Bacon const manualCommand = `${whichEmulator()} @${device.name}`; 928d307f52SEvan Bacon stopWaitingAndReject( 938d307f52SEvan Bacon `It took too long to start the Android emulator: ${device.name}. You can try starting the emulator manually from the terminal with: ${manualCommand}` 948d307f52SEvan Bacon ); 958d307f52SEvan Bacon }, timeout); 968d307f52SEvan Bacon 978d307f52SEvan Bacon const stopWaiting = () => { 988d307f52SEvan Bacon clearTimeout(maxTimer); 998d307f52SEvan Bacon clearInterval(waitTimer); 100c4dcb435SEvan Bacon removeExitHook(); 1018d307f52SEvan Bacon }; 1028d307f52SEvan Bacon 1038d307f52SEvan Bacon const stopWaitingAndReject = (message: string) => { 1048d307f52SEvan Bacon stopWaiting(); 1058d307f52SEvan Bacon reject(new Error(message)); 1068d307f52SEvan Bacon }; 1078d307f52SEvan Bacon 108c4dcb435SEvan Bacon const removeExitHook = installExitHooks((signal) => { 109c4dcb435SEvan Bacon stopWaiting(); 110c4dcb435SEvan Bacon emulatorProcess.kill(signal); 111c4dcb435SEvan Bacon reject(new AbortCommandError()); 112c4dcb435SEvan Bacon }); 113c4dcb435SEvan Bacon 1148d307f52SEvan Bacon emulatorProcess.on('error', ({ message }) => stopWaitingAndReject(message)); 1158d307f52SEvan Bacon 1168d307f52SEvan Bacon emulatorProcess.on('exit', () => { 1178d307f52SEvan Bacon const manualCommand = `${whichEmulator()} @${device.name}`; 1188d307f52SEvan Bacon stopWaitingAndReject( 1198d307f52SEvan Bacon `The emulator (${device.name}) quit before it finished opening. You can try starting the emulator manually from the terminal with: ${manualCommand}` 1208d307f52SEvan Bacon ); 1218d307f52SEvan Bacon }); 1228d307f52SEvan Bacon }); 1238d307f52SEvan Bacon} 124