1 | import _ from 'lodash';
|
2 | import log from './logger';
|
3 | import { getBuildInfo, updateBuildInfo, APPIUM_VER } from './config';
|
4 | import { BaseDriver, errors, isSessionCommand } from 'appium-base-driver';
|
5 | import B from 'bluebird';
|
6 | import AsyncLock from 'async-lock';
|
7 | import { parseCapsForInnerDriver, getPackageVersion, pullSettings } from './utils';
|
8 | import semver from 'semver';
|
9 | import wrap from 'word-wrap';
|
10 | import { EOL } from 'os';
|
11 |
|
12 |
|
13 | const 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 |
|
23 | const 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 | };
|
36 | const 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 |
|
79 | const PLATFORMS_MAP = {
|
80 | [PLATFORMS.FAKE]: () => AUTOMATION_NAMES.FAKE,
|
81 | [PLATFORMS.ANDROID]: () => {
|
82 |
|
83 |
|
84 | const logDividerLength = 70;
|
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 |
|
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 |
|
128 | const 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 |
|
141 | const sessionsListGuard = new AsyncLock();
|
142 | const pendingDriversGuard = new AsyncLock();
|
143 |
|
144 | class AppiumDriver extends BaseDriver {
|
145 | constructor (args) {
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | if (args.tmpDir) {
|
151 | process.env.APPIUM_TMP_DIR = args.tmpDir;
|
152 | }
|
153 |
|
154 | super(args);
|
155 |
|
156 | this.desiredCapConstraints = desiredCapabilityConstraints;
|
157 |
|
158 |
|
159 | this.newCommandTimeoutMs = 0;
|
160 |
|
161 | this.args = Object.assign({}, args);
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | this.sessions = {};
|
167 |
|
168 |
|
169 |
|
170 |
|
171 | this.pendingDrivers = {};
|
172 |
|
173 |
|
174 | updateBuildInfo();
|
175 | }
|
176 |
|
177 | |
178 |
|
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 |
|
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 |
|
219 |
|
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 () {
|
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 |
|
260 |
|
261 |
|
262 |
|
263 |
|
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 |
|
272 |
|
273 |
|
274 |
|
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 |
|
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 |
|
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 |
|
317 |
|
318 |
|
319 |
|
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 |
|
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 |
|
370 |
|
371 |
|
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 |
|
378 | d.startNewCommandTimeout();
|
379 |
|
380 |
|
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 |
|
405 |
|
406 |
|
407 | try {
|
408 | await driver.onUnexpectedShutdown;
|
409 |
|
410 | throw new Error('Unexpected shutdown');
|
411 | } catch (e) {
|
412 | if (e instanceof B.CancellationError) {
|
413 |
|
414 |
|
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 |
|
457 |
|
458 |
|
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 |
|
476 |
|
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 |
|
520 |
|
521 | function isAppiumDriverCommand (cmd) {
|
522 | return !isSessionCommand(cmd) || cmd === 'deleteSession';
|
523 | }
|
524 |
|
525 | export { AppiumDriver };
|