diff --git a/lib/webdriveragent.js b/lib/webdriveragent.js index a80909954..0d3a60188 100644 --- a/lib/webdriveragent.js +++ b/lib/webdriveragent.js @@ -1,3 +1,4 @@ +import { waitForCondition } from 'asyncbox'; import _ from 'lodash'; import path from 'path'; import url from 'url'; @@ -66,6 +67,7 @@ class WebDriverAgent { this.updatedWDABundleId = args.updatedWDABundleId; + this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT; this.usePreinstalledWDA = args.usePreinstalledWDA; this.xctestApiClient = null; @@ -87,7 +89,7 @@ class WebDriverAgent { useSimpleBuildTest: args.useSimpleBuildTest, usePrebuiltWDA: args.usePrebuiltWDA, updatedWDABundleId: this.updatedWDABundleId, - launchTimeout: args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT, + launchTimeout: this.wdaLaunchTimeout, wdaRemotePort: this.wdaRemotePort, useXctestrunFile: this.useXctestrunFile, derivedDataPath: args.derivedDataPath, @@ -184,7 +186,6 @@ class WebDriverAgent { * } * * @return {Promise} State Object - * @throws {Error} If there was invalid response code or body */ async getStatus () { const noSessionProxy = new NoSessionProxy({ @@ -287,6 +288,73 @@ class WebDriverAgent { } } + + /** + * @typedef {Object} LaunchWdaViaDeviceCtlOptions + * @property {Record} [env] environment variables for the launching WDA process + */ + + /** + * Launch WDA with preinstalled package with 'xcrun devicectl device process launch'. + * The WDA package must be prepared properly like published via + * https://github.com/appium/WebDriverAgent/releases + * with proper sign for this case. + * + * When we implement launching XCTest service via appium-ios-device, + * this implementation can be replaced with it. + * + * @param {LaunchWdaViaDeviceCtlOptions} [opts={}] launching WDA with devicectl command options. + * @return {Promise} + */ + async _launchViaDevicectl(opts = {}) { + // FIXME: use appium-xcuitest-driver's Devicectl. Maybe it needs to be included in the `this.device`? + // + + const {env} = opts; + + let xcrunBinnaryPath; + try { + xcrunBinnaryPath = await fs.which('xcrun'); + } catch (e) { + throw new Error( + `xcrun has not been found in PATH. ` + + `Please make sure XCode development tools are installed`, + ); + } + + const cmd = [ + 'devicectl', + 'device', + 'process', + 'launch', + `--device`, this.device.udid, + '--terminate-existing' + ]; + if (!_.isEmpty(env)) { + cmd.push('--environment-variables', JSON.stringify(_.mapValues(env, (v) => _.toString(v)))); + }; + cmd.push(this.bundleIdForXctest); + + const {stdout} = await exec(xcrunBinnaryPath, cmd); + this.log.debug(`The output of devicectl command: ${stdout}`); + + // Launching app via decictl does not wait for the app start. + // We should wait for the app start by ourselves. + try { + await waitForCondition(async () => !_.isNull(await this.getStatus()), { + waitMs: this.wdaLaunchTimeout, + intervalMs: 300, + }); + + } catch (err) { + throw new Error( + `Failed to start the preinstalled WebDriverAgent in ${this.wdaLaunchTimeout} ms. ` + + `The WebDriverAgent might not be properly built or the device might be locked. ` + + 'appium:wdaLaunchTimeout capability modifies the timeout.' + ); + } + } + /** * Launch WDA with preinstalled package without xcodebuild. * @param {string} sessionId Launch WDA and establish the session with this sessionId @@ -302,8 +370,15 @@ class WebDriverAgent { } this.log.info('Launching WebDriverAgent on the device without xcodebuild'); if (this.isRealDevice) { - this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv}); - await this.xctestApiClient.start(); + // Current method to launch WDA process can be done via 'xcrun devicectl', + // but it has limitation about the WDA preinstalled package. + // https://github.com/appium/appium/issues/19206#issuecomment-2014182674 + if (util.compareVersions(this.platformVersion, '>=', '17.0')) { + await this._launchViaDevicectl({env: xctestEnv}); + } else { + this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv}); + await this.xctestApiClient.start(); + } } else { await this.device.simctl.exec('launch', { args: [