Skip to content

Commit

Permalink
Only perform uiautomation process cleanup if instrumentation crashes (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jan 14, 2019
1 parent a91a341 commit a133e5c
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 111 deletions.
6 changes: 3 additions & 3 deletions lib/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,13 @@ class AndroidUiautomator2Driver extends BaseDriver {
this.caps.deviceName = this.adb.curDeviceId;
this.caps.deviceUDID = this.opts.udid;

// set up the modified UiAutomator2 server etc
await this.initUiAutomator2Server();

// start an avd, set the language/locale, pick an emulator, etc...
// TODO with multiple devices we'll need to parameterize this
this.defaultIME = await helpers.initDevice(this.adb, this.opts);

// set up the modified UiAutomator2 server etc
await this.initUiAutomator2Server();

// Further prepare the device by forwarding the UiAutomator2 port
logger.debug(`Forwarding UiAutomator2 Server port ${DEVICE_PORT} to ${this.opts.systemPort}`);
await this.adb.forwardPort(this.opts.systemPort, DEVICE_PORT);
Expand Down
240 changes: 150 additions & 90 deletions lib/uiautomator2.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import _ from 'lodash';
import { JWProxy } from 'appium-base-driver';
import { retryInterval } from 'asyncbox';
import logger from './logger';
import { waitForCondition } from 'asyncbox';
import log from './logger';
import { SERVER_APK_PATH as apkPath, TEST_APK_PATH as testApkPath, version as serverVersion } from 'appium-uiautomator2-server';
import adbkit from 'adbkit';
import { getRetries } from './utils';
import { util } from 'appium-support';
import { util, logger } from 'appium-support';
import B from 'bluebird';


const REQD_PARAMS = ['adb', 'tmpDir', 'host', 'systemPort', 'devicePort', 'disableWindowAnimation'];
const SERVER_LAUNCH_RETRIES = 20;
const SERVER_LAUNCH_TIMEOUT = 30000;
const SERVER_INSTALL_RETRIES = 20;
const INSTRUMENTATION_TARGET = 'io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner';
const SERVICES_LAUNCH_TIMEOUT = 30000;
const SERVER_PACKAGE_ID = 'io.appium.uiautomator2.server';
const SERVER_TEST_PACKAGE_ID = `${SERVER_PACKAGE_ID}.test`;

const INSTRUMENTATION_TARGET = `${SERVER_TEST_PACKAGE_ID}/androidx.test.runner.AndroidJUnitRunner`;
const instrumentationLogger = logger.getLogger('Instrumentation');

class UiAutomator2Server {
constructor (opts = {}) {
Expand All @@ -26,11 +26,6 @@ class UiAutomator2Server {
}
this.jwproxy = new JWProxy({server: this.host, port: this.systemPort});
this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy);

this.client = adbkit.createClient({
port: this.adb.adbPort,
host: this.host
});
}

/**
Expand Down Expand Up @@ -64,7 +59,7 @@ class UiAutomator2Server {
}

const appState = await this.adb.getApplicationInstallState(appPath, appId);
logger.debug(`${appId} installation state: ${appState}`);
log.debug(`${appId} installation state: ${appState}`);
if (await this.adb.checkApkCert(appPath, appId)) {
shouldUninstallServerPackages = shouldUninstallServerPackages || [
this.adb.APP_INSTALL_STATE.OLDER_VERSION_INSTALLED,
Expand All @@ -80,16 +75,16 @@ class UiAutomator2Server {
this.adb.APP_INSTALL_STATE.NOT_INSTALLED,
].includes(appState);
}
logger.info(`Server packages are ${shouldInstallServerPackages ? '' : 'not '}going to be (re)installed`);
log.info(`Server packages are ${shouldInstallServerPackages ? '' : 'not '}going to be (re)installed`);
if (shouldInstallServerPackages && shouldUninstallServerPackages) {
logger.info('Full packages reinstall is going to be performed');
log.info('Full packages reinstall is going to be performed');
}
for (const {appId, appPath} of packagesInfo) {
if (shouldUninstallServerPackages) {
try {
await this.adb.uninstallApk(appId);
} catch (err) {
logger.warn(`Error uninstalling '${appId}': ${err.message}`);
log.warn(`Error uninstalling '${appId}': ${err.message}`);
}
}
if (shouldInstallServerPackages) {
Expand All @@ -99,116 +94,181 @@ class UiAutomator2Server {
});
}
}
let retries = getRetries('Server install', installTimeout, SERVER_INSTALL_RETRIES);

logger.debug(`Waiting up to ${retries * 1000}ms for instrumentation '${INSTRUMENTATION_TARGET}' to be available`);
let output;
await this.verifyServicesAvailability();
}

async verifyServicesAvailability () {
log.debug(`Waiting up to ${SERVICES_LAUNCH_TIMEOUT}ms for services to be available`);
let isPmServiceAvailable = false;
let isAmServiceAvailable = false;
let pmOutput = '';
let pmError = null;
let amError = null;
try {
await retryInterval(retries, 1000, async () => {
output = await this.adb.shell(['pm', 'list', 'instrumentation']);
if (output.indexOf('Could not access the Package Manager') !== -1) {
let err = new Error(`Problem running package manager: ${output}`);
output = null; // remove output, so it is not printed below
throw err;
} else if (output.indexOf(INSTRUMENTATION_TARGET) === -1) {
throw new Error('No instrumentation process found. Retrying...');
await waitForCondition(async () => {
if (!isPmServiceAvailable) {
pmError = null;
pmOutput = '';
try {
pmOutput = await this.adb.shell(['pm', 'list', 'instrumentation']);
} catch (e) {
pmError = e;
}
if (pmOutput.includes('Could not access the Package Manager')) {
pmError = new Error(`Problem running Package Manager: ${pmOutput}`);
pmOutput = ''; // remove output, so it is not printed below
} else if (pmOutput.includes(INSTRUMENTATION_TARGET)) {
pmOutput = ''; // remove output, so it is not printed below
log.debug(`Instrumentation target '${INSTRUMENTATION_TARGET}' is available`);
isPmServiceAvailable = true;
} else if (!pmError) {
pmError = new Error('The instrumentation target is not listed by Package Manager');
}
}
logger.debug(`Instrumentation '${INSTRUMENTATION_TARGET}' available`);
if (!isAmServiceAvailable) {
// https://travis-ci.org/appium/appium-uiautomator2-driver/jobs/478514097#L4806
amError = null;
try {
await this.adb.shell(['am']);
log.debug('Activity manager service is available');
isAmServiceAvailable = true;
} catch (e) {
amError = e;
}
}
return isPmServiceAvailable && isAmServiceAvailable;
}, {
waitMs: SERVICES_LAUNCH_TIMEOUT,
intervalMs: 1000,
});
} catch (err) {
logger.error(`Unable to find instrumentation target '${INSTRUMENTATION_TARGET}': ${err.message}`);
if (output) {
logger.debug('Available targets:');
for (let line of output.split('\n')) {
logger.debug(` ${line.replace('instrumentation:', '')}`);
if (!isPmServiceAvailable) {
log.error(`Unable to find instrumentation target '${INSTRUMENTATION_TARGET}': ${(pmError || {}).message}`);
if (pmOutput) {
log.debug('Available targets:');
for (const line of pmOutput.split('\n')) {
log.debug(` ${line.replace('instrumentation:', '')}`);
}
}
}
if (!isAmServiceAvailable) {
log.error(`Activity manager service is not available`);
if (amError) {
log.debug(amError.message);
}
}
}
}

async startSession (caps) {
// kill any uiautomator existing processes
await this.killUiAutomatorOnDevice();

await this.cleanupAutomationLeftovers();
if (caps.skipServerInstallation) {
logger.info(`'skipServerInstallation' is set. Attempting to use UIAutomator2 server from the device.`);
log.info(`'skipServerInstallation' is set. Attempting to use UIAutomator2 server from the device`);
} else {
logger.info(`Starting UIAutomator2 server ${serverVersion}`);
logger.info(`Using UIAutomator2 server from '${apkPath}' and test from '${testApkPath}'`);
log.info(`Starting UIAutomator2 server ${serverVersion}`);
log.info(`Using UIAutomator2 server from '${apkPath}' and test from '${testApkPath}'`);
}

// let cmd = ['am', 'instrument', '-w',
// 'io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner'];
// this.adb.shell(cmd);
// using 3rd party module called 'adbKit' for time being as using 'appium-adb'
// facing IllegalStateException: UiAutomation not connected exception
// https://github.com/appium/appium-uiautomator2-driver/issues/19

await this.startSessionUsingAdbKit(caps.deviceUDID);
const initStarted = process.hrtime();
let retries = 0;
const maxRetries = 2;
const delayBetweenRetries = 3000;
while (retries < maxRetries) {
let didProcessExit = false;
await this.startInstrumentationProcess(() => {
didProcessExit = true;
});
const timeout = caps.uiautomator2ServerLaunchTimeout || SERVER_LAUNCH_TIMEOUT;
log.info(`Waiting up to ${timeout}ms for UiAutomator2 to be online...`);
try {
await waitForCondition(async () => {
try {
await this.jwproxy.command('/status', 'GET');
return true;
} catch (err) {
if (didProcessExit) {
// short circuit to retry or fail fast
return true;
}
return false;
}
}, {
waitMs: timeout,
intervalMs: 1000,
});
} catch (err) {
log.errorAndThrow(`The instrumentation process cannot be initialized within ${timeout}ms timeout. `
+ 'Make sure the application under test does not crash and investigate the logcat output. '
+ `You could also try to increase the value of 'uiautomator2ServerLaunchTimeout' capability. `);
}
if (!didProcessExit) {
break;
}

let retries = getRetries('Server launch', caps.uiautomator2ServerLaunchTimeout, SERVER_LAUNCH_RETRIES);
retries++;
if (retries >= maxRetries) {
log.errorAndThrow('The instrumentation process cannot be initialized. '
+ 'Make sure the application under test does not crash and investigate the logcat output.');
}
log.warn(`The instrumentation process has been unexpectedly terminated. `
+ `Retrying UiAutomator2 startup (#${retries} of ${maxRetries - 1})`);
await this.cleanupAutomationLeftovers(true);
await B.delay(delayBetweenRetries);
}

logger.info(`Waiting up to ${retries * 1000}ms for UiAutomator2 to be online...`);
// wait for UiAutomator2 to be online
await retryInterval(retries, 1000, this.jwproxy.command.bind(this.jwproxy), '/status', 'GET');
const [seconds, nanoseconds] = process.hrtime(initStarted);
log.debug(`The initialization of the instrumentation process took `
+ `${Math.ceil(seconds * 1000 + nanoseconds / 1000000)}ms`);
await this.jwproxy.command('/session', 'POST', {desiredCapabilities: caps});
}

async startSessionUsingAdbKit (deviceUDID) { // eslint-disable-line require-await
let cmd = 'am instrument -w';
async startInstrumentationProcess (onExit = null) {
const cmd = ['am', 'instrument', '-w'];
if (this.disableWindowAnimation) {
cmd = `${cmd} --no-window-animation`;
cmd.push('--no-window-animation');
}
cmd = `${cmd} ${INSTRUMENTATION_TARGET}`;
logger.info(`Running command: 'adb -s ${deviceUDID} shell ${cmd}'`);
this.client.shell(deviceUDID, cmd)
.then(adbkit.util.readAll) // eslint-disable-line promise/prefer-await-to-then
.then(function (output) { // eslint-disable-line promise/prefer-await-to-then
for (let line of output.toString().split('\n')) {
if (line.length) {
logger.debug(`[UIAutomator2] ${line}`);
}
}
}).catch(function (err) { // eslint-disable-line promise/prefer-await-to-callbacks
logger.error(`[UIAutomator2 Error] ${err.message}`);
logger.debug(`Full error: ${err.stack}`);
});
cmd.push(INSTRUMENTATION_TARGET);
const instrumentationProcess = this.adb.createSubProcess(['shell', ...cmd]);
instrumentationProcess.on('output', (stdout, stderr) => {
const output = _.trim(stdout || stderr);
if (output) {
instrumentationLogger.debug(output);
}
});
instrumentationProcess.on('exit', (code) => {
instrumentationLogger.debug(`The process has exited with code ${code}`);
if (_.isFunction(onExit)) {
onExit();
}
});
await instrumentationProcess.start(0);
}

async deleteSession () {
logger.debug('Deleting UiAutomator2 server session');
log.debug('Deleting UiAutomator2 server session');
// rely on jwproxy's intelligence to know what we're talking about and
// delete the current session
try {
await this.jwproxy.command('/', 'DELETE');
} catch (err) {
logger.warn(`Did not get confirmation UiAutomator2 deleteSession worked; ` +
log.warn(`Did not get confirmation UiAutomator2 deleteSession worked; ` +
`Error was: ${err}`);
}
}

async killUiAutomatorOnDevice () {
async cleanupAutomationLeftovers (strictCleanup = false) {
log.debug(`Performing ${strictCleanup ? 'strict' : 'shallow'} cleanup of automation leftovers`);
try {
const pids = (await this.adb.getPIDsByName('uiautomator')).map((p) => `${p}`);
if (!_.isEmpty(pids)) {
const isRoot = await this.adb.root();
try {
await this.adb.shell(['kill', '-9', ...pids]);
} finally {
if (isRoot) {
await this.adb.unroot();
}
}
}
} catch (err) {
logger.warn(`Unable to stop uiautomator process: ${err.message}`);
await this.adb.forceStop(SERVER_TEST_PACKAGE_ID);
} catch (ignore) {}
if (!strictCleanup) {
return;
}

// https://github.com/appium/appium/issues/10749
try {
await this.adb.forceStop('io.appium.uiautomator2.server');
} catch (ignore) {
logger.info('Unable to kill the io.appium.uiautomator2.server process, assuming it is already killed');
}
await this.adb.killProcessesByName('uiautomator');
} catch (ignore) {}
}
}

Expand Down
17 changes: 0 additions & 17 deletions lib/utils.js

This file was deleted.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
],
"dependencies": {
"@babel/runtime": "^7.0.0",
"adbkit": "^2.4.1",
"appium-adb": "^7.0.0",
"appium-android-driver": "^4.0.0",
"appium-base-driver": "^3.10.0",
Expand Down

0 comments on commit a133e5c

Please sign in to comment.