UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

111 lines (102 loc) 4.43 kB
import type {AppiumLogger} from '@appium/types'; import {services, INSTRUMENT_CHANNEL} from 'appium-ios-device'; import type {Devicectl} from 'node-devicectl'; import {isEmpty, isIos17OrNewerPlatform} from '../utils'; import {InstallationProxyClient} from './installation-proxy-client'; import type {RemoteXPCFacade} from './remote-xpc'; type TerminateAppResult = | {terminated: true; pid: number} | {terminated: false; reason: 'not_running' | 'error'; detail?: string}; export class AppTerminationClient { constructor( private readonly udid: string, private readonly platformVersion: string, private readonly devicectl: Devicectl, private readonly log: AppiumLogger, private readonly remoteXPCFacade: RemoteXPCFacade | null = null, ) {} async terminate(bundleId: string): Promise<boolean> { let result: TerminateAppResult | null = (await this.remoteXPCFacade?.attemptService(`terminate '${bundleId}'`, async (Services) => { const dvt = await Services.startDVTService(this.udid); try { const pid = await dvt.processControl.getPidForBundleIdentifier(bundleId); if (!pid) { return {terminated: false, reason: 'not_running'} satisfies TerminateAppResult; } await dvt.processControl.kill(pid); return {terminated: true, pid} satisfies TerminateAppResult; } finally { await dvt.dvtService.close(); } })) ?? null; if (!result) { result = await this.terminateLegacy(bundleId); } if (result.terminated) { this.log.debug(`Killed process for '${bundleId}' app with PID ${result.pid}`); return true; } switch (result.reason) { case 'not_running': this.log.info(`The process of '${bundleId}' app was not running`); break; case 'error': this.log.warn(`Failed to kill '${bundleId}'. Original error: ${result.detail ?? 'unknown'}`); break; } return false; } private async terminateLegacy(bundleId: string): Promise<TerminateAppResult> { let instrumentService: any; let installProxyClient: InstallationProxyClient | undefined; try { installProxyClient = await InstallationProxyClient.create(this.udid); const apps = await installProxyClient.listApplications({ returnAttributes: ['CFBundleIdentifier', 'CFBundleExecutable'], }); if (!apps[bundleId]) { return {terminated: false, reason: 'not_running'}; } const executableName = apps[bundleId].CFBundleExecutable; this.log.debug(`The executable name for the bundle id '${bundleId}' was '${executableName}'`); if (isIos17OrNewerPlatform(this.platformVersion)) { if (process.platform !== 'darwin') { return { terminated: false, reason: 'error', detail: `devicectl is only available on macOS; cannot terminate '${bundleId}' via devicectl from '${process.platform}'`, }; } this.log.debug(`Calling devicectl to kill the process`); const pids = (await this.devicectl.listProcesses()) .filter(({executable}) => executable.endsWith(`/${executableName}`)) .map(({processIdentifier}) => processIdentifier); if (isEmpty(pids)) { return {terminated: false, reason: 'not_running'}; } await this.devicectl.sendSignalToProcess(pids[0], 2); return {terminated: true, pid: pids[0]}; } // iOS < 17: use instrument service instrumentService = await services.startInstrumentService(this.udid); const processes = await instrumentService.callChannel(INSTRUMENT_CHANNEL.DEVICE_INFO, 'runningProcesses'); const matchingProcess = processes.selector.find((proc: any) => proc.name === executableName); if (!matchingProcess) { return {terminated: false, reason: 'not_running'}; } await instrumentService.callChannel(INSTRUMENT_CHANNEL.PROCESS_CONTROL, 'killPid:', `${matchingProcess.pid}`); return {terminated: true, pid: matchingProcess.pid}; } catch (err) { const detail = (err as any).stderr ?? (err as Error).message; return {terminated: false, reason: 'error', detail: String(detail)}; } finally { if (installProxyClient) { await installProxyClient.close(); } if (instrumentService) { instrumentService.close(); } } } }