Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only try to cleanup uiautomator process if instrumentation crashes #280

Merged
merged 1 commit into from
Jan 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// 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']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this command expected?

I failed test cases in ruby_lib_core because of this line.
https://gist.github.com/KazuCocoa/244e7cff066353be5dfc8e916d292c87

The result of /Users/kazuaki/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell am which issued here was 255: command not found. Then, the error caches and it becomes an error. We can see the list of command which is available am option.

Should we ignore the error only the result is command not found?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of command was:

$ /Users/kazuaki/Library/Android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell am
Activity manager (activity) commands:
  help
      Print this help text.
  start-activity [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]
          [--sampling INTERVAL] [--streaming] [-R COUNT] [-S]
          [--track-allocation] [--user <USER_ID> | current] <INTENT>
      Start an Activity.  Options are:
      -D: enable debugging
      -N: enable native debugging
      -W: wait for launch to complete
      --start-profiler <FILE>: start profiler and send results to <FILE>
      --sampling INTERVAL: use sample profiling with INTERVAL microseconds
          between samples (use with --start-profiler)
      --streaming: stream the profiling output to the specified file
          (use with --start-profiler)
      -P <FILE>: like above, but profiling stops when app goes idle
      --attach-agent <agent>: attach the given agent before binding
      -R: repeat the activity launch <COUNT> times.  Prior to each repeat,
          the top activity will be finished.
      -S: force stop the target app before starting the activity
      --track-allocation: enable tracking of object allocations
      --user <USER_ID> | current: Specify which user to run as; if not
          specified then run as the current user.
      --stack <STACK_ID>: Specify into which stack should the activity be put.
...

The exit status was 255: command not found in $? after the command

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"appium-adb": "^7.0.0",
"appium-android-driver": "^4.0.0",
"appium-base-driver": "^3.10.0",
Expand Down