import {exec} from 'teen_process'; import {fs} from '@appium/support'; import type {AppiumLogger, StringRecord} from '@appium/types'; import {getPIDsListeningOnPort, resetTestProcesses} from './utils'; import type {NoSessionProxy} from './no-session-proxy'; import type {XcodeBuild} from './xcodebuild'; import type { AppleDevice, RealDevicePreinstalledHostOps, RealDeviceXcodebuildHostOps, SimulatorHostOps, WdaHostOps, WdaLaunchEnvironment, WdaStartupStrategyName, } from './types'; const WDA_AGENT_PORT = 8100; const HOST_OPS_REQUIRED_MESSAGE = 'Host operations must be provided to launch or terminate preinstalled WebDriverAgent'; export interface WdaStartupStrategy { readonly name: WdaStartupStrategyName; launch(sessionId: string): Promise; quit(): Promise; } export interface WdaStartupStrategyContext { readonly argsWebDriverAgentUrl?: string; readonly webDriverAgentUrl?: string; readonly usePreinstalledWDA?: boolean; readonly useXctestrunFile?: boolean; readonly usePrebuiltWDA?: boolean; readonly prebuildWDA?: boolean; readonly isRealDevice: boolean; readonly device: AppleDevice; readonly agentPath: string; readonly bootstrapPath: string; readonly bundleIdForXctest: string; readonly wdaLocalPort?: number; readonly wdaRemotePort: number; readonly wdaBindingIP?: string; readonly wdaLaunchTimeout: number; readonly mjpegServerPort?: number; readonly maxHttpRequestBodySize?: number; readonly platformName?: string; readonly platformVersion?: string; readonly log: AppiumLogger; readonly hostOps: Required; setWebDriverAgentUrl(value?: string): void; setUrl(value: string): void; setupProxies(sessionId: string): void; getStatus(timeoutMs?: number): Promise; cleanupProjectIfFresh(): Promise; xcodebuild(): XcodeBuild; noSessionProxy(): NoSessionProxy; setStarted(started: boolean): void; } class ExistingWdaUrlStrategy implements WdaStartupStrategy { readonly name = 'existing-url' as const; constructor(private readonly ctx: WdaStartupStrategyContext) {} async launch(sessionId: string): Promise { this.ctx.log.info(`Using provided WebdriverAgent at '${this.ctx.webDriverAgentUrl}'`); this.ctx.setUrl(this.ctx.webDriverAgentUrl as string); this.ctx.setupProxies(sessionId); return await this.ctx.getStatus(); } async quit(): Promise { this.ctx.log.debug( 'Stopping neither xcodebuild nor XCTest session since WDA lifecycle is not managed by this driver', ); } } class SimulatorWdaStrategy implements WdaStartupStrategy { readonly name = 'simulator' as const; constructor(private readonly ctx: WdaStartupStrategyContext) {} async launch(sessionId: string): Promise { if (this.ctx.usePreinstalledWDA) { return await launchPreinstalled(this.ctx, this.ctx.hostOps.simulator, sessionId); } return await launchWithXcodebuild(this.ctx, sessionId); } async quit(): Promise { if (this.ctx.usePreinstalledWDA) { await terminatePreinstalled(this.ctx, this.ctx.hostOps.simulator); return; } await quitXcodebuild(this.ctx); } } class RealDeviceXcodebuildStrategy implements WdaStartupStrategy { readonly name = 'real-device-xcodebuild' as const; constructor(private readonly ctx: WdaStartupStrategyContext) {} async launch(sessionId: string): Promise { return await launchWithXcodebuild(this.ctx, sessionId); } async quit(): Promise { await quitXcodebuild(this.ctx); } } class RealDevicePreinstalledStrategy implements WdaStartupStrategy { readonly name = 'real-device-preinstalled' as const; constructor(private readonly ctx: WdaStartupStrategyContext) {} async launch(sessionId: string): Promise { return await launchPreinstalled(this.ctx, this.ctx.hostOps.realDevicePreinstalled, sessionId); } async quit(): Promise { await terminatePreinstalled(this.ctx, this.ctx.hostOps.realDevicePreinstalled); } } /** * Selects the WDA startup strategy for the provided launch arguments. */ export function selectWdaStartupStrategyName(args: { realDevice?: boolean; webDriverAgentUrl?: string; usePreinstalledWDA?: boolean; }): WdaStartupStrategyName { if (args.webDriverAgentUrl) { return 'existing-url'; } if (!args.realDevice) { return 'simulator'; } if (args.usePreinstalledWDA) { return 'real-device-preinstalled'; } return 'real-device-xcodebuild'; } /** * Creates a WDA startup strategy for the current facade state. */ export function createWdaStartupStrategy(ctx: WdaStartupStrategyContext): WdaStartupStrategy { const startupStrategy = selectWdaStartupStrategyName({ realDevice: ctx.isRealDevice, webDriverAgentUrl: ctx.webDriverAgentUrl, usePreinstalledWDA: ctx.usePreinstalledWDA, }); switch (startupStrategy) { case 'existing-url': return new ExistingWdaUrlStrategy(ctx); case 'simulator': return new SimulatorWdaStrategy(ctx); case 'real-device-preinstalled': return new RealDevicePreinstalledStrategy(ctx); case 'real-device-xcodebuild': return new RealDeviceXcodebuildStrategy(ctx); default: throw new Error(`Unknown WDA startup strategy: ${startupStrategy}`); } } /** * Creates default host operations for flows the package can own directly. */ export function createDefaultWdaHostOps(): Required { return { simulator: createDefaultSimulatorWdaHostOps(), realDevicePreinstalled: createDefaultRealDevicePreinstalledHostOps(), realDeviceXcodebuild: createDefaultRealDeviceXcodebuildHostOps(), }; } /** * Creates default simulator host operations. */ export function createDefaultSimulatorWdaHostOps(): SimulatorHostOps { return { async launchPreinstalled() { throw new Error(HOST_OPS_REQUIRED_MESSAGE); }, async terminate() { throw new Error(HOST_OPS_REQUIRED_MESSAGE); }, async resetTestProcesses({udid, isSimulator}) { await resetTestProcesses(udid, isSimulator); }, }; } /** * Creates default real-device preinstalled host operations. */ export function createDefaultRealDevicePreinstalledHostOps(): RealDevicePreinstalledHostOps { return { async launchPreinstalled() { throw new Error(HOST_OPS_REQUIRED_MESSAGE); }, async terminate() { throw new Error(HOST_OPS_REQUIRED_MESSAGE); }, }; } /** * Creates default real-device xcodebuild host operations. */ export function createDefaultRealDeviceXcodebuildHostOps(): RealDeviceXcodebuildHostOps { return { async resetTestProcesses({udid, isSimulator}) { await resetTestProcesses(udid, isSimulator); }, async cleanupObsoleteProcesses({udid, port, commandLineIncludes}) { const obsoletePids = await getPIDsListeningOnPort( port, (cmdLine) => cmdLine.includes(commandLineIncludes) && !cmdLine.toLowerCase().includes(udid.toLowerCase()), ); if (obsoletePids.length > 0) { await exec('kill', obsoletePids); } }, }; } async function launchWithXcodebuild( ctx: WdaStartupStrategyContext, sessionId: string, ): Promise { ctx.log.info('Launching WebDriverAgent on the device'); ctx.setupProxies(sessionId); if (!ctx.useXctestrunFile && !(await fs.exists(ctx.agentPath))) { throw new Error( `Trying to use WebDriverAgent project at '${ctx.agentPath}' but the ` + 'file does not exist', ); } if (ctx.useXctestrunFile || ctx.usePrebuiltWDA) { ctx.log.info('Skipped WDA project cleanup according to the provided capabilities'); } else { await ctx.cleanupProjectIfFresh(); } const resetTestProcesses = ctx.isRealDevice ? ctx.hostOps.realDeviceXcodebuild.resetTestProcesses : ctx.hostOps.simulator.resetTestProcesses; await resetTestProcesses?.({ udid: ctx.device.udid, isSimulator: !ctx.isRealDevice, }); const xcodebuild = ctx.xcodebuild(); await xcodebuild.init(ctx.noSessionProxy()); if (ctx.prebuildWDA) { await xcodebuild.prebuild(); } return (await xcodebuild.start()) as StringRecord | null; } async function launchPreinstalled( ctx: WdaStartupStrategyContext, hostOps: SimulatorHostOps | RealDevicePreinstalledHostOps, sessionId: string, ): Promise { const xctestEnv = createPreinstalledWdaEnvironment(ctx); ctx.log.info('Launching WebDriverAgent on the device without xcodebuild'); await hostOps.launchPreinstalled({ udid: ctx.device.udid, bundleId: ctx.bundleIdForXctest, env: xctestEnv, wdaLocalPort: ctx.wdaLocalPort, wdaRemotePort: ctx.wdaRemotePort, platformName: ctx.platformName, platformVersion: ctx.platformVersion, timeoutMs: ctx.wdaLaunchTimeout, }); ctx.setupProxies(sessionId); let status: StringRecord | null; try { status = await ctx.getStatus(ctx.wdaLaunchTimeout); } catch { throw new Error( `Failed to start the preinstalled WebDriverAgent in ${ctx.wdaLaunchTimeout} ms. ` + `The WebDriverAgent might not be properly built or the device might be locked. ` + `The 'appium:wdaLaunchTimeout' capability modifies the timeout.`, ); } ctx.setStarted(true); return status; } async function terminatePreinstalled( ctx: WdaStartupStrategyContext, hostOps: SimulatorHostOps | RealDevicePreinstalledHostOps, ): Promise { ctx.log.info('Stopping the XCTest session'); try { await hostOps.terminate({ udid: ctx.device.udid, bundleId: ctx.bundleIdForXctest, }); } catch (e: any) { ctx.log.warn(e.message); } } async function quitXcodebuild(ctx: WdaStartupStrategyContext): Promise { ctx.log.info('Shutting down sub-processes'); await ctx.xcodebuild().quit(); } function createPreinstalledWdaEnvironment(ctx: WdaStartupStrategyContext): WdaLaunchEnvironment { const xctestEnv: WdaLaunchEnvironment = { USE_PORT: ctx.wdaLocalPort || WDA_AGENT_PORT, WDA_PRODUCT_BUNDLE_IDENTIFIER: ctx.bundleIdForXctest, }; if (ctx.mjpegServerPort) { xctestEnv.MJPEG_SERVER_PORT = ctx.mjpegServerPort; } if (ctx.wdaBindingIP) { xctestEnv.USE_IP = ctx.wdaBindingIP; } if (ctx.maxHttpRequestBodySize) { xctestEnv.MAX_HTTP_REQUEST_BODY_SIZE = ctx.maxHttpRequestBodySize; } return xctestEnv; }