UNPKG

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