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