11import _ from 'lodash' ;
22import { fs , tempDir , util , system } from '@appium/support' ;
33import { log } from '../logger.js' ;
4- import { sleep , waitForCondition } from 'asyncbox' ;
5- import B from 'bluebird' ;
4+ import { waitForCondition } from 'asyncbox' ;
65import path from 'path' ;
76
87/** @type {import('./types').StringRecord<import('./types').InstallState> } */
@@ -19,9 +18,6 @@ const IGNORED_PERM_ERRORS = [
1918 / U n k n o w n p e r m i s s i o n / i,
2019] ;
2120const MIN_API_LEVEL_WITH_PERMS_SUPPORT = 23 ;
22- const PID_COLUMN_TITLE = 'PID' ;
23- const PROCESS_NAME_COLUMN_TITLE = 'NAME' ;
24- const PS_TITLE_PATTERN = new RegExp ( `^(.*\\b${ PID_COLUMN_TITLE } \\b.*\\b${ PROCESS_NAME_COLUMN_TITLE } \\b.*)$` , 'm' ) ;
2521const RESOLVER_ACTIVITY_NAME = 'android/com.android.internal.app.ResolverActivity' ;
2622const MAIN_ACTION = 'android.intent.action.MAIN' ;
2723const LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER' ;
@@ -308,211 +304,6 @@ export async function stopAndClear (pkg) {
308304 }
309305}
310306
311-
312- /**
313- * At some point of time Google has changed the default `ps` behaviour, so it only
314- * lists processes that belong to the current shell user rather to all
315- * users. It is necessary to execute ps with -A command line argument
316- * to mimic the previous behaviour.
317- *
318- * @this {import('../adb.js').ADB}
319- * @returns {Promise<string> } the output of `ps` command where all processes are included
320- */
321- export async function listProcessStatus ( ) {
322- if ( ! _ . isBoolean ( this . _doesPsSupportAOption ) ) {
323- try {
324- this . _doesPsSupportAOption = / ^ - A \b / m. test ( await this . shell ( [ 'ps' , '--help' ] ) ) ;
325- } catch ( e ) {
326- log . debug ( ( /** @type {Error } */ ( e ) ) . stack ) ;
327- this . _doesPsSupportAOption = false ;
328- }
329- }
330- return await this . shell ( this . _doesPsSupportAOption ? [ 'ps' , '-A' ] : [ 'ps' ] ) ;
331- }
332-
333- /**
334- * Returns process name for the given process identifier
335- *
336- * @this {import('../adb.js').ADB}
337- * @param {string|number } pid - The valid process identifier
338- * @throws {Error } If the given PID is either invalid or is not present
339- * in the active processes list
340- * @returns {Promise<string> } The process name
341- */
342- export async function getNameByPid ( pid ) {
343- // @ts -ignore This validation works as expected
344- if ( isNaN ( pid ) ) {
345- throw new Error ( `The PID value must be a valid number. '${ pid } ' is given instead` ) ;
346- }
347- pid = parseInt ( `${ pid } ` , 10 ) ;
348-
349- const stdout = await this . listProcessStatus ( ) ;
350- const titleMatch = PS_TITLE_PATTERN . exec ( stdout ) ;
351- if ( ! titleMatch ) {
352- log . debug ( stdout ) ;
353- throw new Error ( `Could not get the process name for PID '${ pid } '` ) ;
354- }
355- const allTitles = titleMatch [ 1 ] . trim ( ) . split ( / \s + / ) ;
356- const pidIndex = allTitles . indexOf ( PID_COLUMN_TITLE ) ;
357- // it might not be stable to take NAME by index, because depending on the
358- // actual SDK the ps output might not contain an abbreviation for the S flag:
359- // USER PID PPID VSIZE RSS WCHAN PC NAME
360- // USER PID PPID VSIZE RSS WCHAN PC S NAME
361- const nameOffset = allTitles . indexOf ( PROCESS_NAME_COLUMN_TITLE ) - allTitles . length ;
362- const pidRegex = new RegExp ( `^(.*\\b${ pid } \\b.*)$` , 'gm' ) ;
363- let matchedLine ;
364- while ( ( matchedLine = pidRegex . exec ( stdout ) ) ) {
365- const items = matchedLine [ 1 ] . trim ( ) . split ( / \s + / ) ;
366- if ( parseInt ( items [ pidIndex ] , 10 ) === pid && items [ items . length + nameOffset ] ) {
367- return items [ items . length + nameOffset ] ;
368- }
369- }
370- log . debug ( stdout ) ;
371- throw new Error ( `Could not get the process name for PID '${ pid } '` ) ;
372- }
373-
374- /**
375- * Get the list of process ids for the particular process on the device under test.
376- *
377- * @this {import('../adb.js').ADB}
378- * @param {string } name - The part of process name.
379- * @return {Promise<number[]> } The list of matched process IDs or an empty list.
380- * @throws {Error } If the passed process name is not a valid one
381- */
382- export async function getPIDsByName ( name ) {
383- log . debug ( `Getting IDs of all '${ name } ' processes` ) ;
384- if ( ! this . isValidClass ( name ) ) {
385- throw new Error ( `Invalid process name: '${ name } '` ) ;
386- }
387-
388- const pidRegex = new RegExp ( `ProcessRecord\\{[\\w]+\\s+(\\d+):${ _ . escapeRegExp ( name ) } \\/` ) ;
389- const processesInfo = await this . shell ( [ 'dumpsys' , 'activity' , 'processes' ] ) ;
390- const pids = processesInfo . split ( '\n' )
391- . map ( ( line ) => line . match ( pidRegex ) )
392- . filter ( ( match ) => ! ! match )
393- . map ( ( [ , pidStr ] ) => parseInt ( pidStr , 10 ) ) ;
394- return _ . uniq ( pids ) ;
395- }
396-
397- /**
398- * Get the list of process ids for the particular process on the device under test.
399- *
400- * @this {import('../adb.js').ADB}
401- * @param {string } name - The part of process name.
402- */
403- export async function killProcessesByName ( name ) {
404- try {
405- log . debug ( `Attempting to kill all ${ name } processes` ) ;
406- const pids = await this . getPIDsByName ( name ) ;
407- if ( _ . isEmpty ( pids ) ) {
408- log . info ( `No '${ name } ' process has been found` ) ;
409- } else {
410- await B . all ( pids . map ( ( p ) => this . killProcessByPID ( p ) ) ) ;
411- }
412- } catch ( e ) {
413- const err = /** @type {Error } */ ( e ) ;
414- throw new Error ( `Unable to kill ${ name } processes. Original error: ${ err . message } ` ) ;
415- }
416- }
417-
418- /**
419- * Kill the particular process on the device under test.
420- * The current user is automatically switched to root if necessary in order
421- * to properly kill the process.
422- *
423- * @this {import('../adb.js').ADB}
424- * @param {string|number } pid - The ID of the process to be killed.
425- * @throws {Error } If the process cannot be killed.
426- */
427- export async function killProcessByPID ( pid ) {
428- log . debug ( `Attempting to kill process ${ pid } ` ) ;
429- const noProcessFlag = 'No such process' ;
430- try {
431- // Check if the process exists and throw an exception otherwise
432- await this . shell ( [ 'kill' , `${ pid } ` ] ) ;
433- } catch ( e ) {
434- const err = /** @type {import('teen_process').ExecError } */ ( e ) ;
435- if ( _ . includes ( err . stderr , noProcessFlag ) ) {
436- return ;
437- }
438- if ( ! _ . includes ( err . stderr , 'Operation not permitted' ) ) {
439- throw err ;
440- }
441- log . info ( `Cannot kill PID ${ pid } due to insufficient permissions. Retrying as root` ) ;
442- try {
443- await this . shell ( [ 'kill' , `${ pid } ` ] , {
444- privileged : true
445- } ) ;
446- } catch ( e1 ) {
447- const err1 = /** @type {import('teen_process').ExecError } */ ( e1 ) ;
448- if ( _ . includes ( err1 . stderr , noProcessFlag ) ) {
449- return ;
450- }
451- throw err1 ;
452- }
453- }
454- }
455-
456- /**
457- * Broadcast process killing on the device under test.
458- *
459- * @deprecated This method is deprecated and marked for removal in future releases.
460- * @this {import('../adb.js').ADB}
461- * @param {string } intent - The name of the intent to broadcast to.
462- * @param {string } processName - The name of the killed process.
463- * @throws {error } If the process was not killed.
464- */
465- export async function broadcastProcessEnd ( intent , processName ) {
466- // start the broadcast without waiting for it to finish.
467- this . broadcast ( intent ) ;
468- // wait for the process to end
469- let start = Date . now ( ) ;
470- let timeoutMs = 40000 ;
471- try {
472- while ( ( Date . now ( ) - start ) < timeoutMs ) {
473- if ( await this . processExists ( processName ) ) {
474- // cool down
475- await sleep ( 400 ) ;
476- continue ;
477- }
478- return ;
479- }
480- throw new Error ( `Process never died within ${ timeoutMs } ms` ) ;
481- } catch ( e ) {
482- const err = /** @type {Error } */ ( e ) ;
483- throw new Error ( `Unable to broadcast process end. Original error: ${ err . message } ` ) ;
484- }
485- }
486-
487- /**
488- * Broadcast a message to the given intent.
489- *
490- * @this {import('../adb.js').ADB}
491- * @param {string } intent - The name of the intent to broadcast to.
492- * @throws {error } If intent name is not a valid class name.
493- */
494- export async function broadcast ( intent ) {
495- if ( ! this . isValidClass ( intent ) ) {
496- throw new Error ( `Invalid intent ${ intent } ` ) ;
497- }
498- log . debug ( `Broadcasting: ${ intent } ` ) ;
499- await this . shell ( [ 'am' , 'broadcast' , '-a' , intent ] ) ;
500- }
501-
502- /**
503- * Check whether the process with the particular name is running on the device
504- * under test.
505- *
506- * @this {import('../adb.js').ADB}
507- * @param {string } processName - The name of the process to be checked.
508- * @return {Promise<boolean> } True if the given process is running.
509- * @throws {Error } If the given process name is not a valid class name.
510- */
511- export async function processExists ( processName ) {
512- return ! _ . isEmpty ( await this . getPIDsByName ( processName ) ) ;
513- }
514-
515-
516307/**
517308 * Get the package info from the installed application.
518309 *
@@ -631,7 +422,6 @@ export async function activateApp (appId) {
631422 }
632423}
633424
634-
635425/**
636426 * Check whether the particular package is present on the device under test.
637427 *
@@ -1285,7 +1075,52 @@ export function extractMatchingPermissions (dumpsysOutput, groupNames, grantedSt
12851075 log . debug ( `Retrieved ${ util . pluralize ( 'permission' , filteredResult . length , true ) } ` +
12861076 `from ${ groupNames } ${ util . pluralize ( 'group' , groupNames . length , false ) } ` ) ;
12871077 return filteredResult ;
1288- } ;
1078+ }
1079+
1080+ /**
1081+ * Broadcast a message to the given intent.
1082+ *
1083+ * @this {import('../adb.js').ADB}
1084+ * @param {string } intent - The name of the intent to broadcast to.
1085+ * @throws {error } If intent name is not a valid class name.
1086+ */
1087+ export async function broadcast ( intent ) {
1088+ if ( ! this . isValidClass ( intent ) ) {
1089+ throw new Error ( `Invalid intent ${ intent } ` ) ;
1090+ }
1091+ log . debug ( `Broadcasting: ${ intent } ` ) ;
1092+ await this . shell ( [ 'am' , 'broadcast' , '-a' , intent ] ) ;
1093+ }
1094+
1095+ /**
1096+ * Get the list of process ids for the particular package on the device under test.
1097+ *
1098+ * @this {import('../adb.js').ADB}
1099+ * @param {string } pkg
1100+ * @returns {Promise<number[]> } The list of matched process IDs or an empty list.
1101+ */
1102+ export async function listAppProcessIds ( pkg ) {
1103+ log . debug ( `Getting IDs of all '${ pkg } ' package` ) ;
1104+ const pidRegex = new RegExp ( `ProcessRecord\\{[\\w]+\\s+(\\d+):${ _ . escapeRegExp ( pkg ) } \\/` ) ;
1105+ const processesInfo = await this . shell ( [ 'dumpsys' , 'activity' , 'processes' ] ) ;
1106+ const pids = processesInfo . split ( '\n' )
1107+ . map ( ( line ) => line . match ( pidRegex ) )
1108+ . filter ( ( match ) => ! ! match )
1109+ . map ( ( [ , pidStr ] ) => parseInt ( pidStr , 10 ) ) ;
1110+ return _ . uniq ( pids ) ;
1111+ }
1112+
1113+ /**
1114+ * Check whether the process with the particular name is running on the device
1115+ * under test.
1116+ *
1117+ * @this {import('../adb.js').ADB}
1118+ * @param {string } pkg - The id of the package to be checked.
1119+ * @returns True if the given package is running.
1120+ */
1121+ export async function isAppRunning ( pkg ) {
1122+ return ! _ . isEmpty ( await this . listAppProcessIds ( pkg ) ) ;
1123+ }
12891124
12901125/**
12911126 * @typedef {Object } StartCmdOptions
0 commit comments