UNPKG

21.4 kBJavaScriptView Raw
1import _ from 'lodash';
2import log from './logger';
3import { getBuildInfo, updateBuildInfo, APPIUM_VER } from './config';
4import { BaseDriver, errors, isSessionCommand } from 'appium-base-driver';
5import B from 'bluebird';
6import AsyncLock from 'async-lock';
7import { parseCapsForInnerDriver, getPackageVersion, pullSettings } from './utils';
8import semver from 'semver';
9import wrap from 'word-wrap';
10import { EOL } from 'os';
11import { util } from 'appium-support';
12
13
14const PLATFORMS = {
15 FAKE: 'fake',
16 ANDROID: 'android',
17 IOS: 'ios',
18 APPLE_TVOS: 'tvos',
19 WINDOWS: 'windows',
20 MAC: 'mac',
21 TIZEN: 'tizen',
22 LINUX: 'linux',
23 ROKU: 'roku',
24 WEBOS: 'webos'
25};
26
27const AUTOMATION_NAMES = {
28 APPIUM: 'Appium',
29 UIAUTOMATOR2: 'UiAutomator2',
30 UIAUTOMATOR1: 'UiAutomator1',
31 XCUITEST: 'XCUITest',
32 YOUIENGINE: 'YouiEngine',
33 ESPRESSO: 'Espresso',
34 TIZEN: 'Tizen',
35 FAKE: 'Fake',
36 INSTRUMENTS: 'Instruments',
37 WINDOWS: 'Windows',
38 MAC: 'Mac',
39 MAC2: 'Mac2',
40 FLUTTER: 'Flutter',
41 SAFARI: 'Safari',
42 GECKO: 'Gecko',
43 ROKU: 'Roku',
44 WEBOS: 'WebOS'
45};
46const DRIVER_MAP = {
47 [AUTOMATION_NAMES.UIAUTOMATOR2.toLowerCase()]: {
48 driverClassName: 'AndroidUiautomator2Driver',
49 driverPackage: 'appium-uiautomator2-driver',
50 },
51 [AUTOMATION_NAMES.XCUITEST.toLowerCase()]: {
52 driverClassName: 'XCUITestDriver',
53 driverPackage: 'appium-xcuitest-driver',
54 },
55 [AUTOMATION_NAMES.YOUIENGINE.toLowerCase()]: {
56 driverClassName: 'YouiEngineDriver',
57 driverPackage: 'appium-youiengine-driver',
58 },
59 [AUTOMATION_NAMES.FAKE.toLowerCase()]: {
60 driverClassName: 'FakeDriver',
61 driverPackage: 'appium-fake-driver',
62 },
63 [AUTOMATION_NAMES.UIAUTOMATOR1.toLowerCase()]: {
64 driverClassName: 'AndroidDriver',
65 driverPackage: 'appium-android-driver',
66 },
67 [AUTOMATION_NAMES.INSTRUMENTS.toLowerCase()]: {
68 driverClassName: 'IosDriver',
69 driverPackage: 'appium-ios-driver',
70 },
71 [AUTOMATION_NAMES.WINDOWS.toLowerCase()]: {
72 driverClassName: 'WindowsDriver',
73 driverPackage: 'appium-windows-driver',
74 },
75 [AUTOMATION_NAMES.MAC.toLowerCase()]: {
76 driverClassName: 'MacDriver',
77 driverPackage: 'appium-mac-driver',
78 },
79 [AUTOMATION_NAMES.MAC2.toLowerCase()]: {
80 driverClassName: 'Mac2Driver',
81 driverPackage: 'appium-mac2-driver',
82 },
83 [AUTOMATION_NAMES.ESPRESSO.toLowerCase()]: {
84 driverClassName: 'EspressoDriver',
85 driverPackage: 'appium-espresso-driver',
86 },
87 [AUTOMATION_NAMES.TIZEN.toLowerCase()]: {
88 driverClassName: 'TizenDriver',
89 driverPackage: 'appium-tizen-driver',
90 },
91 [AUTOMATION_NAMES.FLUTTER.toLowerCase()]: {
92 driverClassName: 'FlutterDriver',
93 driverPackage: 'appium-flutter-driver'
94 },
95 [AUTOMATION_NAMES.SAFARI.toLowerCase()]: {
96 driverClassName: 'SafariDriver',
97 driverPackage: 'appium-safari-driver'
98 },
99 [AUTOMATION_NAMES.GECKO.toLowerCase()]: {
100 driverClassName: 'GeckoDriver',
101 driverPackage: 'appium-geckodriver'
102 },
103 [AUTOMATION_NAMES.ROKU.toLowerCase()]: {
104 driverClassName: 'RokuDriver',
105 driverPackage: 'appium-roku-driver'
106 },
107 [AUTOMATION_NAMES.WEBOS.toLowerCase()]: {
108 driverClassName: 'WebOSDriver',
109 driverPackage: 'appium-webos-driver'
110 },
111};
112
113const PLATFORMS_MAP = {
114 [PLATFORMS.FAKE]: () => AUTOMATION_NAMES.FAKE,
115 [PLATFORMS.ANDROID]: () => {
116 // Warn users that default automation is going to change to UiAutomator2 for 1.14
117 // and will become required on Appium 2.0
118 const logDividerLength = 70; // Fit in command line
119
120 const automationWarning = [
121 `The 'automationName' capability was not provided in the desired capabilities for this Android session`,
122 `Setting 'automationName=UiAutomator2' by default and using the UiAutomator2 Driver`,
123 `The next major version of Appium (2.x) will **require** the 'automationName' capability to be set for all sessions on all platforms`,
124 `In previous versions (Appium <= 1.13.x), the default was 'automationName=UiAutomator1'`,
125 `If you wish to use that automation instead of UiAutomator2, please add 'automationName=UiAutomator1' to your desired capabilities`,
126 `For more information about drivers, please visit http://appium.io/docs/en/about-appium/intro/ and explore the 'Drivers' menu`
127 ];
128
129 let divider = `${EOL}${_.repeat('=', logDividerLength)}${EOL}`;
130 let automationWarningString = divider;
131 automationWarningString += ` DEPRECATION WARNING:` + EOL;
132 for (let log of automationWarning) {
133 automationWarningString += EOL + wrap(log, {width: logDividerLength - 2}) + EOL;
134 }
135 automationWarningString += divider;
136
137 // Recommend users to upgrade to UiAutomator2 if they're using Android >= 6
138 log.warn(automationWarningString);
139
140 return AUTOMATION_NAMES.UIAUTOMATOR2;
141 },
142 [PLATFORMS.IOS]: (caps) => {
143 const platformVersion = semver.valid(semver.coerce(caps.platformVersion));
144 log.warn(`DeprecationWarning: 'automationName' capability was not provided. ` +
145 `Future versions of Appium will require 'automationName' capability to be set for iOS sessions.`);
146 if (platformVersion && semver.satisfies(platformVersion, '>=10.0.0')) {
147 log.info('Requested iOS support with version >= 10, ' +
148 `using '${AUTOMATION_NAMES.XCUITEST}' ` +
149 'driver instead of UIAutomation-based driver, since the ' +
150 'latter is unsupported on iOS 10 and up.');
151 return AUTOMATION_NAMES.XCUITEST;
152 }
153
154 return AUTOMATION_NAMES.INSTRUMENTS;
155 },
156 [PLATFORMS.APPLE_TVOS]: () => AUTOMATION_NAMES.XCUITEST,
157 [PLATFORMS.WINDOWS]: () => AUTOMATION_NAMES.WINDOWS,
158 [PLATFORMS.MAC]: () => AUTOMATION_NAMES.MAC,
159 [PLATFORMS.TIZEN]: () => AUTOMATION_NAMES.TIZEN,
160 [PLATFORMS.LINUX]: () => AUTOMATION_NAMES.GECKO,
161 [PLATFORMS.ROKU]: () => AUTOMATION_NAMES.ROKU,
162 [PLATFORMS.WEBOS]: () => AUTOMATION_NAMES.WEBOS
163};
164
165const desiredCapabilityConstraints = {
166 automationName: {
167 presence: false,
168 isString: true,
169 inclusionCaseInsensitive: _.values(AUTOMATION_NAMES),
170 },
171 platformName: {
172 presence: true,
173 isString: true,
174 inclusionCaseInsensitive: _.keys(PLATFORMS_MAP),
175 },
176};
177
178const sessionsListGuard = new AsyncLock();
179const pendingDriversGuard = new AsyncLock();
180
181class AppiumDriver extends BaseDriver {
182 constructor (args) {
183 // It is necessary to set `--tmp` here since it should be set to
184 // process.env.APPIUM_TMP_DIR once at an initial point in the Appium lifecycle.
185 // The process argument will be referenced by BaseDriver.
186 // Please call appium-support.tempDir module to apply this benefit.
187 if (args.tmpDir) {
188 process.env.APPIUM_TMP_DIR = args.tmpDir;
189 }
190
191 super(args);
192
193 this.desiredCapConstraints = desiredCapabilityConstraints;
194
195 // the main Appium Driver has no new command timeout
196 this.newCommandTimeoutMs = 0;
197
198 this.args = Object.assign({}, args);
199
200 // Access to sessions list must be guarded with a Semaphore, because
201 // it might be changed by other async calls at any time
202 // It is not recommended to access this property directly from the outside
203 this.sessions = {};
204
205 // Access to pending drivers list must be guarded with a Semaphore, because
206 // it might be changed by other async calls at any time
207 // It is not recommended to access this property directly from the outside
208 this.pendingDrivers = {};
209
210 // allow this to happen in the background, so no `await`
211 updateBuildInfo();
212 }
213
214 /**
215 * Cancel commands queueing for the umbrella Appium driver
216 */
217 get isCommandsQueueEnabled () {
218 return false;
219 }
220
221 sessionExists (sessionId) {
222 const dstSession = this.sessions[sessionId];
223 return dstSession && dstSession.sessionId !== null;
224 }
225
226 driverForSession (sessionId) {
227 return this.sessions[sessionId];
228 }
229
230 getDriverAndVersionForCaps (caps) {
231 if (!_.isString(caps.platformName)) {
232 throw new Error('You must include a platformName capability');
233 }
234
235 const platformName = caps.platformName.toLowerCase();
236
237 // we don't necessarily have an `automationName` capability
238 let automationNameCap = caps.automationName;
239 if (!_.isString(automationNameCap) || automationNameCap.toLowerCase() === 'appium') {
240 const driverSelector = PLATFORMS_MAP[platformName];
241 if (driverSelector) {
242 automationNameCap = driverSelector(caps);
243 }
244 }
245 automationNameCap = _.toLower(automationNameCap);
246
247 let failureVerb = 'find';
248 let suggestion = 'Please check your desired capabilities';
249 if (_.isPlainObject(DRIVER_MAP[automationNameCap])) {
250 try {
251 const {driverPackage, driverClassName} = DRIVER_MAP[automationNameCap];
252 const driver = require(driverPackage)[driverClassName];
253 return {
254 driver,
255 version: this.getDriverVersion(driver.name, driverPackage),
256 };
257 } catch (e) {
258 log.debug(e);
259 failureVerb = 'load';
260 suggestion = 'Please verify your Appium installation';
261 }
262 }
263
264 const msg = _.isString(caps.automationName)
265 ? `Could not ${failureVerb} a driver for automationName '${caps.automationName}' and platformName ` +
266 `'${caps.platformName}'`
267 : `Could not ${failureVerb} a driver for platformName '${caps.platformName}'`;
268 throw new Error(`${msg}. ${suggestion}`);
269 }
270
271 getDriverVersion (driverName, driverPackage) {
272 const version = getPackageVersion(driverPackage);
273 if (version) {
274 return version;
275 }
276 log.warn(`Unable to get version of driver '${driverName}'`);
277 }
278
279 async getStatus () { // eslint-disable-line require-await
280 return {
281 build: _.clone(getBuildInfo()),
282 };
283 }
284
285 async getSessions () {
286 const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
287 return _.toPairs(sessions)
288 .map(([id, driver]) => ({id, capabilities: driver.caps}));
289 }
290
291 printNewSessionAnnouncement (driverName, driverVersion) {
292 const introString = driverVersion
293 ? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
294 : `Appium v${APPIUM_VER} creating new ${driverName} session`;
295 log.info(introString);
296 }
297
298 /**
299 * Create a new session
300 * @param {Object} jsonwpCaps JSONWP formatted desired capabilities
301 * @param {Object} reqCaps Required capabilities (JSONWP standard)
302 * @param {Object} w3cCapabilities W3C capabilities
303 * @return {Array} Unique session ID and capabilities
304 */
305 async createSession (jsonwpCaps, reqCaps, w3cCapabilities) {
306 const defaultCapabilities = _.cloneDeep(this.args.defaultCapabilities);
307 const defaultSettings = pullSettings(defaultCapabilities);
308 jsonwpCaps = _.cloneDeep(jsonwpCaps);
309 const jwpSettings = Object.assign({}, defaultSettings, pullSettings(jsonwpCaps));
310 w3cCapabilities = _.cloneDeep(w3cCapabilities);
311 // It is possible that the client only provides caps using JSONWP standard,
312 // although firstMatch/alwaysMatch properties are still present.
313 // In such case we assume the client understands W3C protocol and merge the given
314 // JSONWP caps to W3C caps
315 const w3cSettings = Object.assign({}, jwpSettings);
316 Object.assign(w3cSettings, pullSettings((w3cCapabilities || {}).alwaysMatch || {}));
317 for (const firstMatchEntry of ((w3cCapabilities || {}).firstMatch || [])) {
318 Object.assign(w3cSettings, pullSettings(firstMatchEntry));
319 }
320
321 let protocol;
322 let innerSessionId, dCaps;
323 try {
324 // Parse the caps into a format that the InnerDriver will accept
325 const parsedCaps = parseCapsForInnerDriver(
326 jsonwpCaps,
327 w3cCapabilities,
328 this.desiredCapConstraints,
329 defaultCapabilities
330 );
331
332 const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, error} = parsedCaps;
333 protocol = parsedCaps.protocol;
334
335 // If the parsing of the caps produced an error, throw it in here
336 if (error) {
337 throw error;
338 }
339
340 const {driver: InnerDriver, version: driverVersion} = this.getDriverAndVersionForCaps(desiredCaps);
341 this.printNewSessionAnnouncement(InnerDriver.name, driverVersion);
342
343 if (this.args.sessionOverride) {
344 await this.deleteAllSessions();
345 }
346
347 let runningDriversData, otherPendingDriversData;
348 const d = new InnerDriver(this.args);
349
350 // We want to assign security values directly on the driver. The driver
351 // should not read security values from `this.opts` because those values
352 // could have been set by a malicious user via capabilities, whereas we
353 // want a guarantee the values were set by the appium server admin
354 if (this.args.relaxedSecurityEnabled) {
355 log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
356 `server command line argument. All insecure features will be ` +
357 `enabled unless explicitly disabled by --deny-insecure`);
358 d.relaxedSecurityEnabled = true;
359 }
360
361 if (!_.isEmpty(this.args.denyInsecure)) {
362 log.info('Explicitly preventing use of insecure features:');
363 this.args.denyInsecure.map((a) => log.info(` ${a}`));
364 d.denyInsecure = this.args.denyInsecure;
365 }
366
367 if (!_.isEmpty(this.args.allowInsecure)) {
368 log.info('Explicitly enabling use of insecure features:');
369 this.args.allowInsecure.map((a) => log.info(` ${a}`));
370 d.allowInsecure = this.args.allowInsecure;
371 }
372
373 // This assignment is required for correct web sockets functionality inside the driver
374 d.server = this.server;
375 try {
376 runningDriversData = await this.curSessionDataForDriver(InnerDriver);
377 } catch (e) {
378 throw new errors.SessionNotCreatedError(e.message);
379 }
380 await pendingDriversGuard.acquire(AppiumDriver.name, () => {
381 this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
382 otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
383 this.pendingDrivers[InnerDriver.name].push(d);
384 });
385
386 try {
387 [innerSessionId, dCaps] = await d.createSession(
388 processedJsonwpCapabilities,
389 reqCaps,
390 processedW3CCapabilities,
391 [...runningDriversData, ...otherPendingDriversData]
392 );
393 protocol = d.protocol;
394 await sessionsListGuard.acquire(AppiumDriver.name, () => {
395 this.sessions[innerSessionId] = d;
396 });
397 } finally {
398 await pendingDriversGuard.acquire(AppiumDriver.name, () => {
399 _.pull(this.pendingDrivers[InnerDriver.name], d);
400 });
401 }
402
403 this.attachUnexpectedShutdownHandler(d, innerSessionId);
404
405 log.info(`New ${InnerDriver.name} session created successfully, session ` +
406 `${innerSessionId} added to master session list`);
407
408 // set the New Command Timeout for the inner driver
409 d.startNewCommandTimeout();
410
411 // apply initial values to Appium settings (if provided)
412 if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
413 log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
414 JSON.stringify(w3cSettings));
415 await d.updateSettings(w3cSettings);
416 } else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
417 log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
418 JSON.stringify(jwpSettings));
419 await d.updateSettings(jwpSettings);
420 }
421 } catch (error) {
422 return {
423 protocol,
424 error,
425 };
426 }
427
428 return {
429 protocol,
430 value: [innerSessionId, dCaps, protocol]
431 };
432 }
433
434 attachUnexpectedShutdownHandler (driver, innerSessionId) {
435 const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
436 log.warn(`Closing session, cause was '${cause.message}'`);
437 log.info(`Removing session '${innerSessionId}' from our master session list`);
438 delete this.sessions[innerSessionId];
439 };
440
441 // eslint-disable-next-line promise/prefer-await-to-then
442 if (_.isFunction((driver.onUnexpectedShutdown || {}).then)) {
443 // TODO: Remove this block after all the drivers use base driver above v 5.0.0
444 // Remove the session on unexpected shutdown, so that we are in a position
445 // to open another session later on.
446 driver.onUnexpectedShutdown
447 // eslint-disable-next-line promise/prefer-await-to-then
448 .then(() => {
449 // if we get here, we've had an unexpected shutdown, so error
450 throw new Error('Unexpected shutdown');
451 })
452 .catch((e) => {
453 // if we cancelled the unexpected shutdown promise, that means we
454 // no longer care about it, and can safely ignore it
455 if (!(e instanceof B.CancellationError)) {
456 removeSessionFromMasterList(e);
457 }
458 }); // this is a cancellable promise
459 } else if (_.isFunction(driver.onUnexpectedShutdown)) {
460 // since base driver v 5.0.0
461 driver.onUnexpectedShutdown(removeSessionFromMasterList);
462 } else {
463 log.warn(`Failed to attach the unexpected shutdown listener. ` +
464 `Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`);
465 }
466 }
467
468 async curSessionDataForDriver (InnerDriver) {
469 const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
470 const data = _.values(sessions)
471 .filter((s) => s.constructor.name === InnerDriver.name)
472 .map((s) => s.driverData);
473 for (let datum of data) {
474 if (!datum) {
475 throw new Error(`Problem getting session data for driver type ` +
476 `${InnerDriver.name}; does it implement 'get ` +
477 `driverData'?`);
478 }
479 }
480 return data;
481 }
482
483 async deleteSession (sessionId) {
484 let protocol;
485 try {
486 let otherSessionsData = null;
487 let dstSession = null;
488 await sessionsListGuard.acquire(AppiumDriver.name, () => {
489 if (!this.sessions[sessionId]) {
490 return;
491 }
492 const curConstructorName = this.sessions[sessionId].constructor.name;
493 otherSessionsData = _.toPairs(this.sessions)
494 .filter(([key, value]) => value.constructor.name === curConstructorName && key !== sessionId)
495 .map(([, value]) => value.driverData);
496 dstSession = this.sessions[sessionId];
497 protocol = dstSession.protocol;
498 log.info(`Removing session ${sessionId} from our master session list`);
499 // regardless of whether the deleteSession completes successfully or not
500 // make the session unavailable, because who knows what state it might
501 // be in otherwise
502 delete this.sessions[sessionId];
503 });
504 return {
505 protocol,
506 value: await dstSession.deleteSession(sessionId, otherSessionsData),
507 };
508 } catch (e) {
509 log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
510 return {
511 protocol,
512 error: e,
513 };
514 }
515 }
516
517 async deleteAllSessions (opts = {}) {
518 const sessionsCount = _.size(this.sessions);
519 if (0 === sessionsCount) {
520 log.debug('There are no active sessions for cleanup');
521 return;
522 }
523
524 const {
525 force = false,
526 reason,
527 } = opts;
528 log.debug(`Cleaning up ${util.pluralize('active session', sessionsCount, true)}`);
529 const cleanupPromises = force
530 ? _.values(this.sessions).map((drv) => drv.startUnexpectedShutdown(reason && new Error(reason)))
531 : _.keys(this.sessions).map((id) => this.deleteSession(id));
532 for (const cleanupPromise of cleanupPromises) {
533 try {
534 await cleanupPromise;
535 } catch (e) {
536 log.debug(e);
537 }
538 }
539 }
540
541 async executeCommand (cmd, ...args) {
542 // getStatus command should not be put into queue. If we do it as part of super.executeCommand, it will be added to queue.
543 // There will be lot of status commands in queue during createSession command, as createSession can take up to or more than a minute.
544 if (cmd === 'getStatus') {
545 return await this.getStatus();
546 }
547
548 if (isAppiumDriverCommand(cmd)) {
549 return await super.executeCommand(cmd, ...args);
550 }
551
552 const sessionId = _.last(args);
553 const dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
554 if (!dstSession) {
555 throw new Error(`The session with id '${sessionId}' does not exist`);
556 }
557
558 let res = {
559 protocol: dstSession.protocol
560 };
561
562 try {
563 res.value = await dstSession.executeCommand(cmd, ...args);
564 } catch (e) {
565 res.error = e;
566 }
567 return res;
568 }
569
570 proxyActive (sessionId) {
571 const dstSession = this.sessions[sessionId];
572 return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
573 }
574
575 getProxyAvoidList (sessionId) {
576 const dstSession = this.sessions[sessionId];
577 return dstSession ? dstSession.getProxyAvoidList() : [];
578 }
579
580 canProxy (sessionId) {
581 const dstSession = this.sessions[sessionId];
582 return dstSession && dstSession.canProxy(sessionId);
583 }
584}
585
586// help decide which commands should be proxied to sub-drivers and which
587// should be handled by this, our umbrella driver
588function isAppiumDriverCommand (cmd) {
589 return !isSessionCommand(cmd) || cmd === 'deleteSession';
590}
591
592export { AppiumDriver };