@@ -11,12 +11,14 @@ import B from 'bluebird';
11
11
import axios from 'axios' ;
12
12
13
13
const REQD_PARAMS = [ 'adb' , 'tmpDir' , 'host' , 'systemPort' , 'devicePort' , 'disableWindowAnimation' ] ;
14
- const SERVER_LAUNCH_TIMEOUT = 30000 ;
14
+ const SERVER_LAUNCH_TIMEOUT_MS = 30000 ;
15
15
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` ;
20
22
21
23
class UIA2Proxy extends JWProxy {
22
24
/** @type {boolean } */
@@ -36,7 +38,7 @@ class UIA2Proxy extends JWProxy {
36
38
}
37
39
}
38
40
39
- class UiAutomator2Server {
41
+ export class UiAutomator2Server {
40
42
/** @type {string } */
41
43
host ;
42
44
@@ -209,7 +211,7 @@ class UiAutomator2Server {
209
211
}
210
212
211
213
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` ) ;
213
215
let isPmServiceAvailable = false ;
214
216
let pmOutput = '' ;
215
217
let pmError = null ;
@@ -236,7 +238,7 @@ class UiAutomator2Server {
236
238
}
237
239
return isPmServiceAvailable ;
238
240
} , {
239
- waitMs : SERVICES_LAUNCH_TIMEOUT ,
241
+ waitMs : SERVICES_LAUNCH_TIMEOUT_MS ,
240
242
intervalMs : 1000 ,
241
243
} ) ;
242
244
} catch {
@@ -260,7 +262,7 @@ class UiAutomator2Server {
260
262
this . log . info ( `Using UIAutomator2 server from '${ apkPath } ' and test from '${ testApkPath } '` ) ;
261
263
}
262
264
263
- const timeout = caps . uiautomator2ServerLaunchTimeout || SERVER_LAUNCH_TIMEOUT ;
265
+ const timeout = caps . uiautomator2ServerLaunchTimeout || SERVER_LAUNCH_TIMEOUT_MS ;
264
266
const timer = new timing . Timer ( ) . start ( ) ;
265
267
let retries = 0 ;
266
268
const maxRetries = 2 ;
@@ -344,81 +346,67 @@ class UiAutomator2Server {
344
346
}
345
347
346
348
async stopInstrumentationProcess ( ) {
347
- if ( ! this . instrumentationProcess ) {
348
- return ;
349
- }
350
-
351
349
try {
352
- if ( this . instrumentationProcess . isRunning ) {
350
+ if ( this . instrumentationProcess ? .isRunning ) {
353
351
await this . instrumentationProcess . stop ( ) ;
354
352
}
355
353
} finally {
356
- this . instrumentationProcess . removeAllListeners ( ) ;
354
+ this . instrumentationProcess ? .removeAllListeners ( ) ;
357
355
this . instrumentationProcess = null ;
358
356
}
359
357
}
360
358
361
359
async deleteSession ( ) {
362
360
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
+
365
362
try {
366
363
await this . jwproxy . command ( '/' , 'DELETE' ) ;
367
364
} 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
+ ) ;
370
369
}
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
+
371
376
try {
372
377
await this . stopInstrumentationProcess ( ) ;
373
378
} catch ( err ) {
374
379
this . log . warn ( `Could not stop the instrumentation process. Original error: ${ err . message } ` ) ;
375
380
}
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 { }
376
388
}
377
389
378
390
async cleanupAutomationLeftovers ( strictCleanup = false ) {
379
391
this . log . debug ( `Performing ${ strictCleanup ? 'strict' : 'shallow' } cleanup of automation leftovers` ) ;
380
392
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 } ` ;
408
394
try {
409
395
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 ,
412
398
} ) ) . data ;
413
399
const activeSessionIds = value . map ( ( { id} ) => id ) . filter ( Boolean ) ;
414
400
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 } ` ) ;
416
402
this . log . debug ( `Cleaning up ${ util . pluralize ( 'obsolete session' , activeSessionIds . length , true ) } ` ) ;
417
403
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
+ } ) )
419
407
) ;
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 ( ) ;
422
410
} else {
423
411
this . log . debug ( 'No obsolete sessions have been detected' ) ;
424
412
}
@@ -438,11 +426,35 @@ class UiAutomator2Server {
438
426
await this . adb . killProcessesByName ( 'uiautomator' ) ;
439
427
} catch { }
440
428
}
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
+ }
442
455
}
443
456
}
444
457
445
- export { UiAutomator2Server , INSTRUMENTATION_TARGET , SERVER_PACKAGE_ID , SERVER_TEST_PACKAGE_ID } ;
446
458
export default UiAutomator2Server ;
447
459
448
460
/**
0 commit comments