appium-adb
Version:
Android Debug Bridge interface
194 lines (181 loc) • 6.85 kB
text/typescript
import _ from 'lodash';
import {log} from '../logger';
import B from 'bluebird';
import type {ExecError} from 'teen_process';
import type {ADB} from '../adb';
const PID_COLUMN_TITLE: string = 'PID';
const PROCESS_NAME_COLUMN_TITLE: string = 'NAME';
const PS_TITLE_PATTERN: RegExp = new RegExp(
`^(.*\\b${PID_COLUMN_TITLE}\\b.*\\b${PROCESS_NAME_COLUMN_TITLE}\\b.*)$`,
'm',
);
/**
* At some point of time Google has changed the default `ps` behaviour, so it only
* lists processes that belong to the current shell user rather to all
* users. It is necessary to execute ps with -A command line argument
* to mimic the previous behaviour.
*
* @returns the output of `ps` command where all processes are included
*/
export async function listProcessStatus(this: ADB): Promise<string> {
if (!_.isBoolean(this._doesPsSupportAOption)) {
try {
this._doesPsSupportAOption = /^-A\b/m.test(await this.shell(['ps', '--help']));
} catch (e: unknown) {
const error: Error = e as Error;
log.debug(error.stack);
this._doesPsSupportAOption = false;
}
}
return await this.shell(this._doesPsSupportAOption ? ['ps', '-A'] : ['ps']);
}
/**
* Returns process name for the given process identifier
*
* @param pid - The valid process identifier
* @returns The process name
* @throws {Error} If the given PID is either invalid or is not present
* in the active processes list
*/
export async function getProcessNameById(this: ADB, pid: string | number): Promise<string> {
// @ts-ignore This validation works as expected
if (isNaN(Number(pid))) {
throw new Error(`The PID value must be a valid number. '${pid}' is given instead`);
}
const numericPid: number = parseInt(`${pid}`, 10);
const stdout: string = await this.listProcessStatus();
const titleMatch: RegExpExecArray | null = PS_TITLE_PATTERN.exec(stdout);
if (!titleMatch) {
log.debug(stdout);
throw new Error(`Could not get the process name for PID '${numericPid}'`);
}
const allTitles: string[] = titleMatch[1].trim().split(/\s+/);
const pidIndex: number = allTitles.indexOf(PID_COLUMN_TITLE);
// it might not be stable to take NAME by index, because depending on the
// actual SDK the ps output might not contain an abbreviation for the S flag:
// USER PID PPID VSIZE RSS WCHAN PC NAME
// USER PID PPID VSIZE RSS WCHAN PC S NAME
const nameOffset: number = allTitles.indexOf(PROCESS_NAME_COLUMN_TITLE) - allTitles.length;
const pidRegex: RegExp = new RegExp(`^(.*\\b${numericPid}\\b.*)$`, 'gm');
let matchedLine: RegExpExecArray | null;
while ((matchedLine = pidRegex.exec(stdout))) {
const items: string[] = matchedLine[1].trim().split(/\s+/);
if (parseInt(items[pidIndex], 10) === numericPid && items[items.length + nameOffset]) {
return items[items.length + nameOffset];
}
}
log.debug(stdout);
throw new Error(`Could not get the process name for PID '${numericPid}'`);
}
/**
* Get the list of process ids for the particular process on the device under test.
*
* @param name - The part of process name.
* @returns The list of matched process IDs or an empty list.
*/
export async function getProcessIdsByName(this: ADB, name: string): Promise<number[]> {
log.debug(`Getting IDs of all '${name}' processes`);
const stdout: string = await this.listProcessStatus();
const titleMatch: RegExpExecArray | null = PS_TITLE_PATTERN.exec(stdout);
if (!titleMatch) {
log.debug(stdout);
throw new Error(`Could not parse process list for name '${name}'`);
}
const allTitles: string[] = titleMatch[1].trim().split(/\s+/);
const pidIndex: number = allTitles.indexOf(PID_COLUMN_TITLE);
const nameIndex: number = allTitles.indexOf(PROCESS_NAME_COLUMN_TITLE);
const pids: number[] = [];
const lines: string[] = stdout.split('\n');
for (const line of lines) {
const items: string[] = line.trim().split(/\s+/);
if (
items.length > Math.max(pidIndex, nameIndex) &&
items[nameIndex] &&
items[pidIndex] &&
items[nameIndex] === name
) {
const pid: number = parseInt(items[pidIndex], 10);
if (!isNaN(pid)) {
pids.push(pid);
}
}
}
return _.uniq(pids);
}
/**
* Kill all processes with the given name on the device under test.
*
* @param name - The part of process name.
* @param signal - The signal to send to the process. Default is 'SIGTERM' ('15').
* @throws {Error} If the processes cannot be killed.
*/
export async function killProcessesByName(
this: ADB,
name: string,
signal: string = 'SIGTERM',
): Promise<void> {
try {
log.debug(`Attempting to kill all ${name} processes`);
const pids: number[] = await this.getProcessIdsByName(name);
if (_.isEmpty(pids)) {
log.info(`No '${name}' process has been found`);
} else {
await B.all(pids.map((p: number) => this.killProcessByPID(p, signal)));
}
} catch (e: unknown) {
const err: Error = e as Error;
throw new Error(`Unable to kill ${name} processes. Original error: ${err.message}`);
}
}
/**
* Kill the particular process on the device under test.
* The current user is automatically switched to root if necessary in order
* to properly kill the process.
*
* @param pid - The ID of the process to be killed.
* @param signal - The signal to send to the process. Default is 'SIGTERM' ('15').
* @throws {Error} If the process cannot be killed.
*/
export async function killProcessByPID(
this: ADB,
pid: string | number,
signal: string = 'SIGTERM',
): Promise<void> {
log.debug(`Attempting to kill process ${pid}`);
const noProcessFlag: string = 'No such process';
try {
// Check if the process exists and throw an exception otherwise
await this.shell(['kill', `-${signal}`, `${pid}`]);
} catch (e: unknown) {
const err: ExecError = e as ExecError;
if (_.includes(err.stderr, noProcessFlag)) {
return;
}
if (!_.includes(err.stderr, 'Operation not permitted')) {
throw err;
}
log.info(`Cannot kill PID ${pid} due to insufficient permissions. Retrying as root`);
try {
await this.shell(['kill', `${pid}`], {
privileged: true,
});
} catch (e1: unknown) {
const err1: ExecError = e1 as ExecError;
if (_.includes(err1.stderr, noProcessFlag)) {
return;
}
throw err1;
}
}
}
/**
* Check whether the process with the particular name is running on the device
* under test.
*
* @param processName - The name of the process to be checked.
* @returns True if the given process is running.
* @throws {Error} If the given process name is not a valid class name.
*/
export async function processExists(this: ADB, processName: string): Promise<boolean> {
return !_.isEmpty(await this.getProcessIdsByName(processName));
}