Skip to content

Commit 1062d4c

Browse files
feat: Split process and app commands (#822)
BREAKING CHANGE: Renamed getPIDsByName to listAppProcessIds BREAKING CHANGE: Renamed getNameByPid to getProcessNameById BREAKING CHANGE: Removed the obsolete broadcastProcessEnd method BREAKING CHANGE: Removed the deprecated appDeviceReadyTimeout property
1 parent b92b060 commit 1062d4c

16 files changed

+973
-460
lines changed

.github/workflows/functional-test.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,8 @@ jobs:
2727
apiLevel: 28
2828
emuTag: default
2929
arch: x86
30-
- platformVersion: "7.1"
31-
apiLevel: 25
32-
emuTag: default
33-
arch: x86
34-
- platformVersion: "5.1"
35-
apiLevel: 22
30+
- platformVersion: "8.0"
31+
apiLevel: 26
3632
emuTag: default
3733
arch: x86
3834
fail-fast: false

lib/adb.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import * as fsCommands from './tools/fs-commands';
2828
import * as appCommands from './tools/app-commands';
2929
import * as networkCommands from './tools/network-commands';
3030
import * as logcatCommands from './tools/logcat-commands';
31+
import * as processCommands from './tools/process-commands';
3132

3233

3334
export const DEFAULT_ADB_PORT = 5037;
@@ -59,8 +60,6 @@ export class ADB implements ADBOptions {
5960

6061
sdkRoot?: string;
6162
udid?: string;
62-
/** @deprecated Not used anywhere, marked for deletion */
63-
appDeviceReadyTimeout?: number;
6463
useKeystore?: boolean;
6564
keystorePath?: string;
6665
keystorePassword?: string;
@@ -198,14 +197,6 @@ export class ADB implements ADBOptions {
198197
forceStop = appCommands.forceStop;
199198
killPackage = appCommands.killPackage;
200199
clear = appCommands.clear;
201-
listProcessStatus = appCommands.listProcessStatus;
202-
getNameByPid = appCommands.getNameByPid;
203-
getPIDsByName = appCommands.getPIDsByName;
204-
killProcessesByName = appCommands.killProcessesByName;
205-
killProcessByPID = appCommands.killProcessByPID;
206-
broadcastProcessEnd = appCommands.broadcastProcessEnd;
207-
broadcast = appCommands.broadcast;
208-
processExists = appCommands.processExists;
209200
readonly APP_INSTALL_STATE = appCommands.APP_INSTALL_STATE;
210201
isAppInstalled = appCommands.isAppInstalled;
211202
startUri = appCommands.startUri;
@@ -218,6 +209,16 @@ export class ADB implements ADBOptions {
218209
getPackageInfo = appCommands.getPackageInfo;
219210
pullApk = appCommands.pullApk;
220211
activateApp = appCommands.activateApp;
212+
listAppProcessIds = appCommands.listAppProcessIds;
213+
isAppRunning = appCommands.isAppRunning;
214+
broadcast = appCommands.broadcast;
215+
216+
listProcessStatus = processCommands.listProcessStatus;
217+
getProcessNameById = processCommands.getProcessNameById;
218+
getProcessIdsByName = processCommands.getProcessIdsByName;
219+
killProcessesByName = processCommands.killProcessesByName;
220+
killProcessByPID = processCommands.killProcessByPID;
221+
processExists = processCommands.processExists;
221222

222223
uninstallApk = apkUtilCommands.uninstallApk;
223224
installFromDevicePath = apkUtilCommands.installFromDevicePath;

lib/tools/app-commands.js

Lines changed: 47 additions & 212 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import _ from 'lodash';
22
import { fs, tempDir, util, system } from '@appium/support';
33
import { log } from '../logger.js';
4-
import { sleep, waitForCondition } from 'asyncbox';
5-
import B from 'bluebird';
4+
import { waitForCondition } from 'asyncbox';
65
import path from 'path';
76

87
/** @type {import('./types').StringRecord<import('./types').InstallState>} */
@@ -19,9 +18,6 @@ const IGNORED_PERM_ERRORS = [
1918
/Unknown permission/i,
2019
];
2120
const 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');
2521
const RESOLVER_ACTIVITY_NAME = 'android/com.android.internal.app.ResolverActivity';
2622
const MAIN_ACTION = 'android.intent.action.MAIN';
2723
const 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

Comments
 (0)