diff --git a/README.md b/README.md index 980879213..18f5cafb5 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ jobs: strategy: matrix: api-level: [21, 23, 29] + target: [default, google_apis] + arch: [x86_64] steps: - name: checkout uses: actions/checkout@v4 @@ -153,13 +155,15 @@ jobs: path: | ~/.android/avd/* ~/.android/adb* - key: avd-${{ matrix.api-level }} + key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }} - name: create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: false @@ -169,6 +173,8 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true diff --git a/action-types.yml b/action-types.yml index b77d4b11d..ebefb399f 100644 --- a/action-types.yml +++ b/action-types.yml @@ -16,55 +16,57 @@ inputs: arch: type: enum allowed-values: - - x86 - - x86_64 - - arm64-v8a + - x86 + - x86_64 + - arm64-v8a profile: - type: string + type: string cores: - type: integer + type: integer ram-size: - type: string + type: string heap-size: - type: string + type: string sdcard-path-or-size: - type: string + type: string disk-size: - type: string + type: string avd-name: - type: string + type: string force-avd-creation: - type: boolean + type: boolean emulator-boot-timeout: - type: integer + type: integer emulator-port: type: integer emulator-options: - type: string + type: string disable-animations: - type: boolean + type: boolean disable-spellchecker: - type: boolean + type: boolean disable-linux-hw-accel: - type: string + type: string enable-hw-keyboard: - type: boolean + type: boolean emulator-build: - type: string + type: string working-directory: - type: string + type: string ndk: - type: string + type: string cmake: - type: string + type: string channel: - type: enum - allowed-values: - - stable - - beta - - dev - - canary + type: enum + allowed-values: + - stable + - beta + - dev + - canary script: - type: string + type: string pre-emulator-launch-script: - type: string + type: string + retry-count: + type: integer diff --git a/action.yml b/action.yml index fa60d74dd..4a2749d33 100644 --- a/action.yml +++ b/action.yml @@ -1,75 +1,78 @@ -name: 'Android Emulator Runner' -description: 'Installs, configures and starts an Android Emulator directly on hardware-accelerated runners.' -author: 'Reactive Circus' +name: "Android Emulator Runner" +description: "Installs, configures and starts an Android Emulator directly on hardware-accelerated runners." +author: "Reactive Circus" branding: - icon: 'smartphone' - color: 'green' + icon: "smartphone" + color: "green" inputs: api-level: - description: 'API level of the platform and system image - e.g. 23 for Android Marshmallow, 29 for Android 10' + description: "API level of the platform and system image - e.g. 23 for Android Marshmallow, 29 for Android 10" required: true target: - description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv' - default: 'default' + description: "target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv" + default: "default" arch: - description: 'CPU architecture of the system image - x86, x86_64 or arm64-v8a' - default: 'x86' + description: "CPU architecture of the system image - x86, x86_64 or arm64-v8a" + default: "x86" profile: - description: 'hardware profile used for creating the AVD - e.g. `Nexus 6`' + description: "hardware profile used for creating the AVD - e.g. `Nexus 6`" cores: - description: 'the number of cores to use for the emulator' - default: 2 + description: "the number of cores to use for the emulator" + default: "2" ram-size: - description: 'size of RAM to use for this AVD, in KB or MB, denoted with K or M. - e.g. `2048M`' + description: "size of RAM to use for this AVD, in KB or MB, denoted with K or M. - e.g. `2048M`" heap-size: - description: 'size of heap to use for this AVD in MB. - e.g. `512M`' + description: "size of heap to use for this AVD in MB. - e.g. `512M`" sdcard-path-or-size: - description: 'path to the SD card image for this AVD or the size of a new SD card image to create for this AVD, in KB or MB, denoted with K or M. - e.g. `path/to/sdcard`, or `1000M`' + description: "path to the SD card image for this AVD or the size of a new SD card image to create for this AVD, in KB or MB, denoted with K or M. - e.g. `path/to/sdcard`, or `1000M`" disk-size: - description: 'disk size to use for this AVD. Either in bytes or KB, MB or GB, when denoted with K, M or G' + description: "disk size to use for this AVD. Either in bytes or KB, MB or GB, when denoted with K, M or G" avd-name: - description: 'custom AVD name used for creating the Android Virtual Device' - default: 'test' + description: "custom AVD name used for creating the Android Virtual Device" + default: "test" force-avd-creation: - description: 'whether to force create the AVD by overwriting an existing AVD with the same name as `avd-name` - `true` or `false`' - default: 'true' + description: "whether to force create the AVD by overwriting an existing AVD with the same name as `avd-name` - `true` or `false`" + default: "true" emulator-boot-timeout: - description: 'Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes' - default: '600' + description: "Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes" + default: "600" emulator-port: - description: 'Port to run emulator on, allows to run multiple emulators on the same physical machine' - default: '5554' + description: "Port to run emulator on, allows to run multiple emulators on the same physical machine" + default: "5554" emulator-options: - description: 'command-line options used when launching the emulator - e.g. `-no-window -no-snapshot -camera-back emulated`' - default: '-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim' + description: "command-line options used when launching the emulator - e.g. `-no-window -no-snapshot -camera-back emulated`" + default: "-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim" disable-animations: - description: 'whether to disable animations - true or false' - default: 'true' + description: "whether to disable animations - true or false" + default: "true" disable-spellchecker: - description: 'whether to disable the Android spell checker framework, a common source of flakiness in text fields - `true` or `false`' - default: 'false' + description: "whether to disable the Android spell checker framework, a common source of flakiness in text fields - `true` or `false`" + default: "false" disable-linux-hw-accel: - description: 'whether to disable hardware acceleration on Linux machines - `true` or `false` or `auto`' - default: 'auto' + description: "whether to disable hardware acceleration on Linux machines - `true` or `false` or `auto`" + default: "auto" enable-hw-keyboard: - description: 'whether to enable hardware keyboard - `true` or `false`.' - default: 'false' + description: "whether to enable hardware keyboard - `true` or `false`." + default: "false" emulator-build: - description: 'build number of a specific version of the emulator binary to use - e.g. `6061023` for emulator v29.3.0.0' + description: "build number of a specific version of the emulator binary to use - e.g. `6061023` for emulator v29.3.0.0" working-directory: - description: 'A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository' + description: "A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository" ndk: - description: 'version of NDK to install - e.g. 21.0.6113669' + description: "version of NDK to install - e.g. 21.0.6113669" cmake: - description: 'version of CMake to install - e.g. 3.10.2.4988404' + description: "version of CMake to install - e.g. 3.10.2.4988404" channel: - description: 'Channel to download the SDK components from - `stable`, `beta`, `dev`, `canary`' - default: 'stable' + description: "Channel to download the SDK components from - `stable`, `beta`, `dev`, `canary`" + default: "stable" script: - description: 'custom script to run - e.g. `./gradlew connectedCheck`' - required: true + description: "custom script to run - e.g. `./gradlew connectedCheck`" pre-emulator-launch-script: - description: 'custom script to run after creating the AVD and before launching the emulator - e.g. `./adjust-emulator-configs.sh`' + description: "custom script to run after creating the AVD and before launching the emulator - e.g. `./adjust-emulator-configs.sh`" + retry-count: + description: "number of times to retry the action in case of failure - e.g. `3`" + default: "3" runs: - using: 'node20' - main: 'lib/main.js' + using: "node20" + main: "lib/main.js" + post: "lib/post.js" diff --git a/lib/emulator-manager.js b/lib/emulator-manager.js index a74991e14..0874f568c 100644 --- a/lib/emulator-manager.js +++ b/lib/emulator-manager.js @@ -35,10 +35,11 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.killEmulator = exports.launchEmulator = void 0; const exec = __importStar(require("@actions/exec")); const fs = __importStar(require("fs")); +const retry_1 = require("./retry"); /** * Creates and launches a new AVD instance with the specified configurations. */ -function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard) { +function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard, retryCount) { return __awaiter(this, void 0, void 0, function* () { try { console.log(`::group::Launch Emulator`); @@ -48,7 +49,11 @@ function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSiz const profileOption = profile.trim() !== '' ? `--device '${profile}'` : ''; const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : ''; console.log(`Creating AVD.`); - yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`); + // Don't believe this ever failed, but it seems like a strong candidate for failure... + const result = yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`), retryCount); + if (result !== 0) { + throw new Error('Failed to create AVD.'); + } } if (cores) { yield exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`); @@ -72,7 +77,7 @@ function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSiz } // start emulator console.log('Starting emulator.'); - yield exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], { + const result = yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], { listeners: { stderr: (data) => { if (data.toString().includes('invalid command-line parameter')) { @@ -80,7 +85,10 @@ function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSiz } }, }, - }); + }), retryCount); + if (result !== 0) { + throw new Error('Failed to create AVD.'); + } // wait for emulator to complete booting yield waitForDevice(port, emulatorBootTimeout); yield adb(port, `shell input keyevent 82`); diff --git a/lib/main.js b/lib/main.js index 21a5d4918..f292a0a51 100644 --- a/lib/main.js +++ b/lib/main.js @@ -178,8 +178,9 @@ function run() { console.log(`${script}`); })); console.log(`::endgroup::`); + const retryCount = parseInt(core.getInput('retry-count', { required: true })); // install SDK - yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion); + yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion, retryCount); // execute pre emulator launch script if set if (preEmulatorLaunchScripts !== undefined) { console.log(`::group::Run pre emulator launch script`); @@ -198,7 +199,16 @@ function run() { console.log(`::endgroup::`); } // launch an emulator - yield (0, emulator_manager_1.launchEmulator)(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard); + try { + yield (0, emulator_manager_1.launchEmulator)(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard, retryCount); + } + catch (error) { + core.setFailed(error instanceof Error ? error.message : error); + } + if (scripts.length === 0) { + console.log('No custom script to run. Be sure to shut down the emulator in your script.'); + console.log(`(adb -s emulator-${port} emu kill)`); + } // execute the custom script try { // move to custom working directory if set diff --git a/lib/post.js b/lib/post.js new file mode 100644 index 000000000..a5d7e81d0 --- /dev/null +++ b/lib/post.js @@ -0,0 +1,66 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const input_validator_1 = require("./input-validator"); +const emulator_manager_1 = require("./emulator-manager"); +const exec = __importStar(require("@actions/exec")); +function post() { + return __awaiter(this, void 0, void 0, function* () { + let port = input_validator_1.MIN_PORT; + // Emulator port to use + port = parseInt(core.getInput('emulator-port'), 10); + (0, input_validator_1.checkPort)(port); + console.log(`emulator port: ${port}`); + try { + let result = ''; + yield exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], { + listeners: { + stdout: (data) => { + result += data.toString(); + }, + }, + }); + if (result.trim() === '1') { + console.log('Emulator online, killing it.'); + yield (0, emulator_manager_1.killEmulator)(port); + } + } + catch (error) { + yield (0, emulator_manager_1.killEmulator)(port); + console.warn(error instanceof Error ? error.message : error); + } + }); +} +post(); diff --git a/lib/retry.js b/lib/retry.js new file mode 100644 index 000000000..088f1eb0c --- /dev/null +++ b/lib/retry.js @@ -0,0 +1,28 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.execWithRetry = void 0; +function execWithRetry(fn, retryCount) { + return __awaiter(this, void 0, void 0, function* () { + let attempt = 0; + let result = 1; + while (attempt <= retryCount && result !== 0) { + if (attempt > 0) { + console.log(`Retry attempt ${attempt}. (exit code was ${result})`); + yield new Promise((resolve) => setTimeout(resolve, 1000)); + } + result = yield fn(); + attempt++; + } + return result; + }); +} +exports.execWithRetry = execWithRetry; diff --git a/lib/script-parser.js b/lib/script-parser.js index 16b216489..96199fc88 100644 --- a/lib/script-parser.js +++ b/lib/script-parser.js @@ -12,9 +12,6 @@ function parseScript(rawScript) { .filter((value) => { return !value.startsWith('#') && value.length > 0; }); - if (scripts.length == 0) { - throw new Error(`No valid script found.`); - } return scripts; } exports.parseScript = parseScript; diff --git a/lib/sdk-installer.js b/lib/sdk-installer.js index fe7f51385..af35fefce 100644 --- a/lib/sdk-installer.js +++ b/lib/sdk-installer.js @@ -38,6 +38,7 @@ const exec = __importStar(require("@actions/exec")); const io = __importStar(require("@actions/io")); const tc = __importStar(require("@actions/tool-cache")); const fs = __importStar(require("fs")); +const retry_1 = require("./retry"); const BUILD_TOOLS_VERSION = '35.0.0'; // SDK command-line tools 16.0 const CMDLINE_TOOLS_URL_MAC = 'https://dl.google.com/android/repository/commandlinetools-mac-12266719_latest.zip'; @@ -46,8 +47,9 @@ const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/comman * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator, * and the system image for the chosen API level, CPU arch, and target. */ -function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) { +function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion, retryCount) { return __awaiter(this, void 0, void 0, function* () { + retryCount = retryCount || 0; try { console.log(`::group::Install Android SDK`); const isOnMac = process.platform === 'darwin'; @@ -68,9 +70,9 @@ function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndk // accept all Android SDK licenses yield exec.exec(`sh -c \\"yes | sdkmanager --licenses > /dev/null"`); console.log('Installing latest build tools, platform tools, and platform.'); - yield exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`); + yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`), retryCount); console.log('Installing latest emulator.'); - yield exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`); + yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`), retryCount); if (emulatorBuild) { console.log(`Installing emulator build ${emulatorBuild}.`); // TODO find out the correct download URLs for all build ids @@ -90,19 +92,19 @@ function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndk else { downloadUrlSuffix = `-${emulatorBuild}`; } - yield exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`); + yield (0, retry_1.execWithRetry)(() => exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`), retryCount); yield exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`); yield io.rmRF('emulator.zip'); } console.log('Installing system images.'); - yield exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`); + yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`), retryCount); if (ndkVersion) { console.log(`Installing NDK ${ndkVersion}.`); - yield exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`); + yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`), retryCount); } if (cmakeVersion) { console.log(`Installing CMake ${cmakeVersion}.`); - yield exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`); + yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`), retryCount); } } finally { diff --git a/src/emulator-manager.ts b/src/emulator-manager.ts index aa2a67b6d..893d47298 100644 --- a/src/emulator-manager.ts +++ b/src/emulator-manager.ts @@ -1,5 +1,6 @@ import * as exec from '@actions/exec'; import * as fs from 'fs'; +import { execWithRetry } from './retry'; /** * Creates and launches a new AVD instance with the specified configurations. @@ -22,7 +23,8 @@ export async function launchEmulator( disableAnimations: boolean, disableSpellChecker: boolean, disableLinuxHardwareAcceleration: boolean, - enableHardwareKeyboard: boolean + enableHardwareKeyboard: boolean, + retryCount: number ): Promise { try { console.log(`::group::Launch Emulator`); @@ -32,9 +34,13 @@ export async function launchEmulator( const profileOption = profile.trim() !== '' ? `--device '${profile}'` : ''; const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : ''; console.log(`Creating AVD.`); - await exec.exec( - `sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"` - ); + // Don't believe this ever failed, but it seems like a strong candidate for failure... + const result = await execWithRetry( + () => exec.exec( + `sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`), retryCount); + if (result !== 0) { + throw new Error('Failed to create AVD.'); + } } if (cores) { @@ -66,15 +72,20 @@ export async function launchEmulator( // start emulator console.log('Starting emulator.'); - await exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], { - listeners: { - stderr: (data: Buffer) => { - if (data.toString().includes('invalid command-line parameter')) { - throw new Error(data.toString()); - } - }, - }, - }); + const result = await execWithRetry( + () => + exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], { + listeners: { + stderr: (data: Buffer) => { + if (data.toString().includes('invalid command-line parameter')) { + throw new Error(data.toString()); + } + }, + }, + }), retryCount); + if (result !== 0) { + throw new Error('Failed to create AVD.'); + } // wait for emulator to complete booting await waitForDevice(port, emulatorBootTimeout); @@ -97,6 +108,8 @@ export async function launchEmulator( } } + + /** * Kills the running emulator on the default port. */ diff --git a/src/main.ts b/src/main.ts index a9588245e..31f8c2d29 100644 --- a/src/main.ts +++ b/src/main.ts @@ -184,8 +184,10 @@ async function run() { }); console.log(`::endgroup::`); + const retryCount: number = parseInt(core.getInput('retry-count', { required: true })); + // install SDK - await installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion); + await installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion, retryCount); // execute pre emulator launch script if set if (preEmulatorLaunchScripts !== undefined) { @@ -205,26 +207,36 @@ async function run() { } // launch an emulator - await launchEmulator( - apiLevel, - target, - arch, - profile, - cores, - ramSize, - heapSize, - sdcardPathOrSize, - diskSize, - avdName, - forceAvdCreation, - emulatorBootTimeout, - port, - emulatorOptions, - disableAnimations, - disableSpellchecker, - disableLinuxHardwareAcceleration, - enableHardwareKeyboard - ); + try { + await launchEmulator( + apiLevel, + target, + arch, + profile, + cores, + ramSize, + heapSize, + sdcardPathOrSize, + diskSize, + avdName, + forceAvdCreation, + emulatorBootTimeout, + port, + emulatorOptions, + disableAnimations, + disableSpellchecker, + disableLinuxHardwareAcceleration, + enableHardwareKeyboard, + retryCount + ); + } catch (error) { + core.setFailed(error instanceof Error ? error.message : (error as string)); + } + + if (scripts.length === 0) { + console.log('No custom script to run. Be sure to shut down the emulator in your script.'); + console.log(`(adb -s emulator-${port} emu kill)`); + } // execute the custom script try { diff --git a/src/post.ts b/src/post.ts new file mode 100644 index 000000000..6cbb73419 --- /dev/null +++ b/src/post.ts @@ -0,0 +1,31 @@ +import * as core from '@actions/core'; +import { checkPort, MIN_PORT } from './input-validator'; +import { killEmulator } from './emulator-manager'; +import * as exec from '@actions/exec'; + +async function post() { + let port: number = MIN_PORT; + // Emulator port to use + port = parseInt(core.getInput('emulator-port'), 10); + checkPort(port); + console.log(`emulator port: ${port}`); + try { + let result = ''; + await exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], { + listeners: { + stdout: (data: Buffer) => { + result += data.toString(); + }, + }, + }); + if (result.trim() === '1') { + console.log('Emulator online, killing it.'); + await killEmulator(port); + } + } catch (error) { + await killEmulator(port); + console.warn(error instanceof Error ? error.message : error); + } +} + +post(); diff --git a/src/retry.ts b/src/retry.ts new file mode 100644 index 000000000..0eb51d682 --- /dev/null +++ b/src/retry.ts @@ -0,0 +1,13 @@ +export async function execWithRetry(fn: () => Promise, retryCount: number): Promise { + let attempt = 0; + let result = 1; + while (attempt <= retryCount && result !== 0) { + if (attempt > 0) { + console.log(`Retry attempt ${attempt}. (exit code was ${result})`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + result = await fn(); + attempt++; + } + return result; +} diff --git a/src/script-parser.ts b/src/script-parser.ts index 813d36bd2..242056dc8 100644 --- a/src/script-parser.ts +++ b/src/script-parser.ts @@ -9,10 +9,5 @@ export function parseScript(rawScript: string): Array { .filter((value: string) => { return !value.startsWith('#') && value.length > 0; }); - - if (scripts.length == 0) { - throw new Error(`No valid script found.`); - } - return scripts; } diff --git a/src/sdk-installer.ts b/src/sdk-installer.ts index f87028a6f..4f8c4afa0 100644 --- a/src/sdk-installer.ts +++ b/src/sdk-installer.ts @@ -3,6 +3,7 @@ import * as exec from '@actions/exec'; import * as io from '@actions/io'; import * as tc from '@actions/tool-cache'; import * as fs from 'fs'; +import { execWithRetry } from './retry'; const BUILD_TOOLS_VERSION = '35.0.0'; // SDK command-line tools 16.0 @@ -13,7 +14,17 @@ const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/comman * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator, * and the system image for the chosen API level, CPU arch, and target. */ -export async function installAndroidSdk(apiLevel: string, target: string, arch: string, channelId: number, emulatorBuild?: string, ndkVersion?: string, cmakeVersion?: string): Promise { +export async function installAndroidSdk( + apiLevel: string, + target: string, + arch: string, + channelId: number, + emulatorBuild?: string, + ndkVersion?: string, + cmakeVersion?: string, + retryCount?: number +): Promise { + retryCount = retryCount || 0; try { console.log(`::group::Install Android SDK`); const isOnMac = process.platform === 'darwin'; @@ -40,10 +51,10 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch: console.log('Installing latest build tools, platform tools, and platform.'); - await exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`); + await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`), retryCount); console.log('Installing latest emulator.'); - await exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`); + await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`), retryCount); if (emulatorBuild) { console.log(`Installing emulator build ${emulatorBuild}.`); @@ -61,20 +72,20 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch: } else { downloadUrlSuffix = `-${emulatorBuild}`; } - await exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`); + await execWithRetry(() => exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`), retryCount); await exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`); await io.rmRF('emulator.zip'); } console.log('Installing system images.'); - await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`); + await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`), retryCount); if (ndkVersion) { console.log(`Installing NDK ${ndkVersion}.`); - await exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`); + await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`), retryCount); } if (cmakeVersion) { console.log(`Installing CMake ${cmakeVersion}.`); - await exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`); + await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`), retryCount); } } finally { console.log(`::endgroup::`);