Skip to content

Commit

Permalink
Support customizing emulator port (#383)
Browse files Browse the repository at this point in the history
* Add port parameter

* Fix a typo in test description

* Fix avd not being started with correct port

* Fix wrong port being used to kill an emulator if there was an exception
  • Loading branch information
KamilKurde committed Jul 2, 2024
1 parent ca77a10 commit 4b0628e
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 32 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. |
Expand Down
28 changes: 28 additions & 0 deletions __tests__/input-validator.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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 = () => {
Expand Down
2 changes: 2 additions & 0 deletions action-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ inputs:
type: boolean
emulator-boot-timeout:
type: integer
emulator-port:
type: integer
emulator-options:
type: string
disable-animations:
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
29 changes: 17 additions & 12 deletions lib/emulator-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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();
Expand Down
13 changes: 12 additions & 1 deletion lib/input-validator.js
Original file line number Diff line number Diff line change
@@ -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)))
Expand Down Expand Up @@ -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'.`);
Expand Down
14 changes: 10 additions & 4 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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
const 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}`);
Expand Down Expand Up @@ -193,7 +197,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
Expand All @@ -203,18 +207,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)(input_validator_1.MIN_PORT);
core.setFailed(error instanceof Error ? error.message : error);
}
});
Expand Down
29 changes: 17 additions & 12 deletions src/emulator-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function launchEmulator(
avdName: string,
forceAvdCreation: boolean,
emulatorBootTimeout: number,
port: number,
emulatorOptions: string,
disableAnimations: boolean,
disableSpellChecker: boolean,
Expand Down Expand Up @@ -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')) {
Expand All @@ -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::`);
Expand All @@ -99,29 +100,33 @@ export async function launchEmulator(
/**
* Kills the running emulator on the default port.
*/
export async function killEmulator(): Promise<void> {
export async function killEmulator(port: number): Promise<void> {
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 {
console.log(`::endgroup::`);
}
}

async function adb(port: number, command: string): Promise<number> {
return await exec.exec(`adb -s emulator-${port} ${command}`);
}

/**
* Wait for emulator to boot.
*/
async function waitForDevice(emulatorBootTimeout: number): Promise<void> {
async function waitForDevice(port: number, emulatorBootTimeout: number): Promise<void> {
let booted = false;
let attempts = 0;
const retryInterval = 2; // retry every 2 seconds
const maxAttempts = emulatorBootTimeout / 2;
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();
Expand Down
11 changes: 11 additions & 0 deletions src/input-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export const MIN_API_LEVEL = 15;
export const VALID_TARGETS: Array<string> = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv'];
export const VALID_ARCHS: Array<string> = ['x86', 'x86_64', 'arm64-v8a'];
export const VALID_CHANNELS: Array<string> = ['stable', 'beta', 'dev', 'canary'];
export const MIN_PORT = 5554;
export const MAX_PORT = 5584;
export const PREVIEW_API_LEVELS: Array<string> = ['Tiramisu', 'UpsideDownCake', 'VanillaIceCream'];

export function checkApiLevel(apiLevel: string): void {
Expand Down Expand Up @@ -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'.`);
Expand Down
17 changes: 14 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -210,6 +218,7 @@ async function run() {
avdName,
forceAvdCreation,
emulatorBootTimeout,
port,
emulatorOptions,
disableAnimations,
disableSpellchecker,
Expand All @@ -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));
}
}
Expand Down

0 comments on commit 4b0628e

Please sign in to comment.