diff --git a/CHANGELOG.md b/CHANGELOG.md index 27cd23286..15037d6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## v2.32.0 + +* Add `port` parameter for customizing the emulator port to use. - [#383](https://github.com/ReactiveCircus/android-emulator-runner/pull/383) + + ## v2.31.0 * Support setting `VanillaIceCream` as `api-level`. - [#378](https://github.com/ReactiveCircus/android-emulator-runner/pull/378) diff --git a/README.md b/README.md index 15a44b9a5..f245ff388 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ jobs: | `avd-name` | Optional | `test` | Custom AVD name used for creating the Android Virtual Device. | | `force-avd-creation` | Optional | `true` | Whether to force create the AVD by overwriting an existing AVD with the same name as `avd-name` - `true` or `false`. | | `emulator-boot-timeout` | Optional | `600` | Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes. | +| `emulator-port` | Optional | `5554` | Emulator port to use. Allows to run this action on multiple workers on a single machine at the same time. This input is available for the script as `EMULATOR_PORT` enviromental variable. This port is automatically used by android device related tasks in gradle | | `emulator-options` | Optional | See below | Command-line options used when launching the emulator (replacing all default options) - e.g. `-no-window -no-snapshot -camera-back emulated`. | | `disable-animations` | Optional | `true` | Whether to disable animations - `true` or `false`. | | `disable-spellchecker` | Optional | `false` | Whether to disable spellchecker - `true` or `false`. | diff --git a/__tests__/input-validator.test.ts b/__tests__/input-validator.test.ts index 59201f414..b08d5f5d3 100644 --- a/__tests__/input-validator.test.ts +++ b/__tests__/input-validator.test.ts @@ -1,4 +1,5 @@ import * as validator from '../src/input-validator'; +import { MAX_PORT, MIN_PORT } from '../src/input-validator'; describe('api-level validator tests', () => { it('Throws if api-level is not a number', () => { @@ -172,6 +173,33 @@ describe('force-avd-creation validator tests', () => { }); }); +describe('emulator-port validator tests', () => { + it('Validates if emulator-port is even and in range', () => { + const func = () => { + validator.checkPort(5554); + }; + expect(func).not.toThrow(); + }); + it('Throws if emulator-port is lower than MIN_PORT', () => { + const func = () => { + validator.checkPort(MIN_PORT - 2); + }; + expect(func).toThrow(); + }); + it('Throws if emulator-port is higher than MAX_PORT', () => { + const func = () => { + validator.checkPort(MAX_PORT + 2); + }; + expect(func).toThrow(); + }); + it('Throws if emulator-port is odd', () => { + const func = () => { + validator.checkPort(5555); + }; + expect(func).toThrow(); + }); +}); + describe('disable-animations validator tests', () => { it('Throws if disable-animations is not a boolean', () => { const func = () => { diff --git a/action-types.yml b/action-types.yml index 49cbb7368..b77d4b11d 100644 --- a/action-types.yml +++ b/action-types.yml @@ -37,6 +37,8 @@ inputs: type: boolean emulator-boot-timeout: type: integer + emulator-port: + type: integer emulator-options: type: string disable-animations: diff --git a/action.yml b/action.yml index fe609fd37..fa60d74dd 100644 --- a/action.yml +++ b/action.yml @@ -36,6 +36,9 @@ inputs: 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' + emulator-port: + 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' diff --git a/lib/emulator-manager.js b/lib/emulator-manager.js index 5225a189b..a74991e14 100644 --- a/lib/emulator-manager.js +++ b/lib/emulator-manager.js @@ -38,7 +38,7 @@ const fs = __importStar(require("fs")); /** * 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, 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) { return __awaiter(this, void 0, void 0, function* () { try { console.log(`::group::Launch Emulator`); @@ -72,7 +72,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 -avd "${avdName}" ${emulatorOptions} &"`, [], { + yield 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')) { @@ -82,19 +82,19 @@ function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSiz }, }); // wait for emulator to complete booting - yield waitForDevice(emulatorBootTimeout); - yield exec.exec(`adb shell input keyevent 82`); + yield waitForDevice(port, emulatorBootTimeout); + yield adb(port, `shell input keyevent 82`); if (disableAnimations) { console.log('Disabling animations.'); - yield exec.exec(`adb shell settings put global window_animation_scale 0.0`); - yield exec.exec(`adb shell settings put global transition_animation_scale 0.0`); - yield exec.exec(`adb shell settings put global animator_duration_scale 0.0`); + yield adb(port, `shell settings put global window_animation_scale 0.0`); + yield adb(port, `shell settings put global transition_animation_scale 0.0`); + yield adb(port, `shell settings put global animator_duration_scale 0.0`); } if (disableSpellChecker) { - yield exec.exec(`adb shell settings put secure spell_checker_enabled 0`); + yield adb(port, `shell settings put secure spell_checker_enabled 0`); } if (enableHardwareKeyboard) { - yield exec.exec(`adb shell settings put secure show_ime_with_hard_keyboard 0`); + yield adb(port, `shell settings put secure show_ime_with_hard_keyboard 0`); } } finally { @@ -106,11 +106,11 @@ exports.launchEmulator = launchEmulator; /** * Kills the running emulator on the default port. */ -function killEmulator() { +function killEmulator(port) { return __awaiter(this, void 0, void 0, function* () { try { console.log(`::group::Terminate Emulator`); - yield exec.exec(`adb -s emulator-5554 emu kill`); + yield adb(port, `emu kill`); } catch (error) { console.log(error instanceof Error ? error.message : error); @@ -121,10 +121,15 @@ function killEmulator() { }); } exports.killEmulator = killEmulator; +function adb(port, command) { + return __awaiter(this, void 0, void 0, function* () { + return yield exec.exec(`adb -s emulator-${port} ${command}`); + }); +} /** * Wait for emulator to boot. */ -function waitForDevice(emulatorBootTimeout) { +function waitForDevice(port, emulatorBootTimeout) { return __awaiter(this, void 0, void 0, function* () { let booted = false; let attempts = 0; @@ -133,7 +138,7 @@ function waitForDevice(emulatorBootTimeout) { while (!booted) { try { let result = ''; - yield exec.exec(`adb shell getprop sys.boot_completed`, [], { + yield exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], { listeners: { stdout: (data) => { result += data.toString(); diff --git a/lib/input-validator.js b/lib/input-validator.js index 9a26a4070..c27593be7 100644 --- a/lib/input-validator.js +++ b/lib/input-validator.js @@ -1,10 +1,12 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.checkTarget = exports.checkApiLevel = exports.PREVIEW_API_LEVELS = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0; +exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.checkTarget = exports.checkApiLevel = exports.PREVIEW_API_LEVELS = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0; exports.MIN_API_LEVEL = 15; exports.VALID_TARGETS = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv']; exports.VALID_ARCHS = ['x86', 'x86_64', 'arm64-v8a']; exports.VALID_CHANNELS = ['stable', 'beta', 'dev', 'canary']; +exports.MIN_PORT = 5554; +exports.MAX_PORT = 5584; exports.PREVIEW_API_LEVELS = ['Tiramisu', 'UpsideDownCake', 'VanillaIceCream']; function checkApiLevel(apiLevel) { if (exports.PREVIEW_API_LEVELS.some((previewLevel) => apiLevel.startsWith(previewLevel))) @@ -41,6 +43,15 @@ function checkForceAvdCreation(forceAvdCreation) { } } exports.checkForceAvdCreation = checkForceAvdCreation; +function checkPort(port) { + if (port < exports.MIN_PORT || port > exports.MAX_PORT) { + throw new Error(`Emulator port is outside of the supported port range [${exports.MIN_PORT}, ${exports.MAX_PORT}], was ${port}`); + } + if (port % 2 == 1) { + throw new Error(`Emulator port has to be even, was ${port}`); + } +} +exports.checkPort = checkPort; function checkDisableAnimations(disableAnimations) { if (!isValidBoolean(disableAnimations)) { throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`); diff --git a/lib/main.js b/lib/main.js index 1bbc7d012..21a5d4918 100644 --- a/lib/main.js +++ b/lib/main.js @@ -42,6 +42,7 @@ const channel_id_mapper_1 = require("./channel-id-mapper"); const fs_1 = require("fs"); function run() { return __awaiter(this, void 0, void 0, function* () { + let port = input_validator_1.MIN_PORT; try { console.log(`::group::Configure emulator`); let linuxSupportKVM = false; @@ -102,6 +103,10 @@ function run() { // Emulator boot timeout seconds const emulatorBootTimeout = parseInt(core.getInput('emulator-boot-timeout'), 10); console.log(`Emulator boot timeout: ${emulatorBootTimeout}`); + // Emulator port to use + port = parseInt(core.getInput('emulator-port'), 10); + (0, input_validator_1.checkPort)(port); + console.log(`emulator port: ${port}`); // emulator options const emulatorOptions = core.getInput('emulator-options').trim(); console.log(`emulator options: ${emulatorOptions}`); @@ -193,7 +198,7 @@ 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, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard); + yield (0, emulator_manager_1.launchEmulator)(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard); // execute the custom script try { // move to custom working directory if set @@ -203,18 +208,20 @@ function run() { for (const script of scripts) { // use array form to avoid various quote escaping problems // caused by exec(`sh -c "${script}"`) - yield exec.exec('sh', ['-c', script]); + yield exec.exec('sh', ['-c', script], { + env: Object.assign(Object.assign({}, process.env), { EMULATOR_PORT: `${port}`, ANDROID_SERIAL: `emulator-${port}` }), + }); } } catch (error) { core.setFailed(error instanceof Error ? error.message : error); } // finally kill the emulator - yield (0, emulator_manager_1.killEmulator)(); + yield (0, emulator_manager_1.killEmulator)(port); } catch (error) { // kill the emulator so the action can exit - yield (0, emulator_manager_1.killEmulator)(); + yield (0, emulator_manager_1.killEmulator)(port); core.setFailed(error instanceof Error ? error.message : error); } }); diff --git a/src/emulator-manager.ts b/src/emulator-manager.ts index d1fb364f8..aa2a67b6d 100644 --- a/src/emulator-manager.ts +++ b/src/emulator-manager.ts @@ -17,6 +17,7 @@ export async function launchEmulator( avdName: string, forceAvdCreation: boolean, emulatorBootTimeout: number, + port: number, emulatorOptions: string, disableAnimations: boolean, disableSpellChecker: boolean, @@ -65,7 +66,7 @@ export async function launchEmulator( // start emulator console.log('Starting emulator.'); - await exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -avd "${avdName}" ${emulatorOptions} &"`, [], { + 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')) { @@ -76,20 +77,20 @@ export async function launchEmulator( }); // wait for emulator to complete booting - await waitForDevice(emulatorBootTimeout); - await exec.exec(`adb shell input keyevent 82`); + await waitForDevice(port, emulatorBootTimeout); + await adb(port, `shell input keyevent 82`); if (disableAnimations) { console.log('Disabling animations.'); - await exec.exec(`adb shell settings put global window_animation_scale 0.0`); - await exec.exec(`adb shell settings put global transition_animation_scale 0.0`); - await exec.exec(`adb shell settings put global animator_duration_scale 0.0`); + await adb(port, `shell settings put global window_animation_scale 0.0`); + await adb(port, `shell settings put global transition_animation_scale 0.0`); + await adb(port, `shell settings put global animator_duration_scale 0.0`); } if (disableSpellChecker) { - await exec.exec(`adb shell settings put secure spell_checker_enabled 0`); + await adb(port, `shell settings put secure spell_checker_enabled 0`); } if (enableHardwareKeyboard) { - await exec.exec(`adb shell settings put secure show_ime_with_hard_keyboard 0`); + await adb(port, `shell settings put secure show_ime_with_hard_keyboard 0`); } } finally { console.log(`::endgroup::`); @@ -99,10 +100,10 @@ export async function launchEmulator( /** * Kills the running emulator on the default port. */ -export async function killEmulator(): Promise { +export async function killEmulator(port: number): Promise { try { console.log(`::group::Terminate Emulator`); - await exec.exec(`adb -s emulator-5554 emu kill`); + await adb(port, `emu kill`); } catch (error) { console.log(error instanceof Error ? error.message : error); } finally { @@ -110,10 +111,14 @@ export async function killEmulator(): Promise { } } +async function adb(port: number, command: string): Promise { + return await exec.exec(`adb -s emulator-${port} ${command}`); +} + /** * Wait for emulator to boot. */ -async function waitForDevice(emulatorBootTimeout: number): Promise { +async function waitForDevice(port: number, emulatorBootTimeout: number): Promise { let booted = false; let attempts = 0; const retryInterval = 2; // retry every 2 seconds @@ -121,7 +126,7 @@ async function waitForDevice(emulatorBootTimeout: number): Promise { while (!booted) { try { let result = ''; - await exec.exec(`adb shell getprop sys.boot_completed`, [], { + await exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], { listeners: { stdout: (data: Buffer) => { result += data.toString(); diff --git a/src/input-validator.ts b/src/input-validator.ts index 1a3bb217d..8804685ea 100644 --- a/src/input-validator.ts +++ b/src/input-validator.ts @@ -2,6 +2,8 @@ export const MIN_API_LEVEL = 15; export const VALID_TARGETS: Array = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv']; export const VALID_ARCHS: Array = ['x86', 'x86_64', 'arm64-v8a']; export const VALID_CHANNELS: Array = ['stable', 'beta', 'dev', 'canary']; +export const MIN_PORT = 5554; +export const MAX_PORT = 5584; export const PREVIEW_API_LEVELS: Array = ['Tiramisu', 'UpsideDownCake', 'VanillaIceCream']; export function checkApiLevel(apiLevel: string): void { @@ -38,6 +40,15 @@ export function checkForceAvdCreation(forceAvdCreation: string): void { } } +export function checkPort(port: number): void { + if (port < MIN_PORT || port > MAX_PORT) { + throw new Error(`Emulator port is outside of the supported port range [${MIN_PORT}, ${MAX_PORT}], was ${port}`); + } + if (port % 2 == 1) { + throw new Error(`Emulator port has to be even, was ${port}`); + } +} + export function checkDisableAnimations(disableAnimations: string): void { if (!isValidBoolean(disableAnimations)) { throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`); diff --git a/src/main.ts b/src/main.ts index 0f21a2031..a9588245e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,8 @@ import { checkChannel, checkEnableHardwareKeyboard, checkDiskSize, + checkPort, + MIN_PORT, } from './input-validator'; import { launchEmulator, killEmulator } from './emulator-manager'; import * as exec from '@actions/exec'; @@ -20,6 +22,7 @@ import { getChannelId } from './channel-id-mapper'; import { accessSync, constants } from 'fs'; async function run() { + let port: number = MIN_PORT; try { console.log(`::group::Configure emulator`); let linuxSupportKVM = false; @@ -93,6 +96,11 @@ async function run() { const emulatorBootTimeout = parseInt(core.getInput('emulator-boot-timeout'), 10); console.log(`Emulator boot timeout: ${emulatorBootTimeout}`); + // Emulator port to use + port = parseInt(core.getInput('emulator-port'), 10); + checkPort(port); + console.log(`emulator port: ${port}`); + // emulator options const emulatorOptions = core.getInput('emulator-options').trim(); console.log(`emulator options: ${emulatorOptions}`); @@ -210,6 +218,7 @@ async function run() { avdName, forceAvdCreation, emulatorBootTimeout, + port, emulatorOptions, disableAnimations, disableSpellchecker, @@ -226,17 +235,19 @@ async function run() { for (const script of scripts) { // use array form to avoid various quote escaping problems // caused by exec(`sh -c "${script}"`) - await exec.exec('sh', ['-c', script]); + await exec.exec('sh', ['-c', script], { + env: { ...process.env, EMULATOR_PORT: `${port}`, ANDROID_SERIAL: `emulator-${port}` }, + }); } } catch (error) { core.setFailed(error instanceof Error ? error.message : (error as string)); } // finally kill the emulator - await killEmulator(); + await killEmulator(port); } catch (error) { // kill the emulator so the action can exit - await killEmulator(); + await killEmulator(port); core.setFailed(error instanceof Error ? error.message : (error as string)); } }