Skip to content

Commit d3f50ef

Browse files
fix: Explicitly wait for the server shutdown before killing it forcefully (#873)
1 parent 0f19550 commit d3f50ef

File tree

1 file changed

+66
-54
lines changed

1 file changed

+66
-54
lines changed

lib/uiautomator2.js

Lines changed: 66 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import B from 'bluebird';
1111
import axios from 'axios';
1212

1313
const REQD_PARAMS = ['adb', 'tmpDir', 'host', 'systemPort', 'devicePort', 'disableWindowAnimation'];
14-
const SERVER_LAUNCH_TIMEOUT = 30000;
14+
const SERVER_LAUNCH_TIMEOUT_MS = 30000;
1515
const SERVER_INSTALL_RETRIES = 20;
16-
const SERVICES_LAUNCH_TIMEOUT = 30000;
17-
const SERVER_PACKAGE_ID = 'io.appium.uiautomator2.server';
18-
const SERVER_TEST_PACKAGE_ID = `${SERVER_PACKAGE_ID}.test`;
19-
const INSTRUMENTATION_TARGET = `${SERVER_TEST_PACKAGE_ID}/androidx.test.runner.AndroidJUnitRunner`;
16+
const SERVICES_LAUNCH_TIMEOUT_MS = 30000;
17+
const SERVER_SHUTDOWN_TIMEOUT_MS = 5000;
18+
const SERVER_REQUEST_TIMEOUT_MS = 500;
19+
export const SERVER_PACKAGE_ID = 'io.appium.uiautomator2.server';
20+
export const SERVER_TEST_PACKAGE_ID = `${SERVER_PACKAGE_ID}.test`;
21+
export const INSTRUMENTATION_TARGET = `${SERVER_TEST_PACKAGE_ID}/androidx.test.runner.AndroidJUnitRunner`;
2022

2123
class UIA2Proxy extends JWProxy {
2224
/** @type {boolean} */
@@ -36,7 +38,7 @@ class UIA2Proxy extends JWProxy {
3638
}
3739
}
3840

39-
class UiAutomator2Server {
41+
export class UiAutomator2Server {
4042
/** @type {string} */
4143
host;
4244

@@ -209,7 +211,7 @@ class UiAutomator2Server {
209211
}
210212

211213
async verifyServicesAvailability () {
212-
this.log.debug(`Waiting up to ${SERVICES_LAUNCH_TIMEOUT}ms for services to be available`);
214+
this.log.debug(`Waiting up to ${SERVICES_LAUNCH_TIMEOUT_MS}ms for services to be available`);
213215
let isPmServiceAvailable = false;
214216
let pmOutput = '';
215217
let pmError = null;
@@ -236,7 +238,7 @@ class UiAutomator2Server {
236238
}
237239
return isPmServiceAvailable;
238240
}, {
239-
waitMs: SERVICES_LAUNCH_TIMEOUT,
241+
waitMs: SERVICES_LAUNCH_TIMEOUT_MS,
240242
intervalMs: 1000,
241243
});
242244
} catch {
@@ -260,7 +262,7 @@ class UiAutomator2Server {
260262
this.log.info(`Using UIAutomator2 server from '${apkPath}' and test from '${testApkPath}'`);
261263
}
262264

263-
const timeout = caps.uiautomator2ServerLaunchTimeout || SERVER_LAUNCH_TIMEOUT;
265+
const timeout = caps.uiautomator2ServerLaunchTimeout || SERVER_LAUNCH_TIMEOUT_MS;
264266
const timer = new timing.Timer().start();
265267
let retries = 0;
266268
const maxRetries = 2;
@@ -344,81 +346,67 @@ class UiAutomator2Server {
344346
}
345347

346348
async stopInstrumentationProcess () {
347-
if (!this.instrumentationProcess) {
348-
return;
349-
}
350-
351349
try {
352-
if (this.instrumentationProcess.isRunning) {
350+
if (this.instrumentationProcess?.isRunning) {
353351
await this.instrumentationProcess.stop();
354352
}
355353
} finally {
356-
this.instrumentationProcess.removeAllListeners();
354+
this.instrumentationProcess?.removeAllListeners();
357355
this.instrumentationProcess = null;
358356
}
359357
}
360358

361359
async deleteSession () {
362360
this.log.debug('Deleting UiAutomator2 server session');
363-
// rely on jwproxy's intelligence to know what we're talking about and
364-
// delete the current session
361+
365362
try {
366363
await this.jwproxy.command('/', 'DELETE');
367364
} catch (err) {
368-
this.log.warn(`Did not get confirmation UiAutomator2 deleteSession worked; ` +
369-
`Error was: ${err}`);
365+
this.log.warn(
366+
`Did not get the confirmation of UiAutomator2 server session deletion. ` +
367+
`Original error: ${err.message}`
368+
);
370369
}
370+
371+
// Theoretically we could also force kill instumentation and server processes
372+
// without waiting for them to properly quit on their own.
373+
// This may cause unexpected error reports in device logs though.
374+
await this._waitForTermination();
375+
371376
try {
372377
await this.stopInstrumentationProcess();
373378
} catch (err) {
374379
this.log.warn(`Could not stop the instrumentation process. Original error: ${err.message}`);
375380
}
381+
382+
try {
383+
await B.all([
384+
this.adb.forceStop(SERVER_PACKAGE_ID),
385+
this.adb.forceStop(SERVER_TEST_PACKAGE_ID)
386+
]);
387+
} catch {}
376388
}
377389

378390
async cleanupAutomationLeftovers (strictCleanup = false) {
379391
this.log.debug(`Performing ${strictCleanup ? 'strict' : 'shallow'} cleanup of automation leftovers`);
380392

381-
const axiosTimeout = 500;
382-
383-
const waitStop = async () => {
384-
// Wait for the process stop by sending a status request to the port.
385-
// We observed the process stop could be delayed, thus causing unexpected crashes
386-
// in the middle of the session preparation process. It caused an invalid session error response
387-
// by the uia2 server, but that was because the process stop's delay.
388-
const timeout = 3000;
389-
try {
390-
await waitForCondition(async () => {
391-
try {
392-
await axios({
393-
url: `http://${this.host}:${this.systemPort}/status`,
394-
timeout: axiosTimeout,
395-
});
396-
} catch {
397-
return true;
398-
}
399-
}, {
400-
waitMs: timeout,
401-
intervalMs: 100,
402-
});
403-
} catch {
404-
this.log.warn(`The ${SERVER_TEST_PACKAGE_ID} process might fail to stop within ${timeout}ms timeout.`);
405-
}
406-
};
407-
393+
const serverBase = `http://${this.host}:${this.systemPort}`;
408394
try {
409395
const {value} = (await axios({
410-
url: `http://${this.host}:${this.systemPort}/sessions`,
411-
timeout: axiosTimeout,
396+
url: `${serverBase}/sessions`,
397+
timeout: SERVER_REQUEST_TIMEOUT_MS,
412398
})).data;
413399
const activeSessionIds = value.map(({id}) => id).filter(Boolean);
414400
if (activeSessionIds.length) {
415-
this.log.debug(`The following obsolete sessions are still running: ${JSON.stringify(activeSessionIds)}`);
401+
this.log.debug(`The following obsolete sessions are still running: ${activeSessionIds}`);
416402
this.log.debug(`Cleaning up ${util.pluralize('obsolete session', activeSessionIds.length, true)}`);
417403
await B.all(activeSessionIds
418-
.map((id) => axios.delete(`http://${this.host}:${this.systemPort}/session/${id}`))
404+
.map((/** @type {string} */ id) => axios.delete(`${serverBase}/session/${id}`, {
405+
timeout: SERVER_REQUEST_TIMEOUT_MS,
406+
}))
419407
);
420-
// Let all sessions to be properly terminated before continuing
421-
await B.delay(1000);
408+
// Let the server to be properly terminated before continuing
409+
await this._waitForTermination();
422410
} else {
423411
this.log.debug('No obsolete sessions have been detected');
424412
}
@@ -438,11 +426,35 @@ class UiAutomator2Server {
438426
await this.adb.killProcessesByName('uiautomator');
439427
} catch {}
440428
}
441-
await waitStop();
429+
}
430+
431+
/**
432+
* Blocks until UIA2 server stops running
433+
* or SERVER_SHUTDOWN_TIMEOUT_MS expires
434+
*
435+
* @returns {Promise<void>}
436+
*/
437+
async _waitForTermination() {
438+
try {
439+
await waitForCondition(async () => {
440+
try {
441+
return !(await this.adb.processExists(SERVER_PACKAGE_ID));
442+
} catch {
443+
return true;
444+
}
445+
}, {
446+
waitMs: SERVER_SHUTDOWN_TIMEOUT_MS,
447+
intervalMs: 300,
448+
});
449+
} catch {
450+
this.log.warn(
451+
`The UIA2 server did has not been terminated within ${SERVER_SHUTDOWN_TIMEOUT_MS}ms timeout. ` +
452+
`Continuing anyway`
453+
);
454+
}
442455
}
443456
}
444457

445-
export { UiAutomator2Server, INSTRUMENTATION_TARGET, SERVER_PACKAGE_ID, SERVER_TEST_PACKAGE_ID };
446458
export default UiAutomator2Server;
447459

448460
/**

0 commit comments

Comments
 (0)