webdriveragent.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. import _ from 'lodash';
  2. import path from 'path';
  3. import url from 'url';
  4. import B from 'bluebird';
  5. import { JWProxy } from '@appium/base-driver';
  6. import { fs, util, plist } from '@appium/support';
  7. import defaultLogger from './logger';
  8. import { NoSessionProxy } from './no-session-proxy';
  9. import {
  10. getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort, BOOTSTRAP_PATH
  11. } from './utils';
  12. import XcodeBuild from './xcodebuild';
  13. import AsyncLock from 'async-lock';
  14. import { exec } from 'teen_process';
  15. import { bundleWDASim } from './check-dependencies';
  16. import {
  17. WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_BUNDLE_ID_FOR_XCTEST, WDA_RUNNER_APP,
  18. WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH
  19. } from './constants';
  20. import {Xctest} from 'appium-ios-device';
  21. import {strongbox} from '@appium/strongbox';
  22. const WDA_LAUNCH_TIMEOUT = 60 * 1000;
  23. const WDA_AGENT_PORT = 8100;
  24. const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner';
  25. const SHARED_RESOURCES_GUARD = new AsyncLock();
  26. const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion';
  27. class WebDriverAgent {
  28. constructor (xcodeVersion, args = {}, log = null) {
  29. this.xcodeVersion = xcodeVersion;
  30. this.args = _.clone(args);
  31. this.log = log ?? defaultLogger;
  32. this.device = args.device;
  33. this.platformVersion = args.platformVersion;
  34. this.platformName = args.platformName;
  35. this.iosSdkVersion = args.iosSdkVersion;
  36. this.host = args.host;
  37. this.isRealDevice = !!args.realDevice;
  38. this.idb = (args.device || {}).idb;
  39. this.wdaBundlePath = args.wdaBundlePath;
  40. this.setWDAPaths(args.bootstrapPath, args.agentPath);
  41. this.wdaLocalPort = args.wdaLocalPort;
  42. this.wdaRemotePort = args.wdaLocalPort || WDA_AGENT_PORT;
  43. this.wdaBaseUrl = args.wdaBaseUrl || WDA_BASE_URL;
  44. this.prebuildWDA = args.prebuildWDA;
  45. // this.args.webDriverAgentUrl guiarantees the capabilities acually
  46. // gave 'appium:webDriverAgentUrl' but 'this.webDriverAgentUrl'
  47. // could be used for caching WDA with xcodebuild.
  48. this.webDriverAgentUrl = args.webDriverAgentUrl;
  49. this.started = false;
  50. this.wdaConnectionTimeout = args.wdaConnectionTimeout;
  51. this.useXctestrunFile = args.useXctestrunFile;
  52. this.usePrebuiltWDA = args.usePrebuiltWDA;
  53. this.derivedDataPath = args.derivedDataPath;
  54. this.mjpegServerPort = args.mjpegServerPort;
  55. this.updatedWDABundleId = args.updatedWDABundleId;
  56. this.usePreinstalledWDA = args.usePreinstalledWDA;
  57. this.xctestApiClient = null;
  58. this.xcodebuild = this.canSkipXcodebuild
  59. ? null
  60. : new XcodeBuild(this.xcodeVersion, this.device, {
  61. platformVersion: this.platformVersion,
  62. platformName: this.platformName,
  63. iosSdkVersion: this.iosSdkVersion,
  64. agentPath: this.agentPath,
  65. bootstrapPath: this.bootstrapPath,
  66. realDevice: this.isRealDevice,
  67. showXcodeLog: args.showXcodeLog,
  68. xcodeConfigFile: args.xcodeConfigFile,
  69. xcodeOrgId: args.xcodeOrgId,
  70. xcodeSigningId: args.xcodeSigningId,
  71. keychainPath: args.keychainPath,
  72. keychainPassword: args.keychainPassword,
  73. useSimpleBuildTest: args.useSimpleBuildTest,
  74. usePrebuiltWDA: args.usePrebuiltWDA,
  75. updatedWDABundleId: this.updatedWDABundleId,
  76. launchTimeout: args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT,
  77. wdaRemotePort: this.wdaRemotePort,
  78. useXctestrunFile: this.useXctestrunFile,
  79. derivedDataPath: args.derivedDataPath,
  80. mjpegServerPort: this.mjpegServerPort,
  81. allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration,
  82. resultBundlePath: args.resultBundlePath,
  83. resultBundleVersion: args.resultBundleVersion,
  84. }, this.log);
  85. }
  86. /**
  87. * Return true if the session does not need xcodebuild.
  88. * @returns {boolean} Whether the session needs/has xcodebuild.
  89. */
  90. get canSkipXcodebuild () {
  91. // Use this.args.webDriverAgentUrl to guarantee
  92. // the capabilities set gave the `appium:webDriverAgentUrl`.
  93. return this.usePreinstalledWDA || this.args.webDriverAgentUrl;
  94. }
  95. /**
  96. *
  97. * @returns {string} Bundle ID for Xctest.
  98. */
  99. get bundleIdForXctest () {
  100. return this.updatedWDABundleId ? `${this.updatedWDABundleId}.xctrunner` : WDA_RUNNER_BUNDLE_ID_FOR_XCTEST;
  101. }
  102. setWDAPaths (bootstrapPath, agentPath) {
  103. // allow the user to specify a place for WDA. This is undocumented and
  104. // only here for the purposes of testing development of WDA
  105. this.bootstrapPath = bootstrapPath || BOOTSTRAP_PATH;
  106. this.log.info(`Using WDA path: '${this.bootstrapPath}'`);
  107. // for backward compatibility we need to be able to specify agentPath too
  108. this.agentPath = agentPath || path.resolve(this.bootstrapPath, 'WebDriverAgent.xcodeproj');
  109. this.log.info(`Using WDA agent: '${this.agentPath}'`);
  110. }
  111. async cleanupObsoleteProcesses () {
  112. const obsoletePids = await getPIDsListeningOnPort(this.url.port,
  113. (cmdLine) => cmdLine.includes('/WebDriverAgentRunner') &&
  114. !cmdLine.toLowerCase().includes(this.device.udid.toLowerCase()));
  115. if (_.isEmpty(obsoletePids)) {
  116. this.log.debug(`No obsolete cached processes from previous WDA sessions ` +
  117. `listening on port ${this.url.port} have been found`);
  118. return;
  119. }
  120. this.log.info(`Detected ${obsoletePids.length} obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} ` +
  121. `from previous WDA sessions. Cleaning them up`);
  122. try {
  123. await exec('kill', obsoletePids);
  124. } catch (e) {
  125. this.log.warn(`Failed to kill obsolete cached process${obsoletePids.length === 1 ? '' : 'es'} '${obsoletePids}'. ` +
  126. `Original error: ${e.message}`);
  127. }
  128. }
  129. /**
  130. * Return boolean if WDA is running or not
  131. * @return {Promise<boolean>} True if WDA is running
  132. * @throws {Error} If there was invalid response code or body
  133. */
  134. async isRunning () {
  135. return !!(await this.getStatus());
  136. }
  137. get basePath () {
  138. if (this.url.path === '/') {
  139. return '';
  140. }
  141. return this.url.path || '';
  142. }
  143. /**
  144. * Return current running WDA's status like below
  145. * {
  146. * "state": "success",
  147. * "os": {
  148. * "name": "iOS",
  149. * "version": "11.4",
  150. * "sdkVersion": "11.3"
  151. * },
  152. * "ios": {
  153. * "simulatorVersion": "11.4",
  154. * "ip": "172.254.99.34"
  155. * },
  156. * "build": {
  157. * "time": "Jun 24 2018 17:08:21",
  158. * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner"
  159. * }
  160. * }
  161. *
  162. * @return {Promise<any?>} State Object
  163. * @throws {Error} If there was invalid response code or body
  164. */
  165. async getStatus () {
  166. const noSessionProxy = new NoSessionProxy({
  167. server: this.url.hostname,
  168. port: this.url.port,
  169. base: this.basePath,
  170. timeout: 3000,
  171. });
  172. try {
  173. return await noSessionProxy.command('/status', 'GET');
  174. } catch (err) {
  175. this.log.debug(`WDA is not listening at '${this.url.href}'`);
  176. return null;
  177. }
  178. }
  179. /**
  180. * Uninstall WDAs from the test device.
  181. * Over Xcode 11, multiple WDA can be in the device since Xcode 11 generates different WDA.
  182. * Appium does not expect multiple WDAs are running on a device.
  183. */
  184. async uninstall () {
  185. try {
  186. const bundleIds = await this.device.getUserInstalledBundleIdsByBundleName(WDA_CF_BUNDLE_NAME);
  187. if (_.isEmpty(bundleIds)) {
  188. this.log.debug('No WDAs on the device.');
  189. return;
  190. }
  191. this.log.debug(`Uninstalling WDAs: '${bundleIds}'`);
  192. for (const bundleId of bundleIds) {
  193. await this.device.removeApp(bundleId);
  194. }
  195. } catch (e) {
  196. this.log.debug(e);
  197. this.log.warn(`WebDriverAgent uninstall failed. Perhaps, it is already uninstalled? ` +
  198. `Original error: ${e.message}`);
  199. }
  200. }
  201. async _cleanupProjectIfFresh () {
  202. if (this.canSkipXcodebuild) {
  203. return;
  204. }
  205. const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8'));
  206. const box = strongbox(packageInfo.name);
  207. let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME);
  208. if (!boxItem) {
  209. const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH);
  210. if (await fs.exists(timestampPath)) {
  211. // TODO: It is probably a bit ugly to hardcode the recent version string,
  212. // TODO: hovewer it should do the job as a temporary transition trick
  213. // TODO: to switch from a hardcoded file path to the strongbox usage.
  214. try {
  215. boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0');
  216. } catch (e) {
  217. this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
  218. return;
  219. }
  220. } else {
  221. this.log.info('There is no need to perform the project cleanup. A fresh install has been detected');
  222. try {
  223. await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version);
  224. } catch (e) {
  225. this.log.warn(`The actual module version cannot be persisted: ${e.message}`);
  226. }
  227. return;
  228. }
  229. }
  230. let recentModuleVersion = await boxItem.read();
  231. try {
  232. recentModuleVersion = util.coerceVersion(recentModuleVersion, true);
  233. } catch (e) {
  234. this.log.warn(`The persisted module version string has been damaged: ${e.message}`);
  235. this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`);
  236. await boxItem.write(packageInfo.version);
  237. return;
  238. }
  239. if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) {
  240. this.log.info(
  241. `WebDriverAgent does not need a cleanup. The project sources are up to date ` +
  242. `(${recentModuleVersion} >= ${packageInfo.version})`
  243. );
  244. return;
  245. }
  246. this.log.info(
  247. `Cleaning up the WebDriverAgent project after the module upgrade has happened ` +
  248. `(${recentModuleVersion} < ${packageInfo.version})`
  249. );
  250. try {
  251. // @ts-ignore xcodebuild should be set
  252. await this.xcodebuild.cleanProject();
  253. await boxItem.write(packageInfo.version);
  254. } catch (e) {
  255. this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`);
  256. }
  257. }
  258. /**
  259. * Launch WDA with preinstalled package without xcodebuild.
  260. * @param {string} sessionId Launch WDA and establish the session with this sessionId
  261. * @return {Promise<any?>} State Object
  262. */
  263. async launchWithPreinstalledWDA(sessionId) {
  264. const xctestEnv = {
  265. USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT,
  266. WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest
  267. };
  268. if (this.mjpegServerPort) {
  269. xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort;
  270. }
  271. this.log.info('Launching WebDriverAgent on the device without xcodebuild');
  272. this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv});
  273. await this.xctestApiClient.start();
  274. this.setupProxies(sessionId);
  275. const status = await this.getStatus();
  276. this.started = true;
  277. return status;
  278. }
  279. /**
  280. * Return current running WDA's status like below after launching WDA
  281. * {
  282. * "state": "success",
  283. * "os": {
  284. * "name": "iOS",
  285. * "version": "11.4",
  286. * "sdkVersion": "11.3"
  287. * },
  288. * "ios": {
  289. * "simulatorVersion": "11.4",
  290. * "ip": "172.254.99.34"
  291. * },
  292. * "build": {
  293. * "time": "Jun 24 2018 17:08:21",
  294. * "productBundleIdentifier": "com.facebook.WebDriverAgentRunner"
  295. * }
  296. * }
  297. *
  298. * @param {string} sessionId Launch WDA and establish the session with this sessionId
  299. * @return {Promise<any?>} State Object
  300. * @throws {Error} If there was invalid response code or body
  301. */
  302. async launch (sessionId) {
  303. if (this.webDriverAgentUrl) {
  304. this.log.info(`Using provided WebdriverAgent at '${this.webDriverAgentUrl}'`);
  305. this.url = this.webDriverAgentUrl;
  306. this.setupProxies(sessionId);
  307. return await this.getStatus();
  308. }
  309. if (this.usePreinstalledWDA) {
  310. if (this.isRealDevice) {
  311. return await this.launchWithPreinstalledWDA(sessionId);
  312. }
  313. throw new Error('usePreinstalledWDA is available only for a real device.');
  314. }
  315. this.log.info('Launching WebDriverAgent on the device');
  316. this.setupProxies(sessionId);
  317. if (!this.useXctestrunFile && !await fs.exists(this.agentPath)) {
  318. throw new Error(`Trying to use WebDriverAgent project at '${this.agentPath}' but the ` +
  319. 'file does not exist');
  320. }
  321. // useXctestrunFile and usePrebuiltWDA use existing dependencies
  322. // It depends on user side
  323. if (this.idb || this.useXctestrunFile || (this.derivedDataPath && this.usePrebuiltWDA)) {
  324. this.log.info('Skipped WDA project cleanup according to the provided capabilities');
  325. } else {
  326. const synchronizationKey = path.normalize(this.bootstrapPath);
  327. await SHARED_RESOURCES_GUARD.acquire(synchronizationKey,
  328. async () => await this._cleanupProjectIfFresh());
  329. }
  330. // We need to provide WDA local port, because it might be occupied
  331. await resetTestProcesses(this.device.udid, !this.isRealDevice);
  332. if (this.idb) {
  333. return await this.startWithIDB();
  334. }
  335. // @ts-ignore xcodebuild should be set
  336. await this.xcodebuild.init(this.noSessionProxy);
  337. // Start the xcodebuild process
  338. if (this.prebuildWDA) {
  339. // @ts-ignore xcodebuild should be set
  340. await this.xcodebuild.prebuild();
  341. }
  342. // @ts-ignore xcodebuild should be set
  343. return await this.xcodebuild.start();
  344. }
  345. async startWithIDB () {
  346. this.log.info('Will launch WDA with idb instead of xcodebuild since the corresponding flag is enabled');
  347. const {wdaBundleId, testBundleId} = await this.prepareWDA();
  348. const env = {
  349. USE_PORT: this.wdaRemotePort,
  350. WDA_PRODUCT_BUNDLE_IDENTIFIER: this.updatedWDABundleId,
  351. };
  352. if (this.mjpegServerPort) {
  353. env.MJPEG_SERVER_PORT = this.mjpegServerPort;
  354. }
  355. return await this.idb.runXCUITest(wdaBundleId, wdaBundleId, testBundleId, {env});
  356. }
  357. async parseBundleId (wdaBundlePath) {
  358. const infoPlistPath = path.join(wdaBundlePath, 'Info.plist');
  359. const infoPlist = await plist.parsePlist(await fs.readFile(infoPlistPath));
  360. if (!infoPlist.CFBundleIdentifier) {
  361. throw new Error(`Could not find bundle id in '${infoPlistPath}'`);
  362. }
  363. return infoPlist.CFBundleIdentifier;
  364. }
  365. async prepareWDA () {
  366. const wdaBundlePath = this.wdaBundlePath || await this.fetchWDABundle();
  367. const wdaBundleId = await this.parseBundleId(wdaBundlePath);
  368. if (!await this.device.isAppInstalled(wdaBundleId)) {
  369. await this.device.installApp(wdaBundlePath);
  370. }
  371. const testBundleId = await this.idb.installXCTestBundle(path.join(wdaBundlePath, 'PlugIns', 'WebDriverAgentRunner.xctest'));
  372. return {wdaBundleId, testBundleId, wdaBundlePath};
  373. }
  374. async fetchWDABundle () {
  375. if (!this.derivedDataPath) {
  376. return await bundleWDASim(this.xcodebuild);
  377. }
  378. const wdaBundlePaths = await fs.glob(`${this.derivedDataPath}/**/*${WDA_RUNNER_APP}/`, {
  379. absolute: true,
  380. });
  381. if (_.isEmpty(wdaBundlePaths)) {
  382. throw new Error(`Could not find the WDA bundle in '${this.derivedDataPath}'`);
  383. }
  384. return wdaBundlePaths[0];
  385. }
  386. async isSourceFresh () {
  387. const existsPromises = [
  388. 'Resources',
  389. `Resources${path.sep}WebDriverAgent.bundle`,
  390. ].map((subPath) => fs.exists(path.resolve(this.bootstrapPath, subPath)));
  391. return (await B.all(existsPromises)).some((v) => v === false);
  392. }
  393. setupProxies (sessionId) {
  394. const proxyOpts = {
  395. log: this.log,
  396. server: this.url.hostname,
  397. port: this.url.port,
  398. base: this.basePath,
  399. timeout: this.wdaConnectionTimeout,
  400. keepAlive: true,
  401. };
  402. this.jwproxy = new JWProxy(proxyOpts);
  403. this.jwproxy.sessionId = sessionId;
  404. this.proxyReqRes = this.jwproxy.proxyReqRes.bind(this.jwproxy);
  405. this.noSessionProxy = new NoSessionProxy(proxyOpts);
  406. }
  407. async quit () {
  408. if (this.usePreinstalledWDA) {
  409. if (this.xctestApiClient) {
  410. this.log.info('Stopping the XCTest session');
  411. this.xctestApiClient.stop();
  412. this.xctestApiClient = null;
  413. }
  414. } else if (!this.args.webDriverAgentUrl) {
  415. this.log.info('Shutting down sub-processes');
  416. await this.xcodebuild?.quit();
  417. await this.xcodebuild?.reset();
  418. } else {
  419. this.log.debug('Do not stop xcodebuild nor XCTest session ' +
  420. 'since the WDA session is managed by outside this driver.');
  421. }
  422. if (this.jwproxy) {
  423. this.jwproxy.sessionId = null;
  424. }
  425. this.started = false;
  426. if (!this.args.webDriverAgentUrl) {
  427. // if we populated the url ourselves (during `setupCaching` call, for instance)
  428. // then clean that up. If the url was supplied, we want to keep it
  429. this.webDriverAgentUrl = null;
  430. }
  431. }
  432. get url () {
  433. if (!this._url) {
  434. if (this.webDriverAgentUrl) {
  435. this._url = url.parse(this.webDriverAgentUrl);
  436. } else {
  437. const port = this.wdaLocalPort || WDA_AGENT_PORT;
  438. const {protocol, hostname} = url.parse(this.wdaBaseUrl || WDA_BASE_URL);
  439. this._url = url.parse(`${protocol}//${hostname}:${port}`);
  440. }
  441. }
  442. return this._url;
  443. }
  444. set url (_url) {
  445. this._url = url.parse(_url);
  446. }
  447. get fullyStarted () {
  448. return this.started;
  449. }
  450. set fullyStarted (started) {
  451. this.started = started ?? false;
  452. }
  453. async retrieveDerivedDataPath () {
  454. if (this.canSkipXcodebuild) {
  455. return;
  456. }
  457. // @ts-ignore xcodebuild should be set
  458. return await this.xcodebuild.retrieveDerivedDataPath();
  459. }
  460. /**
  461. * Reuse running WDA if it has the same bundle id with updatedWDABundleId.
  462. * Or reuse it if it has the default id without updatedWDABundleId.
  463. * Uninstall it if the method faces an exception for the above situation.
  464. */
  465. async setupCaching () {
  466. const status = await this.getStatus();
  467. if (!status || !status.build) {
  468. this.log.debug('WDA is currently not running. There is nothing to cache');
  469. return;
  470. }
  471. const {
  472. productBundleIdentifier,
  473. upgradedAt,
  474. } = status.build;
  475. // for real device
  476. if (util.hasValue(productBundleIdentifier) && util.hasValue(this.updatedWDABundleId) && this.updatedWDABundleId !== productBundleIdentifier) {
  477. this.log.info(`Will uninstall running WDA since it has different bundle id. The actual value is '${productBundleIdentifier}'.`);
  478. return await this.uninstall();
  479. }
  480. // for simulator
  481. if (util.hasValue(productBundleIdentifier) && !util.hasValue(this.updatedWDABundleId) && WDA_RUNNER_BUNDLE_ID !== productBundleIdentifier) {
  482. this.log.info(`Will uninstall running WDA since its bundle id is not equal to the default value ${WDA_RUNNER_BUNDLE_ID}`);
  483. return await this.uninstall();
  484. }
  485. const actualUpgradeTimestamp = await getWDAUpgradeTimestamp();
  486. this.log.debug(`Upgrade timestamp of the currently bundled WDA: ${actualUpgradeTimestamp}`);
  487. this.log.debug(`Upgrade timestamp of the WDA on the device: ${upgradedAt}`);
  488. if (actualUpgradeTimestamp && upgradedAt && _.toLower(`${actualUpgradeTimestamp}`) !== _.toLower(`${upgradedAt}`)) {
  489. this.log.info('Will uninstall running WDA since it has different version in comparison to the one ' +
  490. `which is bundled with appium-xcuitest-driver module (${actualUpgradeTimestamp} != ${upgradedAt})`);
  491. return await this.uninstall();
  492. }
  493. const message = util.hasValue(productBundleIdentifier)
  494. ? `Will reuse previously cached WDA instance at '${this.url.href}' with '${productBundleIdentifier}'`
  495. : `Will reuse previously cached WDA instance at '${this.url.href}'`;
  496. this.log.info(`${message}. Set the wdaLocalPort capability to a value different from ${this.url.port} if this is an undesired behavior.`);
  497. this.webDriverAgentUrl = this.url.href;
  498. }
  499. /**
  500. * Quit and uninstall running WDA.
  501. */
  502. async quitAndUninstall () {
  503. await this.quit();
  504. await this.uninstall();
  505. }
  506. }
  507. export default WebDriverAgent;
  508. export { WebDriverAgent };