diff --git a/packages/cli-platform-android/src/commands/runAndroid/__tests__/tryLaunchEmulator.test.ts b/packages/cli-platform-android/src/commands/runAndroid/__tests__/tryLaunchEmulator.test.ts new file mode 100644 index 000000000..657bda037 --- /dev/null +++ b/packages/cli-platform-android/src/commands/runAndroid/__tests__/tryLaunchEmulator.test.ts @@ -0,0 +1,69 @@ +import {execFileSync, execSync} from 'child_process'; +import execa from 'execa'; +import tryLaunchEmulator from '../tryLaunchEmulator'; + +jest.mock('child_process'); +jest.mock('execa'); + +const flushPromises = () => Promise.resolve().then(() => Promise.resolve()); + +describe('tryLaunchEmulator', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.clearAllMocks(); + + (execa.sync as jest.Mock).mockReturnValue({stdout: 'Pixel_9'}); + (execa as unknown as jest.Mock).mockReturnValue({ + on: jest.fn(), + unref: jest.fn(), + }); + (execSync as jest.Mock).mockReturnValue( + Buffer.from('List of devices attached\nemulator-5554\tdevice\n'), + ); + (execFileSync as jest.Mock).mockReturnValue(Buffer.from('')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('waits for Android framework boot completion after adb connects', async () => { + const resultPromise = tryLaunchEmulator('/path/to/adb'); + const onResult = jest.fn(); + resultPromise.then(onResult); + + jest.advanceTimersByTime(1000); + await flushPromises(); + + expect(onResult).not.toHaveBeenCalled(); + expect(execFileSync).toHaveBeenCalledWith('/path/to/adb', [ + '-s', + 'emulator-5554', + 'shell', + 'getprop', + 'sys.boot_completed', + ]); + + (execFileSync as jest.Mock).mockReturnValue(Buffer.from('1\n')); + jest.advanceTimersByTime(1000); + + await expect(resultPromise).resolves.toEqual({success: true}); + }); + + test('allows up to two minutes for a cold boot to complete', async () => { + const resultPromise = tryLaunchEmulator('/path/to/adb'); + const onResult = jest.fn(); + resultPromise.then(onResult); + + jest.advanceTimersByTime(119_000); + await flushPromises(); + expect(onResult).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1000); + + await expect(resultPromise).resolves.toEqual({ + success: false, + error: expect.stringContaining('It took too long'), + }); + }); +}); diff --git a/packages/cli-platform-android/src/commands/runAndroid/adb.ts b/packages/cli-platform-android/src/commands/runAndroid/adb.ts index 8cd6995de..bab30c7fd 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/adb.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/adb.ts @@ -41,6 +41,27 @@ function getDevices(adbPath: string): Array { } } +/** + * Checks whether Android has finished booting on a connected device. + */ +function isDeviceBooted(adbPath: string, device: string): boolean { + try { + return ( + execFileSync(adbPath, [ + '-s', + device, + 'shell', + 'getprop', + 'sys.boot_completed', + ]) + .toString() + .trim() === '1' + ); + } catch (e) { + return false; + } +} + /** * Gets available CPUs of devices from ADB */ @@ -90,6 +111,7 @@ function getCPU(adbPath: string, device: string): string | null { export default { getDevices, + isDeviceBooted, getAvailableCPUs, getCPU, }; diff --git a/packages/cli-platform-android/src/commands/runAndroid/tryLaunchEmulator.ts b/packages/cli-platform-android/src/commands/runAndroid/tryLaunchEmulator.ts index 2739182a8..30bcb4b88 100644 --- a/packages/cli-platform-android/src/commands/runAndroid/tryLaunchEmulator.ts +++ b/packages/cli-platform-android/src/commands/runAndroid/tryLaunchEmulator.ts @@ -37,15 +37,15 @@ const launchEmulator = async ( }, ); cp.unref(); - const timeout = 30; + const timeout = 120; return new Promise((resolve, reject) => { - const bootCheckInterval = setInterval(async () => { + const bootCheckInterval = setInterval(() => { const devices = adb.getDevices(adbPath); - const connected = port + const connectedDevice = port ? devices.find((d) => d.includes(`${port}`)) - : devices.length > 0; - if (connected) { + : devices[0]; + if (connectedDevice && adb.isDeviceBooted(adbPath, connectedDevice)) { cleanup(); resolve(true); }