-
-
Notifications
You must be signed in to change notification settings - Fork 187
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = {}) { | ||
|
@@ -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 | ||
}); | ||
} | ||
|
||
/** | ||
|
@@ -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, | ||
|
@@ -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) { | ||
|
@@ -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']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. The result of Should we ignore the error only the result is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The result of command was:
The exit status was |
||
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) {} | ||
} | ||
} | ||
|
||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,7 +33,6 @@ | |
], | ||
"dependencies": { | ||
"@babel/runtime": "^7.0.0", | ||
"adbkit": "^2.4.1", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍