UNPKG

appium-adb

Version:

Android Debug Bridge interface

194 lines (181 loc) 6.85 kB
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)); }