742 lines
25 KiB
JavaScript
742 lines
25 KiB
JavaScript
|
|
import { waitForCondition } from 'asyncbox';
|
||
|
|
import _ from 'lodash';
|
||
|
|
import path from 'path';
|
||
|
|
import url from 'url';
|
||
|
|
import B from 'bluebird';
|
||
|
|
import { JWProxy } from '@appium/base-driver';
|
||
|
|
import { fs, util, plist } from '@appium/support';
|
||
|
|
import defaultLogger from './logger';
|
||
|
|
import { NoSessionProxy } from './no-session-proxy';
|
||
|
|
import {
|
||
|
|
getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort, BOOTSTRAP_PATH
|
||
|
|
} from './utils';
|
||
|
|
import {XcodeBuild} from './xcodebuild';
|
||
|
|
import AsyncLock from 'async-lock';
|
||
|
|
import { exec } from 'teen_process';
|
||
|
|
import { bundleWDASim } from './check-dependencies';
|
||
|
|
import {
|
||
|
|
WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_APP,
|
||
|
|
WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, DEFAULT_TEST_BUNDLE_SUFFIX
|
||
|
|
} from './constants';
|
||
|
|
import {Xctest} from 'appium-ios-device';
|
||
|
|
import {strongbox} from '@appium/strongbox';
|
||
|
|
|
||
|
|
const WDA_LAUNCH_TIMEOUT = 60 * 1000;
|
||
|
|
const WDA_AGENT_PORT = 8100;
|
||
|
|
const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner';
|
||
|
|
const SHARED_RESOURCES_GUARD = new AsyncLock();
|
||
|
|
const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion';
|
||
|
|
|
||
|
|
export class WebDriverAgent {
|
||
|
|
/** @type {string} */
|
||
|
|
bootstrapPath;
|
||
|
|
|
||
|
|
/** @type {string} */
|
||
|
|
agentPath;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {import('appium-xcode').XcodeVersion} xcodeVersion
|
||
|
|
* // TODO: make args typed
|
||
|
|
* @param {import('@appium/types').StringRecord} [args={}]
|
||
|
|
* @param {import('@appium/types').AppiumLogger?} [log=null]
|
||
|
|
*/
|
||
|
|
constructor (xcodeVersion, args = {}, log = null) {
|
||
|
|
this.xcodeVersion = xcodeVersion;
|
||
|
|
|
||
|
|
this.args = _.clone(args);
|
||
|
|
this.log = log ?? defaultLogger;
|
||
|
|
|
||
|
|
this.device = args.device;
|
||
|
|
this.platformVersion = args.platformVersion;
|
||
|
|
this.platformName = args.platformName;
|
||
|
|
this.iosSdkVersion = args.iosSdkVersion;
|
||
|
|
this.host = args.host;
|
||
|
|
this.isRealDevice = !!args.realDevice;
|
||
|
|
this.idb = (args.device || {}).idb;
|
||
|
|
this.wdaBundlePath = args.wdaBundlePath;
|
||
|
|
|
||
|
|
this.setWDAPaths(args.bootstrapPath, args.agentPath);
|
||
|
|
|
||
|
|
this.wdaLocalPort = args.wdaLocalPort;
|
||
|
|
this.wdaRemotePort = ((this.isRealDevice ? args.wdaRemotePort : null) ?? args.wdaLocalPort)
|
||
|
|
|| WDA_AGENT_PORT;
|
||
|
|
this.wdaBaseUrl = args.wdaBaseUrl || WDA_BASE_URL;
|
||
|
|
|
||
|
|
this.prebuildWDA = args.prebuildWDA;
|
||
|
|
|
||
|
|
// this.args.webDriverAgentUrl guiarantees the capabilities acually
|
||
|
|
// gave 'appium:webDriverAgentUrl' but 'this.webDriverAgentUrl'
|
||
|
|
// could be used for caching WDA with xcodebuild.
|
||
|
|
this.webDriverAgentUrl = args.webDriverAgentUrl;
|
||
|
|
|
||
|
|
this.started = false;
|
||
|
|
|
||
|
|
this.wdaConnectionTimeout = args.wdaConnectionTimeout;
|
||
|
|
|
||
|
|
this.useXctestrunFile = args.useXctestrunFile;
|
||
|
|
this.usePrebuiltWDA = args.usePrebuiltWDA;
|
||
|
|
this.derivedDataPath = args.derivedDataPath;
|
||
|
|
this.mjpegServerPort = args.mjpegServerPort;
|
||
|
|
|
||
|
|
this.updatedWDABundleId = args.updatedWDABundleId;
|
||
|
|
|
||
|
|
this.wdaLaunchTimeout = args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT;
|
||
|
|
this.usePreinstalledWDA = args.usePreinstalledWDA;
|
||
|
|
this.xctestApiClient = null;
|
||
|
|
this.updatedWDABundleIdSuffix = args.updatedWDABundleIdSuffix ?? DEFAULT_TEST_BUNDLE_SUFFIX;
|
||
|
|
|
||
|
|
this.xcodebuild = this.canSkipXcodebuild
|
||
|
|
? null
|
||
|
|
: new XcodeBuild(this.xcodeVersion, this.device, {
|
||
|
|
platformVersion: this.platformVersion,
|
||
|
|
platformName: this.platformName,
|
||
|
|
iosSdkVersion: this.iosSdkVersion,
|
||
|
|
agentPath: this.agentPath,
|
||
|
|
bootstrapPath: this.bootstrapPath,
|
||
|
|
realDevice: this.isRealDevice,
|
||
|
|
showXcodeLog: args.showXcodeLog,
|
||
|
|
xcodeConfigFile: args.xcodeConfigFile,
|
||
|
|
xcodeOrgId: args.xcodeOrgId,
|
||
|
|
xcodeSigningId: args.xcodeSigningId,
|
||
|
|
keychainPath: args.keychainPath,
|
||
|
|
keychainPassword: args.keychainPassword,
|
||
|
|
useSimpleBuildTest: args.useSimpleBuildTest,
|
||
|
|
usePrebuiltWDA: args.usePrebuiltWDA,
|
||
|
|
updatedWDABundleId: this.updatedWDABundleId,
|
||
|
|
launchTimeout: this.wdaLaunchTimeout,
|
||
|
|
wdaRemotePort: this.wdaRemotePort,
|
||
|
|
useXctestrunFile: this.useXctestrunFile,
|
||
|
|
derivedDataPath: args.derivedDataPath,
|
||
|
|
mjpegServerPort: this.mjpegServerPort,
|
||
|
|
allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration,
|
||
|
|
resultBundlePath: args.resultBundlePath,
|
||
|
|
resultBundleVersion: args.resultBundleVersion,
|
||
|
|
}, this.log);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return true if the session does not need xcodebuild.
|
||
|
|
* @returns {boolean} Whether the session needs/has xcodebuild.
|
||
|
|
*/
|
||
|
|
get canSkipXcodebuild () {
|
||
|
|
// Use this.args.webDriverAgentUrl to guarantee
|
||
|
|
// the capabilities set gave the `appium:webDriverAgentUrl`.
|
||
|
|
return this.usePreinstalledWDA || this.args.webDriverAgentUrl;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return bundle id for WebDriverAgent to launch the WDA.
|
||
|
|
* The primary usage is with 'this.usePreinstalledWDA'.
|
||
|
|
* It adds `.xctrunner` as suffix by default but 'this.updatedWDABundleIdSuffix'
|
||
|
|
* lets skip it.
|
||
|
|
*
|
||
|
|
* @returns {string} Bundle ID for Xctest.
|
||
|
|
*/
|
||
|
|
get bundleIdForXctest () {
|
||
|
|
return `${this.updatedWDABundleId ? this.updatedWDABundleId : WDA_RUNNER_BUNDLE_ID}${this.updatedWDABundleIdSuffix}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {string} [bootstrapPath]
|
||
|
|
* @param {string} [agentPath]
|
||
|
|
*/
|
||
|
|
setWDAPaths (bootstrapPath, agentPath) {
|
||
|
|
// allow the user to specify a place for WDA. This is undocumented and
|
||
|
|
// only here for the purposes of testing development of WDA
|
||
|
|
this.bootstrapPath = bootstrapPath || BOOTSTRAP_PATH;
|
||
|
|
this.log.info(`Using WDA path: '${this.bootstrapPath}'`);
|
||
|
|
|
||
|
|
// for backward compatibility we need to be able to specify agentPath too
|
||
|
|
this.agentPath = agentPath || path.resolve(this.bootstrapPath, 'WebDriverAgent.xcodeproj');
|
||
|
|
this.log.info(`Using WDA agent: '${this.agentPath}'`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async cleanupObsoleteProcesses () {
|
||
|
|
const obsoletePids = await getPIDsListeningOnPort(/** @type {string} */ (this.url.port),
|
||
|
|
(cmdLine) => cmdLine.includes('/WebDriverAgentRunner') &&
|
||
|
|
!cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()));
|
||
|
|
|
||
|
|
if (_.isEmpty(obsoletePids)) {
|
||
|
|
this.log.debug(`No obsolete cached processes from previous WDA sessions ` +
|
||
|
|
`listening on port ${this.url.port} have been found`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.log.info(`Detected ${obsoletePids.length} obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} ` +
|
||
|
|
`from previous WDA sessions. Cleaning them up`);
|
||
|
|
try {
|
||
|
|
await exec('kill', obsoletePids);
|
||
|
|
} catch (e) {
|
||
|
|
this.log.warn(`Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` +
|
||
|
|
`Original error: ${e.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return boolean if WDA is running or not
|
||
|
|
* @return {Promise<boolean>} True if WDA is running
|
||
|
|
* @throws {Error} If there was invalid response code or body
|
||
|
|
*/
|
||
|
|
async isRunning () {
|
||
|
|
return !!(await this.getStatus());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {string}
|
||
|
|
*/
|
||
|
|
get basePath () {
|
||
|
|
if (this.url.path === '/') {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
return this.url.path || '';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return current running WDA's status like below
|
||
|
|
* {
|
||
|
|
* "state": "success",
|
||
|
|
* "os": {
|
||
|
|
* "name": "iOS",
|
||
|
|
* "version": "11.4",
|
||
|
|
* "sdkVersion": "11.3"
|
||
|
|
* },
|
||
|
|
* "ios": {
|
||
|
|
* "simulatorVersion": "11.4",
|
||
|
|
* "ip": "172.254.99.34"
|
||
|
|
* },
|
||
|
|
* "build": {
|
||
|
|
* "time": "Jun 24 2018 17:08:21",
|
||
|
|
* "productBundleIdentifier": "com.facebook.WebDriverAgentRunner"
|
||
|
|
* }
|
||
|
|
* }
|
||
|
|
*
|
||
|
|
* @param {number} [timeoutMs=0] If the given timeoutMs is zero or negative number,
|
||
|
|
* this function will return the response of `/status` immediately. If the given timeoutMs,
|
||
|
|
* this function will try to get the response of `/status` up to the timeoutMs.
|
||
|
|
* @return {Promise<import('@appium/types').StringRecord|null>} State Object
|
||
|
|
* @throws {Error} If there was an error within timeoutMs timeout.
|
||
|
|
* No error is raised if zero or negative number for the timeoutMs.
|
||
|
|
*/
|
||
|
|
async getStatus (timeoutMs = 0) {
|
||
|
|
const noSessionProxy = new NoSessionProxy({
|
||
|
|
server: this.url.hostname,
|
||
|
|
port: this.url.port,
|
||
|
|
base: this.basePath,
|
||
|
|
timeout: 3000,
|
||
|
|
});
|
||
|
|
|
||
|
|
const sendGetStatus = async () => await /** @type import('@appium/types').StringRecord */ (noSessionProxy.command('/status', 'GET'));
|
||
|
|
|
||
|
|
if (_.isNil(timeoutMs) || timeoutMs <= 0) {
|
||
|
|
try {
|
||
|
|
return await sendGetStatus();
|
||
|
|
} catch (err) {
|
||
|
|
this.log.debug(`WDA is not listening at '${this.url.href}'. Original error:: ${err.message}`);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let lastError = null;
|
||
|
|
let status = null;
|
||
|
|
try {
|
||
|
|
await waitForCondition(async () => {
|
||
|
|
try {
|
||
|
|
status = await sendGetStatus();
|
||
|
|
return true;
|
||
|
|
} catch (err) {
|
||
|
|
lastError = err;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}, {
|
||
|
|
waitMs: timeoutMs,
|
||
|
|
intervalMs: 300,
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
this.log.debug(`Failed to get the status endpoint in ${timeoutMs} ms. ` +
|
||
|
|
`The last error while accessing ${this.url.href}: ${lastError}. Original error:: ${err.message}.`);
|
||
|
|
throw new Error(`WDA was not ready in ${timeoutMs} ms.`);
|
||
|
|
}
|
||
|
|
return status;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Uninstall WDAs from the test device.
|
||
|
|
* Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA.
|
||
|
|
* Appium does not expect multiple WDAs are running on a device.
|
||
|
|
*
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async uninstall () {
|
||
|
|
try {
|
||
|
|
const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME);
|
||
|
|
if (_.isEmpty(bundleIds)) {
|
||
|
|
this.log.debug('No WDAs on the device.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.log.debug(`Uninstalling WDAs: '${bundleIds}'`);
|
||
|
|
for (const bundleId of bundleIds) {
|
||
|
|
await this.device.removeApp(bundleId);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
this.log.debug(e);
|
||
|
|
this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` +
|
||
|
|
`Original error: ${e.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async _cleanupProjectIfFresh () {
|
||
|
|
if (this.canSkipXcodebuild) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8'));
|
||
|
|
const box = strongbox(packageInfo.name);
|
||
|
|
let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME);
|
||
|
|
if (!boxItem) {
|
||
|
|
const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH);
|
||
|
|
if (await fs.exists(timestampPath)) {
|
||
|
|
// TODO: It is probably a bit ugly to hardcode the recent version string,
|
||
|
|
// TODO: hovewer it should do the job as a temporary transition trick
|
||
|
|
// TODO: to switch from a hardcoded file path to the strongbox usage.
|
||
|
|
try {
|
||
|
|
boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0');
|
||
|
|
} catch (e) {
|
||
|
|
this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
this.log.info('There is no need to perform the project cleanup. A fresh install has been detected');
|
||
|
|
try {
|
||
|
|
await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version);
|
||
|
|
} catch (e) {
|
||
|
|
this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let recentModuleVersion = await boxItem.read();
|
||
|
|
try {
|
||
|
|
recentModuleVersion = util.coerceVersion(recentModuleVersion, true);
|
||
|
|
} catch (e) {
|
||
|
|
this.log.warn(`The persisted module version string has been damaged: ${e.message}`);
|
||
|
|
this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`);
|
||
|
|
await boxItem.write(packageInfo.version);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) {
|
||
|
|
this.log.info(
|
||
|
|
`WebDriverAgent does not need a cleanup. The project sources are up to date ` +
|
||
|
|
`(${recentModuleVersion} >= ${packageInfo.version})`
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.log.info(
|
||
|
|
`Cleaning up the WebDriverAgent project after the module upgrade has happened ` +
|
||
|
|
`(${recentModuleVersion} < ${packageInfo.version})`
|
||
|
|
);
|
||
|
|
try {
|
||
|
|
// @ts-ignore xcodebuild should be set
|
||
|
|
await this.xcodebuild.cleanProject();
|
||
|
|
await boxItem.write(packageInfo.version);
|
||
|
|
} catch (e) {
|
||
|
|
this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Object} LaunchWdaViaDeviceCtlOptions
|
||
|
|
* @property {Record<string, string|number>} [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<void>}
|
||
|
|
*/
|
||
|
|
async _launchViaDevicectl(opts = {}) {
|
||
|
|
const {env} = opts;
|
||
|
|
|
||
|
|
await this.device.devicectl.launchApp(
|
||
|
|
this.bundleIdForXctest, { env, terminateExisting: true }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Launch WDA with preinstalled package without xcodebuild.
|
||
|
|
* @param {string} sessionId Launch WDA and establish the session with this sessionId
|
||
|
|
* @return {Promise<import('@appium/types').StringRecord|null>} State Object
|
||
|
|
* @throws {Error} If there was an error within timeoutMs timeout.
|
||
|
|
* No error is raised if zero or negative number for the timeoutMs.
|
||
|
|
*/
|
||
|
|
async launchWithPreinstalledWDA(sessionId) {
|
||
|
|
const xctestEnv = {
|
||
|
|
USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT,
|
||
|
|
WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest
|
||
|
|
};
|
||
|
|
if (this.mjpegServerPort) {
|
||
|
|
xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort;
|
||
|
|
}
|
||
|
|
this.log.info('Launching WebDriverAgent on the device without xcodebuild');
|
||
|
|
if (this.isRealDevice) {
|
||
|
|
// 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: [
|
||
|
|
'--terminate-running-process',
|
||
|
|
this.device.udid,
|
||
|
|
this.bundleIdForXctest,
|
||
|
|
],
|
||
|
|
env: xctestEnv,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this.setupProxies(sessionId);
|
||
|
|
let status;
|
||
|
|
try {
|
||
|
|
status = await this.getStatus(this.wdaLaunchTimeout);
|
||
|
|
} catch {
|
||
|
|
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. ` +
|
||
|
|
`The 'appium:wdaLaunchTimeout' capability modifies the timeout.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
this.started = true;
|
||
|
|
return status;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return current running WDA's status like below after launching WDA
|
||
|
|
* {
|
||
|
|
* "state": "success",
|
||
|
|
* "os": {
|
||
|
|
* "name": "iOS",
|
||
|
|
* "version": "11.4",
|
||
|
|
* "sdkVersion": "11.3"
|
||
|
|
* },
|
||
|
|
* "ios": {
|
||
|
|
* "simulatorVersion": "11.4",
|
||
|
|
* "ip": "172.254.99.34"
|
||
|
|
* },
|
||
|
|
* "build": {
|
||
|
|
* "time": "Jun 24 2018 17:08:21",
|
||
|
|
* "productBundleIdentifier": "com.facebook.WebDriverAgentRunner"
|
||
|
|
* }
|
||
|
|
* }
|
||
|
|
*
|
||
|
|
* @param {string} sessionId Launch WDA and establish the session with this sessionId
|
||
|
|
* @return {Promise<any?>} State Object
|
||
|
|
* @throws {Error} If there was invalid response code or body
|
||
|
|
*/
|
||
|
|
async launch (sessionId) {
|
||
|
|
if (this.webDriverAgentUrl) {
|
||
|
|
this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`);
|
||
|
|
this.url = this.webDriverAgentUrl;
|
||
|
|
this.setupProxies(sessionId);
|
||
|
|
return await this.getStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.usePreinstalledWDA) {
|
||
|
|
return await this.launchWithPreinstalledWDA(sessionId);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.log.info('Launching WebDriverAgent on the device');
|
||
|
|
|
||
|
|
this.setupProxies(sessionId);
|
||
|
|
|
||
|
|
if (!this.useXctestrunFile && !await fs.exists(this.agentPath)) {
|
||
|
|
throw new Error(`Trying to use WebDriverAgent project at '${this.agentPath}' but the ` +
|
||
|
|
'file does not exist');
|
||
|
|
}
|
||
|
|
|
||
|
|
// useXctestrunFile and usePrebuiltWDA use existing dependencies
|
||
|
|
// It depends on user side
|
||
|
|
if (this.idb || this.useXctestrunFile || this.usePrebuiltWDA) {
|
||
|
|
this.log.info('Skipped WDA project cleanup according to the provided capabilities');
|
||
|
|
} else {
|
||
|
|
const synchronizationKey = path.normalize(this.bootstrapPath);
|
||
|
|
await SHARED_RESOURCES_GUARD.acquire(synchronizationKey,
|
||
|
|
async () => await this._cleanupProjectIfFresh());
|
||
|
|
}
|
||
|
|
|
||
|
|
// We need to provide WDA local port, because it might be occupied
|
||
|
|
await resetTestProcesses(this.device.udid, !this.isRealDevice);
|
||
|
|
|
||
|
|
if (this.idb) {
|
||
|
|
return await this.startWithIDB();
|
||
|
|
}
|
||
|
|
|
||
|
|
// @ts-ignore xcodebuild should be set
|
||
|
|
await this.xcodebuild.init(this.noSessionProxy);
|
||
|
|
|
||
|
|
// Start the xcodebuild process
|
||
|
|
if (this.prebuildWDA) {
|
||
|
|
// @ts-ignore xcodebuild should be set
|
||
|
|
await this.xcodebuild.prebuild();
|
||
|
|
}
|
||
|
|
// @ts-ignore xcodebuild should be set
|
||
|
|
return await this.xcodebuild.start();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async startWithIDB () {
|
||
|
|
this.log.info('Will launch WDA with idb instead of xcodebuild since the corresponding flag is enabled');
|
||
|
|
const {wdaBundleId, testBundleId} = await this.prepareWDA();
|
||
|
|
const env = {
|
||
|
|
USE_PORT: this.wdaRemotePort,
|
||
|
|
WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest,
|
||
|
|
};
|
||
|
|
if (this.mjpegServerPort) {
|
||
|
|
env.MJPEG_SERVER_PORT = this.mjpegServerPort;
|
||
|
|
}
|
||
|
|
|
||
|
|
return await this.idb.runXCUITest(wdaBundleId, wdaBundleId, testBundleId, {env});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {string} wdaBundlePath
|
||
|
|
* @returns {Promise<string>}
|
||
|
|
*/
|
||
|
|
async parseBundleId (wdaBundlePath) {
|
||
|
|
const infoPlistPath = path.join(wdaBundlePath, 'Info.plist');
|
||
|
|
const infoPlist = await plist.parsePlist(await fs.readFile(infoPlistPath));
|
||
|
|
if (!infoPlist.CFBundleIdentifier) {
|
||
|
|
throw new Error(`Could not find bundle id in '${infoPlistPath}'`);
|
||
|
|
}
|
||
|
|
return infoPlist.CFBundleIdentifier;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {Promise<{wdaBundleId: string, testBundleId: string, wdaBundlePath: string}>}
|
||
|
|
*/
|
||
|
|
async prepareWDA () {
|
||
|
|
const wdaBundlePath = this.wdaBundlePath || await this.fetchWDABundle();
|
||
|
|
const wdaBundleId = await this.parseBundleId(wdaBundlePath);
|
||
|
|
if (!await this.device.isAppInstalled(wdaBundleId)) {
|
||
|
|
await this.device.installApp(wdaBundlePath);
|
||
|
|
}
|
||
|
|
const testBundleId = await this.idb.installXCTestBundle(path.join(wdaBundlePath, 'PlugIns', 'WebDriverAgentRunner.xctest'));
|
||
|
|
return {wdaBundleId, testBundleId, wdaBundlePath};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {Promise<string>}
|
||
|
|
*/
|
||
|
|
async fetchWDABundle () {
|
||
|
|
if (!this.derivedDataPath) {
|
||
|
|
return await bundleWDASim(/** @type {XcodeBuild} */ (this.xcodebuild));
|
||
|
|
}
|
||
|
|
const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, {
|
||
|
|
absolute: true,
|
||
|
|
});
|
||
|
|
if (_.isEmpty(wdaBundlePaths)) {
|
||
|
|
throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`);
|
||
|
|
}
|
||
|
|
return wdaBundlePaths[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {Promise<boolean>}
|
||
|
|
*/
|
||
|
|
async isSourceFresh () {
|
||
|
|
const existsPromises = [
|
||
|
|
'Resources',
|
||
|
|
`Resources${path.sep}WebDriverAgent.bundle`,
|
||
|
|
].map((subPath) => fs.exists(path.resolve(/** @type {String} */ (this.bootstrapPath), subPath)));
|
||
|
|
return (await B.all(existsPromises)).some((v) => v === false);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {string} sessionId
|
||
|
|
* @returns {void}
|
||
|
|
*/
|
||
|
|
setupProxies (sessionId) {
|
||
|
|
const proxyOpts = {
|
||
|
|
log: this.log,
|
||
|
|
server: this.url.hostname ?? undefined,
|
||
|
|
port: parseInt(this.url.port ?? '', 10) || undefined,
|
||
|
|
base: this.basePath,
|
||
|
|
timeout: this.wdaConnectionTimeout,
|
||
|
|
keepAlive: true,
|
||
|
|
scheme: this.url.protocol ? this.url.protocol.replace(':', '') : 'http',
|
||
|
|
};
|
||
|
|
if (this.args.reqBasePath) {
|
||
|
|
proxyOpts.reqBasePath = this.args.reqBasePath;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.jwproxy = new JWProxy(proxyOpts);
|
||
|
|
this.jwproxy.sessionId = sessionId;
|
||
|
|
this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy);
|
||
|
|
|
||
|
|
this.noSessionProxy = new NoSessionProxy(proxyOpts);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async quit () {
|
||
|
|
if (this.usePreinstalledWDA) {
|
||
|
|
this.log.info('Stopping the XCTest session');
|
||
|
|
if (this.xctestApiClient) {
|
||
|
|
this.xctestApiClient.stop();
|
||
|
|
this.xctestApiClient = null;
|
||
|
|
} else {
|
||
|
|
try {
|
||
|
|
await this.device.simctl.terminateApp(this.bundleIdForXctest);
|
||
|
|
} catch (e) {
|
||
|
|
this.log.warn(e.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (!this.args.webDriverAgentUrl) {
|
||
|
|
this.log.info('Shutting down sub-processes');
|
||
|
|
await this.xcodebuild?.quit();
|
||
|
|
await this.xcodebuild?.reset();
|
||
|
|
} else {
|
||
|
|
this.log.debug('Do not stop xcodebuild nor XCTest session ' +
|
||
|
|
'since the WDA session is managed by outside this driver.');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.jwproxy) {
|
||
|
|
this.jwproxy.sessionId = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.started = false;
|
||
|
|
|
||
|
|
if (!this.args.webDriverAgentUrl) {
|
||
|
|
// if we populated the url ourselves (during `setupCaching` call, for instance)
|
||
|
|
// then clean that up. If the url was supplied, we want to keep it
|
||
|
|
this.webDriverAgentUrl = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {import('url').UrlWithStringQuery}
|
||
|
|
*/
|
||
|
|
get url () {
|
||
|
|
if (!this._url) {
|
||
|
|
if (this.webDriverAgentUrl) {
|
||
|
|
this._url = url.parse(this.webDriverAgentUrl);
|
||
|
|
} else {
|
||
|
|
const port = this.wdaLocalPort || WDA_AGENT_PORT;
|
||
|
|
const {protocol, hostname} = url.parse(this.wdaBaseUrl || WDA_BASE_URL);
|
||
|
|
this._url = url.parse(`${protocol}//${hostname}:${port}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return this._url;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {string} _url
|
||
|
|
* @returns {void}
|
||
|
|
*/
|
||
|
|
set url (_url) {
|
||
|
|
this._url = url.parse(_url);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
get fullyStarted () {
|
||
|
|
return this.started;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {boolean} started
|
||
|
|
* @returns {void}s
|
||
|
|
*/
|
||
|
|
set fullyStarted (started) {
|
||
|
|
this.started = started ?? false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {Promise<string|undefined>}
|
||
|
|
*/
|
||
|
|
async retrieveDerivedDataPath () {
|
||
|
|
if (this.canSkipXcodebuild) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
return await /** @type {XcodeBuild} */ (this.xcodebuild).retrieveDerivedDataPath();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reuse running WDA if it has the same bundle id with updatedWDABundleId.
|
||
|
|
* Or reuse it if it has the default id without updatedWDABundleId.
|
||
|
|
* Uninstall it if the method faces an exception for the above situation.
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async setupCaching () {
|
||
|
|
const status = await this.getStatus();
|
||
|
|
if (!status || !status.build) {
|
||
|
|
this.log.debug('WDA is currently not running. There is nothing to cache');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const {
|
||
|
|
productBundleIdentifier,
|
||
|
|
upgradedAt,
|
||
|
|
} = status.build;
|
||
|
|
// for real device
|
||
|
|
if (util.hasValue(productBundleIdentifier) && util.hasValue(this.updatedWDABundleId) && this.updatedWDABundleId !== productBundleIdentifier) {
|
||
|
|
this.log.info(`Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`);
|
||
|
|
return await this.uninstall();
|
||
|
|
}
|
||
|
|
// for simulator
|
||
|
|
if (util.hasValue(productBundleIdentifier) && !util.hasValue(this.updatedWDABundleId) && WDA_RUNNER_BUNDLE_ID !== productBundleIdentifier) {
|
||
|
|
this.log.info(`Will uninstall running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`);
|
||
|
|
return await this.uninstall();
|
||
|
|
}
|
||
|
|
|
||
|
|
const actualUpgradeTimestamp = await getWDAUpgradeTimestamp();
|
||
|
|
this.log.debug(`Upgrade timestamp of the currently bundled WDA: ${actualUpgradeTimestamp}`);
|
||
|
|
this.log.debug(`Upgrade timestamp of the WDA on the device: ${upgradedAt}`);
|
||
|
|
if (actualUpgradeTimestamp && upgradedAt && _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`)) {
|
||
|
|
this.log.info('Will uninstall running WDA since it has different version in comparison to the one ' +
|
||
|
|
`which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`);
|
||
|
|
return await this.uninstall();
|
||
|
|
}
|
||
|
|
|
||
|
|
const message = util.hasValue(productBundleIdentifier)
|
||
|
|
? `Will reuse previously cached WDA instance at '${this.url.href}' with '${productBundleIdentifier}'`
|
||
|
|
: `Will reuse previously cached WDA instance at '${this.url.href}'`;
|
||
|
|
this.log.info(`${message}. Set the wdaLocalPort capability to a value different from ${this.url.port} if this is an undesired behavior.`);
|
||
|
|
this.webDriverAgentUrl = this.url.href;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Quit and uninstall running WDA.
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async quitAndUninstall () {
|
||
|
|
await this.quit();
|
||
|
|
await this.uninstall();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default WebDriverAgent;
|