appium-chromedriver 5.2.16 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +18 -18
- package/build/index.js.map +1 -0
- package/build/lib/chromedriver.d.ts +108 -0
- package/build/lib/chromedriver.d.ts.map +1 -0
- package/build/lib/chromedriver.js +685 -559
- package/build/lib/chromedriver.js.map +1 -1
- package/build/lib/install.d.ts +3 -0
- package/build/lib/install.d.ts.map +1 -0
- package/build/lib/install.js +39 -32
- package/build/lib/install.js.map +1 -1
- package/build/lib/protocol-helpers.d.ts +15 -0
- package/build/lib/protocol-helpers.d.ts.map +1 -0
- package/build/lib/protocol-helpers.js +38 -21
- package/build/lib/protocol-helpers.js.map +1 -1
- package/build/lib/storage-client.d.ts +119 -0
- package/build/lib/storage-client.d.ts.map +1 -0
- package/build/lib/storage-client.js +393 -274
- package/build/lib/storage-client.js.map +1 -1
- package/build/lib/types.d.ts +101 -0
- package/build/lib/types.d.ts.map +1 -0
- package/build/lib/types.js +3 -0
- package/build/lib/types.js.map +1 -0
- package/build/lib/utils.d.ts +52 -0
- package/build/lib/utils.d.ts.map +1 -0
- package/build/lib/utils.js +117 -78
- package/build/lib/utils.js.map +1 -1
- package/config/mapping.json +1 -0
- package/index.js +9 -0
- package/lib/chromedriver.js +300 -162
- package/lib/install.js +8 -1
- package/lib/protocol-helpers.js +17 -1
- package/lib/storage-client.js +165 -129
- package/lib/types.ts +101 -0
- package/lib/utils.js +86 -42
- package/package.json +34 -30
- package/tsconfig.json +11 -0
package/lib/chromedriver.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
// transpile:main
|
|
2
|
-
|
|
3
1
|
import events from 'events';
|
|
4
|
-
import {
|
|
2
|
+
import {JWProxy, PROTOCOLS} from '@appium/base-driver';
|
|
5
3
|
import cp from 'child_process';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import {system, fs, logger, util} from '@appium/support';
|
|
5
|
+
import {retryInterval, asyncmap} from 'asyncbox';
|
|
6
|
+
import {SubProcess, exec} from 'teen_process';
|
|
9
7
|
import B from 'bluebird';
|
|
10
8
|
import {
|
|
11
|
-
getChromeVersion,
|
|
12
|
-
|
|
9
|
+
getChromeVersion,
|
|
10
|
+
getChromedriverDir,
|
|
11
|
+
CHROMEDRIVER_CHROME_MAPPING,
|
|
12
|
+
getChromedriverBinaryPath,
|
|
13
|
+
CD_CDN,
|
|
14
|
+
generateLogPrefix,
|
|
13
15
|
} from './utils';
|
|
14
16
|
import semver from 'semver';
|
|
15
17
|
import _ from 'lodash';
|
|
16
18
|
import path from 'path';
|
|
17
|
-
import {
|
|
19
|
+
import {compareVersions} from 'compare-versions';
|
|
18
20
|
import ChromedriverStorageClient from './storage-client';
|
|
19
|
-
import {
|
|
21
|
+
import {toW3cCapNames, getCapValue} from './protocol-helpers';
|
|
20
22
|
|
|
21
23
|
const NEW_CD_VERSION_FORMAT_MAJOR_VERSION = 73;
|
|
22
24
|
const DEFAULT_HOST = '127.0.0.1';
|
|
@@ -24,16 +26,17 @@ const MIN_CD_VERSION_WITH_W3C_SUPPORT = 75;
|
|
|
24
26
|
const DEFAULT_PORT = 9515;
|
|
25
27
|
const CHROME_BUNDLE_ID = 'com.android.chrome';
|
|
26
28
|
const WEBVIEW_SHELL_BUNDLE_ID = 'org.chromium.webview_shell';
|
|
27
|
-
const WEBVIEW_BUNDLE_IDS = [
|
|
28
|
-
'com.google.android.webview',
|
|
29
|
-
'com.android.webview',
|
|
30
|
-
];
|
|
29
|
+
const WEBVIEW_BUNDLE_IDS = ['com.google.android.webview', 'com.android.webview'];
|
|
31
30
|
const VERSION_PATTERN = /([\d.]+)/;
|
|
32
31
|
|
|
33
32
|
const CD_VERSION_TIMEOUT = 5000;
|
|
34
33
|
|
|
35
34
|
class Chromedriver extends events.EventEmitter {
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param {import('./types').ChromedriverOpts} args
|
|
38
|
+
*/
|
|
39
|
+
constructor(args = {}) {
|
|
37
40
|
super();
|
|
38
41
|
|
|
39
42
|
const {
|
|
@@ -75,28 +78,30 @@ class Chromedriver extends events.EventEmitter {
|
|
|
75
78
|
this.logPath = logPath;
|
|
76
79
|
this.disableBuildCheck = !!disableBuildCheck;
|
|
77
80
|
this.storageClient = isAutodownloadEnabled
|
|
78
|
-
? new ChromedriverStorageClient({
|
|
81
|
+
? new ChromedriverStorageClient({chromedriverDir: this.executableDir})
|
|
79
82
|
: null;
|
|
80
83
|
this.details = details;
|
|
84
|
+
/** @type {any} */
|
|
81
85
|
this.capabilities = {};
|
|
82
86
|
this.desiredProtocol = PROTOCOLS.MJSONWP;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
get log
|
|
89
|
+
get log() {
|
|
86
90
|
return this._log;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
async getDriversMapping
|
|
93
|
+
async getDriversMapping() {
|
|
90
94
|
let mapping = _.cloneDeep(CHROMEDRIVER_CHROME_MAPPING);
|
|
91
95
|
if (this.mappingPath) {
|
|
92
96
|
this.log.debug(`Attempting to use Chromedriver->Chrome mapping from '${this.mappingPath}'`);
|
|
93
|
-
if (!await fs.exists(this.mappingPath)) {
|
|
97
|
+
if (!(await fs.exists(this.mappingPath))) {
|
|
94
98
|
this.log.warn(`No file found at '${this.mappingPath}'`);
|
|
95
99
|
this.log.info('Defaulting to the static Chromedriver->Chrome mapping');
|
|
96
100
|
} else {
|
|
97
101
|
try {
|
|
98
102
|
mapping = JSON.parse(await fs.readFile(this.mappingPath, 'utf8'));
|
|
99
|
-
} catch (
|
|
103
|
+
} catch (e) {
|
|
104
|
+
const err = /** @type {Error} */ (e);
|
|
100
105
|
this.log.warn(`Error parsing mapping from '${this.mappingPath}': ${err.message}`);
|
|
101
106
|
this.log.info('Defaulting to the static Chromedriver->Chrome mapping');
|
|
102
107
|
}
|
|
@@ -117,7 +122,10 @@ class Chromedriver extends events.EventEmitter {
|
|
|
117
122
|
return mapping;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
|
|
125
|
+
/**
|
|
126
|
+
* @param {ChromedriverVersionMapping} mapping
|
|
127
|
+
*/
|
|
128
|
+
async getChromedrivers(mapping) {
|
|
121
129
|
// go through the versions available
|
|
122
130
|
const executables = await fs.glob('*', {
|
|
123
131
|
cwd: this.executableDir,
|
|
@@ -125,62 +133,78 @@ class Chromedriver extends events.EventEmitter {
|
|
|
125
133
|
nodir: true,
|
|
126
134
|
absolute: true,
|
|
127
135
|
});
|
|
128
|
-
this.log.debug(
|
|
129
|
-
`
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
errMsg
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
this.log.debug(
|
|
137
|
+
`Found ${util.pluralize('executable', executables.length, true)} ` +
|
|
138
|
+
`in '${this.executableDir}'`
|
|
139
|
+
);
|
|
140
|
+
const cds = (
|
|
141
|
+
await asyncmap(executables, async (executable) => {
|
|
142
|
+
/**
|
|
143
|
+
* @param {{message: string, stdout?: string, stderr?: string}} opts
|
|
144
|
+
*/
|
|
145
|
+
const logError = ({message, stdout, stderr}) => {
|
|
146
|
+
let errMsg =
|
|
147
|
+
`Cannot retrieve version number from '${path.basename(
|
|
148
|
+
executable
|
|
149
|
+
)}' Chromedriver binary. ` +
|
|
150
|
+
`Make sure it returns a valid version string in response to '--version' command line argument. ${message}`;
|
|
151
|
+
if (stdout) {
|
|
152
|
+
errMsg += `\nStdout: ${stdout}`;
|
|
153
|
+
}
|
|
154
|
+
if (stderr) {
|
|
155
|
+
errMsg += `\nStderr: ${stderr}`;
|
|
156
|
+
}
|
|
157
|
+
this.log.warn(errMsg);
|
|
158
|
+
return null;
|
|
159
|
+
};
|
|
143
160
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
let stdout;
|
|
162
|
+
let stderr;
|
|
163
|
+
try {
|
|
164
|
+
({stdout, stderr} = await exec(executable, ['--version'], {
|
|
165
|
+
timeout: CD_VERSION_TIMEOUT,
|
|
166
|
+
}));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
const err = /** @type {import('teen_process').ExecError} */ (e);
|
|
169
|
+
if (
|
|
170
|
+
!(err.message || '').includes('timed out') &&
|
|
171
|
+
!(err.stdout || '').includes('Starting ChromeDriver')
|
|
172
|
+
) {
|
|
173
|
+
return logError(err);
|
|
174
|
+
}
|
|
154
175
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
176
|
+
// if this has timed out, it has actually started Chromedriver,
|
|
177
|
+
// in which case there will also be the version string in the output
|
|
178
|
+
stdout = err.stdout;
|
|
179
|
+
}
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
let version = match[1];
|
|
165
|
-
let minChromeVersion = mapping[version];
|
|
166
|
-
const coercedVersion = semver.coerce(version);
|
|
167
|
-
if (coercedVersion) {
|
|
168
|
-
// before 2019-03-06 versions were of the form major.minor
|
|
169
|
-
if (coercedVersion.major < NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
|
|
170
|
-
version = `${coercedVersion.major}.${coercedVersion.minor}`;
|
|
171
|
-
minChromeVersion = mapping[version];
|
|
181
|
+
const match = /ChromeDriver\s+\(?v?([\d.]+)\)?/i.exec(stdout); // https://regex101.com/r/zpj5wA/1
|
|
182
|
+
if (!match) {
|
|
183
|
+
return logError({message: 'Cannot parse the version string', stdout, stderr});
|
|
172
184
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
let version = match[1];
|
|
186
|
+
let minChromeVersion = mapping[version];
|
|
187
|
+
const coercedVersion = semver.coerce(version);
|
|
188
|
+
if (coercedVersion) {
|
|
189
|
+
// before 2019-03-06 versions were of the form major.minor
|
|
190
|
+
if (coercedVersion.major < NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
|
|
191
|
+
version = /** @type {keyof typeof mapping} */ (
|
|
192
|
+
`${coercedVersion.major}.${coercedVersion.minor}`
|
|
193
|
+
);
|
|
194
|
+
minChromeVersion = mapping[version];
|
|
195
|
+
}
|
|
196
|
+
if (!minChromeVersion && coercedVersion.major >= NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
|
|
197
|
+
// Assume the major Chrome version is the same as the corresponding driver major version
|
|
198
|
+
minChromeVersion = `${coercedVersion.major}`;
|
|
199
|
+
}
|
|
176
200
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
201
|
+
return {
|
|
202
|
+
executable,
|
|
203
|
+
version,
|
|
204
|
+
minChromeVersion,
|
|
205
|
+
};
|
|
206
|
+
})
|
|
207
|
+
)
|
|
184
208
|
.filter((cd) => !!cd)
|
|
185
209
|
.sort((a, b) => compareVersions(b.version, a.version));
|
|
186
210
|
if (_.isEmpty(cds)) {
|
|
@@ -189,19 +213,23 @@ class Chromedriver extends events.EventEmitter {
|
|
|
189
213
|
}
|
|
190
214
|
this.log.debug(`The following Chromedriver executables were found:`);
|
|
191
215
|
for (const cd of cds) {
|
|
192
|
-
this.log.debug(
|
|
216
|
+
this.log.debug(
|
|
217
|
+
` '${cd.executable}' (version '${cd.version}', minimum Chrome version '${
|
|
218
|
+
cd.minChromeVersion ? cd.minChromeVersion : 'Unknown'
|
|
219
|
+
}')`
|
|
220
|
+
);
|
|
193
221
|
}
|
|
194
222
|
return cds;
|
|
195
223
|
}
|
|
196
224
|
|
|
197
|
-
async getChromeVersion
|
|
225
|
+
async getChromeVersion() {
|
|
198
226
|
// Try to retrieve the version from `details` property if it is set
|
|
199
227
|
// The `info` item must contain the output of /json/version CDP command
|
|
200
228
|
// where `Browser` field looks like `Chrome/72.0.3601.0``
|
|
201
229
|
if (this.details?.info) {
|
|
202
230
|
this.log.debug(`Browser version in the supplied details: ${this.details?.info?.Browser}`);
|
|
203
231
|
}
|
|
204
|
-
const versionMatch = VERSION_PATTERN.exec(this.details?.info?.Browser);
|
|
232
|
+
const versionMatch = VERSION_PATTERN.exec(this.details?.info?.Browser ?? '');
|
|
205
233
|
if (versionMatch) {
|
|
206
234
|
const coercedVersion = semver.coerce(versionMatch[1]);
|
|
207
235
|
if (coercedVersion) {
|
|
@@ -213,11 +241,13 @@ class Chromedriver extends events.EventEmitter {
|
|
|
213
241
|
|
|
214
242
|
// in case of WebView Browser Tester, simply try to find the underlying webview
|
|
215
243
|
if (this.bundleId === WEBVIEW_SHELL_BUNDLE_ID) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
244
|
+
if (this.adb) {
|
|
245
|
+
for (const bundleId of WEBVIEW_BUNDLE_IDS) {
|
|
246
|
+
chromeVersion = await getChromeVersion(this.adb, bundleId);
|
|
247
|
+
if (chromeVersion) {
|
|
248
|
+
this.bundleId = bundleId;
|
|
249
|
+
return semver.coerce(chromeVersion);
|
|
250
|
+
}
|
|
221
251
|
}
|
|
222
252
|
}
|
|
223
253
|
return null;
|
|
@@ -226,8 +256,11 @@ class Chromedriver extends events.EventEmitter {
|
|
|
226
256
|
// on Android 7-9 webviews are backed by the main Chrome, not the system webview
|
|
227
257
|
if (this.adb) {
|
|
228
258
|
const apiLevel = await this.adb.getApiLevel();
|
|
229
|
-
if (
|
|
230
|
-
|
|
259
|
+
if (
|
|
260
|
+
apiLevel >= 24 &&
|
|
261
|
+
apiLevel <= 28 &&
|
|
262
|
+
[WEBVIEW_SHELL_BUNDLE_ID, ...WEBVIEW_BUNDLE_IDS].includes(this.bundleId ?? '')
|
|
263
|
+
) {
|
|
231
264
|
this.bundleId = CHROME_BUNDLE_ID;
|
|
232
265
|
}
|
|
233
266
|
}
|
|
@@ -239,16 +272,18 @@ class Chromedriver extends events.EventEmitter {
|
|
|
239
272
|
|
|
240
273
|
// we have a webview of some sort, so try to find the bundle version
|
|
241
274
|
for (const bundleId of WEBVIEW_BUNDLE_IDS) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
if (this.adb) {
|
|
276
|
+
chromeVersion = await getChromeVersion(this.adb, bundleId);
|
|
277
|
+
if (chromeVersion) {
|
|
278
|
+
this.bundleId = bundleId;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
246
281
|
}
|
|
247
282
|
}
|
|
248
283
|
}
|
|
249
284
|
|
|
250
285
|
// if we do not have a chrome version, it must not be a webview
|
|
251
|
-
if (!chromeVersion) {
|
|
286
|
+
if (!chromeVersion && this.adb) {
|
|
252
287
|
chromeVersion = await getChromeVersion(this.adb, this.bundleId);
|
|
253
288
|
}
|
|
254
289
|
|
|
@@ -256,15 +291,27 @@ class Chromedriver extends events.EventEmitter {
|
|
|
256
291
|
return chromeVersion ? semver.coerce(chromeVersion) : null;
|
|
257
292
|
}
|
|
258
293
|
|
|
259
|
-
|
|
294
|
+
/**
|
|
295
|
+
*
|
|
296
|
+
* @param {ChromedriverVersionMapping} newMapping
|
|
297
|
+
* @returns {Promise<void>}
|
|
298
|
+
*/
|
|
299
|
+
async updateDriversMapping(newMapping) {
|
|
260
300
|
let shouldUpdateStaticMapping = true;
|
|
301
|
+
if (!this.mappingPath) {
|
|
302
|
+
this.log.warn('No mapping path provided');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
261
305
|
if (await fs.exists(this.mappingPath)) {
|
|
262
306
|
try {
|
|
263
307
|
await fs.writeFile(this.mappingPath, JSON.stringify(newMapping, null, 2), 'utf8');
|
|
264
308
|
shouldUpdateStaticMapping = false;
|
|
265
309
|
} catch (e) {
|
|
266
|
-
|
|
267
|
-
|
|
310
|
+
const err = /** @type {Error} */ (e);
|
|
311
|
+
this.log.warn(
|
|
312
|
+
`Cannot store the updated chromedrivers mapping into '${this.mappingPath}'. ` +
|
|
313
|
+
`This may reduce the performance of further executions. Original error: ${err.message}`
|
|
314
|
+
);
|
|
268
315
|
}
|
|
269
316
|
}
|
|
270
317
|
if (shouldUpdateStaticMapping) {
|
|
@@ -272,7 +319,10 @@ class Chromedriver extends events.EventEmitter {
|
|
|
272
319
|
}
|
|
273
320
|
}
|
|
274
321
|
|
|
275
|
-
|
|
322
|
+
/**
|
|
323
|
+
* @returns {Promise<string>}
|
|
324
|
+
*/
|
|
325
|
+
async getCompatibleChromedriver() {
|
|
276
326
|
if (!this.adb) {
|
|
277
327
|
return await getChromedriverBinaryPath();
|
|
278
328
|
}
|
|
@@ -283,11 +333,19 @@ class Chromedriver extends events.EventEmitter {
|
|
|
283
333
|
}
|
|
284
334
|
|
|
285
335
|
let didStorageSync = false;
|
|
336
|
+
/**
|
|
337
|
+
*
|
|
338
|
+
* @param {import('semver').SemVer} chromeVersion
|
|
339
|
+
*/
|
|
286
340
|
const syncChromedrivers = async (chromeVersion) => {
|
|
287
341
|
didStorageSync = true;
|
|
342
|
+
if (!this.storageClient) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
288
345
|
const retrievedMapping = await this.storageClient.retrieveMapping();
|
|
289
|
-
this.log.debug(
|
|
290
|
-
JSON.stringify(retrievedMapping, null, 2)
|
|
346
|
+
this.log.debug(
|
|
347
|
+
'Got chromedrivers mapping from the storage: ' + JSON.stringify(retrievedMapping, null, 2)
|
|
348
|
+
);
|
|
291
349
|
const driverKeys = await this.storageClient.syncDrivers({
|
|
292
350
|
minBrowserVersion: chromeVersion.major,
|
|
293
351
|
});
|
|
@@ -298,7 +356,7 @@ class Chromedriver extends events.EventEmitter {
|
|
|
298
356
|
const {version, minBrowserVersion} = retrievedMapping[x];
|
|
299
357
|
acc[version] = minBrowserVersion;
|
|
300
358
|
return acc;
|
|
301
|
-
}, {});
|
|
359
|
+
}, /** @type {ChromedriverVersionMapping} */ ({}));
|
|
302
360
|
Object.assign(mapping, synchronizedDriversMapping);
|
|
303
361
|
await this.updateDriversMapping(mapping);
|
|
304
362
|
return true;
|
|
@@ -307,6 +365,7 @@ class Chromedriver extends events.EventEmitter {
|
|
|
307
365
|
do {
|
|
308
366
|
const cds = await this.getChromedrivers(mapping);
|
|
309
367
|
|
|
368
|
+
/** @type {ChromedriverVersionMapping} */
|
|
310
369
|
const missingVersions = {};
|
|
311
370
|
for (const {version, minChromeVersion} of cds) {
|
|
312
371
|
if (!minChromeVersion || mapping[version]) {
|
|
@@ -320,20 +379,30 @@ class Chromedriver extends events.EventEmitter {
|
|
|
320
379
|
missingVersions[version] = minChromeVersion;
|
|
321
380
|
}
|
|
322
381
|
if (!_.isEmpty(missingVersions)) {
|
|
323
|
-
this.log.info(
|
|
324
|
-
`
|
|
325
|
-
|
|
382
|
+
this.log.info(
|
|
383
|
+
`Found ${util.pluralize('Chromedriver', _.size(missingVersions), true)}, ` +
|
|
384
|
+
`which ${
|
|
385
|
+
_.size(missingVersions) === 1 ? 'is' : 'are'
|
|
386
|
+
} missing in the list of known versions: ` +
|
|
387
|
+
JSON.stringify(missingVersions)
|
|
388
|
+
);
|
|
326
389
|
await this.updateDriversMapping(Object.assign(mapping, missingVersions));
|
|
327
390
|
}
|
|
328
391
|
|
|
329
392
|
if (this.disableBuildCheck) {
|
|
330
393
|
if (_.isEmpty(cds)) {
|
|
331
|
-
this.log.errorAndThrow(
|
|
332
|
-
`
|
|
394
|
+
this.log.errorAndThrow(
|
|
395
|
+
`There must be at least one Chromedriver executable available for use if ` +
|
|
396
|
+
`'chromedriverDisableBuildCheck' capability is set to 'true'`
|
|
397
|
+
);
|
|
333
398
|
}
|
|
334
399
|
const {version, executable} = cds[0];
|
|
335
|
-
this.log.warn(
|
|
336
|
-
|
|
400
|
+
this.log.warn(
|
|
401
|
+
`Chrome build check disabled. Using most recent Chromedriver version (${version}, at '${executable}')`
|
|
402
|
+
);
|
|
403
|
+
this.log.warn(
|
|
404
|
+
`If this is wrong, set 'chromedriverDisableBuildCheck' capability to 'false'`
|
|
405
|
+
);
|
|
337
406
|
return executable;
|
|
338
407
|
}
|
|
339
408
|
|
|
@@ -341,11 +410,15 @@ class Chromedriver extends events.EventEmitter {
|
|
|
341
410
|
if (!chromeVersion) {
|
|
342
411
|
// unable to get the chrome version
|
|
343
412
|
if (_.isEmpty(cds)) {
|
|
344
|
-
this.log.errorAndThrow(
|
|
345
|
-
`
|
|
413
|
+
this.log.errorAndThrow(
|
|
414
|
+
`There must be at least one Chromedriver executable available for use if ` +
|
|
415
|
+
`the current Chrome version cannot be determined`
|
|
416
|
+
);
|
|
346
417
|
}
|
|
347
418
|
const {version, executable} = cds[0];
|
|
348
|
-
this.log.warn(
|
|
419
|
+
this.log.warn(
|
|
420
|
+
`Unable to discover Chrome version. Using Chromedriver ${version} at '${executable}'`
|
|
421
|
+
);
|
|
349
422
|
return executable;
|
|
350
423
|
}
|
|
351
424
|
this.log.debug(`Found Chrome bundle '${this.bundleId}' version '${chromeVersion}'`);
|
|
@@ -367,59 +440,82 @@ class Chromedriver extends events.EventEmitter {
|
|
|
367
440
|
continue;
|
|
368
441
|
}
|
|
369
442
|
} catch (e) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
443
|
+
const err = /** @type {Error} */ (e);
|
|
444
|
+
this.log.warn(
|
|
445
|
+
`Cannot synchronize local chromedrivers with the remote storage at ${CD_CDN}: ` +
|
|
446
|
+
err.message
|
|
447
|
+
);
|
|
448
|
+
this.log.debug(err.stack);
|
|
373
449
|
}
|
|
374
450
|
}
|
|
375
451
|
const autodownloadSuggestion =
|
|
376
452
|
'You could also try to enable automated chromedrivers download as ' +
|
|
377
453
|
'a possible workaround.';
|
|
378
|
-
throw new Error(
|
|
379
|
-
|
|
454
|
+
throw new Error(
|
|
455
|
+
`No Chromedriver found that can automate Chrome '${chromeVersion}'.` +
|
|
456
|
+
(this.storageClient ? '' : ` ${autodownloadSuggestion}`)
|
|
457
|
+
);
|
|
380
458
|
}
|
|
381
459
|
|
|
382
460
|
const binPath = matchingDrivers[0].executable;
|
|
383
|
-
this.log.debug(
|
|
384
|
-
`
|
|
385
|
-
|
|
386
|
-
|
|
461
|
+
this.log.debug(
|
|
462
|
+
`Found ${util.pluralize('executable', matchingDrivers.length, true)} ` +
|
|
463
|
+
`capable of automating Chrome '${chromeVersion}'.\nChoosing the most recent, '${binPath}'.`
|
|
464
|
+
);
|
|
465
|
+
this.log.debug(
|
|
466
|
+
'If a specific version is required, specify it with the `chromedriverExecutable`' +
|
|
467
|
+
'desired capability.'
|
|
468
|
+
);
|
|
387
469
|
return binPath;
|
|
388
|
-
|
|
470
|
+
// eslint-disable-next-line no-constant-condition
|
|
389
471
|
} while (true);
|
|
390
472
|
}
|
|
391
473
|
|
|
392
|
-
async initChromedriverPath
|
|
393
|
-
if (this.executableVerified
|
|
474
|
+
async initChromedriverPath() {
|
|
475
|
+
if (this.executableVerified && this.chromedriver) {
|
|
476
|
+
return /** @type {string} */ (this.chromedriver);
|
|
477
|
+
}
|
|
394
478
|
|
|
479
|
+
let chromedriver = this.chromedriver;
|
|
395
480
|
// the executable might be set (if passed in)
|
|
396
481
|
// or we might want to use the basic one installed with this driver
|
|
397
482
|
// or we want to figure out the best one
|
|
398
|
-
if (!
|
|
399
|
-
this.chromedriver = this.useSystemExecutable
|
|
483
|
+
if (!chromedriver) {
|
|
484
|
+
chromedriver = this.chromedriver = this.useSystemExecutable
|
|
400
485
|
? await getChromedriverBinaryPath()
|
|
401
486
|
: await this.getCompatibleChromedriver();
|
|
402
487
|
}
|
|
403
488
|
|
|
404
|
-
if (!await fs.exists(
|
|
405
|
-
throw new Error(
|
|
406
|
-
|
|
489
|
+
if (!(await fs.exists(chromedriver))) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Trying to use a chromedriver binary at the path ` +
|
|
492
|
+
`${this.chromedriver}, but it doesn't exist!`
|
|
493
|
+
);
|
|
407
494
|
}
|
|
408
495
|
this.executableVerified = true;
|
|
409
496
|
this.log.info(`Set chromedriver binary as: ${this.chromedriver}`);
|
|
497
|
+
return /** @type {string} */ (this.chromedriver);
|
|
410
498
|
}
|
|
411
499
|
|
|
412
|
-
|
|
500
|
+
/**
|
|
501
|
+
*
|
|
502
|
+
* @param {string} [cdVersion]
|
|
503
|
+
*/
|
|
504
|
+
syncProtocol(cdVersion) {
|
|
413
505
|
const coercedVersion = semver.coerce(cdVersion);
|
|
414
506
|
if (!coercedVersion || coercedVersion.major < MIN_CD_VERSION_WITH_W3C_SUPPORT) {
|
|
415
|
-
this.log.debug(
|
|
416
|
-
`
|
|
507
|
+
this.log.debug(
|
|
508
|
+
`Chromedriver v. ${cdVersion} does not fully support ${PROTOCOLS.W3C} protocol. ` +
|
|
509
|
+
`Defaulting to ${PROTOCOLS.MJSONWP}`
|
|
510
|
+
);
|
|
417
511
|
return;
|
|
418
512
|
}
|
|
419
513
|
const chromeOptions = getCapValue(this.capabilities, 'chromeOptions', {});
|
|
420
514
|
if (chromeOptions.w3c === false) {
|
|
421
|
-
this.log.info(
|
|
422
|
-
`
|
|
515
|
+
this.log.info(
|
|
516
|
+
`Chromedriver v. ${cdVersion} supports ${PROTOCOLS.W3C} protocol, ` +
|
|
517
|
+
`but ${PROTOCOLS.MJSONWP} one has been explicitly requested`
|
|
518
|
+
);
|
|
423
519
|
return;
|
|
424
520
|
}
|
|
425
521
|
this.desiredProtocol = PROTOCOLS.W3C;
|
|
@@ -429,7 +525,12 @@ class Chromedriver extends events.EventEmitter {
|
|
|
429
525
|
this.capabilities = toW3cCapNames(this.capabilities);
|
|
430
526
|
}
|
|
431
527
|
|
|
432
|
-
|
|
528
|
+
/**
|
|
529
|
+
*
|
|
530
|
+
* @param {object} caps
|
|
531
|
+
* @param {boolean} emitStartingState
|
|
532
|
+
*/
|
|
533
|
+
async start(caps, emitStartingState = true) {
|
|
433
534
|
this.capabilities = _.cloneDeep(caps);
|
|
434
535
|
|
|
435
536
|
// set the logging preferences to ALL the console logs
|
|
@@ -458,16 +559,16 @@ class Chromedriver extends events.EventEmitter {
|
|
|
458
559
|
args.push('--verbose');
|
|
459
560
|
// what are the process stdout/stderr conditions wherein we know that
|
|
460
561
|
// the process has started to our satisfaction?
|
|
461
|
-
const startDetector = (stdout) => stdout.startsWith('Starting ');
|
|
562
|
+
const startDetector = /** @param {string} stdout */ (stdout) => stdout.startsWith('Starting ');
|
|
462
563
|
|
|
463
564
|
let processIsAlive = false;
|
|
464
565
|
let webviewVersion;
|
|
465
566
|
try {
|
|
466
|
-
await this.initChromedriverPath();
|
|
567
|
+
const chromedriverPath = await this.initChromedriverPath();
|
|
467
568
|
await this.killAll();
|
|
468
569
|
|
|
469
570
|
// set up our subprocess object
|
|
470
|
-
this.proc = new SubProcess(
|
|
571
|
+
this.proc = new SubProcess(chromedriverPath, args);
|
|
471
572
|
processIsAlive = true;
|
|
472
573
|
|
|
473
574
|
// handle log output
|
|
@@ -513,9 +614,11 @@ class Chromedriver extends events.EventEmitter {
|
|
|
513
614
|
// handle out-of-bound exit by simply emitting a stopped state
|
|
514
615
|
this.proc.on('exit', (code, signal) => {
|
|
515
616
|
processIsAlive = false;
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
617
|
+
if (
|
|
618
|
+
this.state !== Chromedriver.STATE_STOPPED &&
|
|
619
|
+
this.state !== Chromedriver.STATE_STOPPING &&
|
|
620
|
+
this.state !== Chromedriver.STATE_RESTARTING
|
|
621
|
+
) {
|
|
519
622
|
const msg = `Chromedriver exited unexpectedly with code ${code}, signal ${signal}`;
|
|
520
623
|
this.log.error(msg);
|
|
521
624
|
this.changeState(Chromedriver.STATE_STOPPED);
|
|
@@ -527,38 +630,41 @@ class Chromedriver extends events.EventEmitter {
|
|
|
527
630
|
await this.waitForOnline();
|
|
528
631
|
await this.startSession();
|
|
529
632
|
} catch (e) {
|
|
530
|
-
|
|
531
|
-
this.
|
|
633
|
+
const err = /** @type {Error} */ (e);
|
|
634
|
+
this.log.debug(err);
|
|
635
|
+
this.emit(Chromedriver.EVENT_ERROR, err);
|
|
532
636
|
// just because we had an error doesn't mean the chromedriver process
|
|
533
637
|
// finished; we should clean up if necessary
|
|
534
638
|
if (processIsAlive) {
|
|
535
|
-
await this.proc
|
|
639
|
+
await this.proc?.stop();
|
|
536
640
|
}
|
|
537
641
|
|
|
538
642
|
let message = '';
|
|
539
643
|
// often the user's Chrome version is not supported by the version of Chromedriver
|
|
540
|
-
if (
|
|
541
|
-
message +=
|
|
644
|
+
if (err.message.includes('Chrome version must be')) {
|
|
645
|
+
message +=
|
|
646
|
+
'Unable to automate Chrome version because it is not supported by this version of Chromedriver.\n';
|
|
542
647
|
if (webviewVersion) {
|
|
543
648
|
message += `Chrome version on the device: ${webviewVersion}\n`;
|
|
544
649
|
}
|
|
545
|
-
const versionsSupportedByDriver =
|
|
650
|
+
const versionsSupportedByDriver =
|
|
651
|
+
/Chrome version must be (.+)/.exec(err.message)?.[1] || '';
|
|
546
652
|
if (versionsSupportedByDriver) {
|
|
547
653
|
message += `Chromedriver supports Chrome version(s): ${versionsSupportedByDriver}\n`;
|
|
548
654
|
}
|
|
549
655
|
message += 'Check the driver tutorial for troubleshooting.\n';
|
|
550
656
|
}
|
|
551
657
|
|
|
552
|
-
message +=
|
|
658
|
+
message += err.message;
|
|
553
659
|
this.log.errorAndThrow(message);
|
|
554
660
|
}
|
|
555
661
|
}
|
|
556
662
|
|
|
557
|
-
sessionId
|
|
663
|
+
sessionId() {
|
|
558
664
|
return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
|
|
559
665
|
}
|
|
560
666
|
|
|
561
|
-
async restart
|
|
667
|
+
async restart() {
|
|
562
668
|
this.log.info('Restarting chromedriver');
|
|
563
669
|
if (this.state !== Chromedriver.STATE_ONLINE) {
|
|
564
670
|
throw new Error("Can't restart when we're not online");
|
|
@@ -568,7 +674,7 @@ class Chromedriver extends events.EventEmitter {
|
|
|
568
674
|
await this.start(this.capabilities, false);
|
|
569
675
|
}
|
|
570
676
|
|
|
571
|
-
async waitForOnline
|
|
677
|
+
async waitForOnline() {
|
|
572
678
|
// we need to make sure that CD hasn't crashed
|
|
573
679
|
let chromedriverStopped = false;
|
|
574
680
|
await retryInterval(20, 200, async () => {
|
|
@@ -584,62 +690,89 @@ class Chromedriver extends events.EventEmitter {
|
|
|
584
690
|
}
|
|
585
691
|
}
|
|
586
692
|
|
|
587
|
-
async getStatus
|
|
693
|
+
async getStatus() {
|
|
588
694
|
return await this.jwproxy.command('/status', 'GET');
|
|
589
695
|
}
|
|
590
696
|
|
|
591
|
-
async startSession
|
|
592
|
-
const sessionCaps =
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
697
|
+
async startSession() {
|
|
698
|
+
const sessionCaps =
|
|
699
|
+
this.desiredProtocol === PROTOCOLS.W3C
|
|
700
|
+
? {capabilities: {alwaysMatch: this.capabilities}}
|
|
701
|
+
: {desiredCapabilities: this.capabilities};
|
|
702
|
+
this.log.info(
|
|
703
|
+
`Starting ${this.desiredProtocol} Chromedriver session with capabilities: ` +
|
|
704
|
+
JSON.stringify(sessionCaps, null, 2)
|
|
705
|
+
);
|
|
706
|
+
// jwproxy types have not been implemented yet
|
|
707
|
+
// @ts-expect-error
|
|
597
708
|
await this.jwproxy.command('/session', 'POST', sessionCaps);
|
|
598
709
|
this.log.prefix = generateLogPrefix(this, this.jwproxy.sessionId);
|
|
599
710
|
this.changeState(Chromedriver.STATE_ONLINE);
|
|
600
711
|
}
|
|
601
712
|
|
|
602
|
-
async stop
|
|
713
|
+
async stop(emitStates = true) {
|
|
603
714
|
if (emitStates) {
|
|
604
715
|
this.changeState(Chromedriver.STATE_STOPPING);
|
|
605
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
*
|
|
719
|
+
* @param {() => Promise<any>|any} f
|
|
720
|
+
*/
|
|
606
721
|
const runSafeStep = async (f) => {
|
|
607
722
|
try {
|
|
608
723
|
return await f();
|
|
609
724
|
} catch (e) {
|
|
610
|
-
|
|
611
|
-
this.log.
|
|
725
|
+
const err = /** @type {Error} */ (e);
|
|
726
|
+
this.log.warn(err.message);
|
|
727
|
+
this.log.debug(err.stack);
|
|
612
728
|
}
|
|
613
729
|
};
|
|
614
730
|
await runSafeStep(() => this.jwproxy.command('', 'DELETE'));
|
|
615
|
-
await runSafeStep(() => this.proc
|
|
731
|
+
await runSafeStep(() => this.proc?.stop('SIGTERM', 20000));
|
|
616
732
|
this.log.prefix = generateLogPrefix(this);
|
|
617
733
|
if (emitStates) {
|
|
618
734
|
this.changeState(Chromedriver.STATE_STOPPED);
|
|
619
735
|
}
|
|
620
736
|
}
|
|
621
737
|
|
|
622
|
-
|
|
738
|
+
/**
|
|
739
|
+
*
|
|
740
|
+
* @param {string} state
|
|
741
|
+
*/
|
|
742
|
+
changeState(state) {
|
|
623
743
|
this.state = state;
|
|
624
744
|
this.log.debug(`Changed state to '${state}'`);
|
|
625
745
|
this.emit(Chromedriver.EVENT_CHANGED, {state});
|
|
626
746
|
}
|
|
627
747
|
|
|
628
|
-
|
|
748
|
+
/**
|
|
749
|
+
*
|
|
750
|
+
* @param {string} url
|
|
751
|
+
* @param {'POST'|'GET'|'DELETE'} method
|
|
752
|
+
* @param {any} body
|
|
753
|
+
* @returns
|
|
754
|
+
*/
|
|
755
|
+
async sendCommand(url, method, body) {
|
|
629
756
|
return await this.jwproxy.command(url, method, body);
|
|
630
757
|
}
|
|
631
758
|
|
|
632
|
-
|
|
759
|
+
/**
|
|
760
|
+
*
|
|
761
|
+
* @param {any} req
|
|
762
|
+
* @param {any} res
|
|
763
|
+
* @privateRemarks req / res probably from Express
|
|
764
|
+
*/
|
|
765
|
+
async proxyReq(req, res) {
|
|
633
766
|
return await this.jwproxy.proxyReqRes(req, res);
|
|
634
767
|
}
|
|
635
768
|
|
|
636
|
-
async killAll
|
|
769
|
+
async killAll() {
|
|
637
770
|
let cmd = system.isWindows()
|
|
638
771
|
? `wmic process where "commandline like '%chromedriver.exe%--port=${this.proxyPort}%'" delete`
|
|
639
772
|
: `pkill -15 -f "${this.chromedriver}.*--port=${this.proxyPort}"`;
|
|
640
773
|
this.log.debug(`Killing any old chromedrivers, running: ${cmd}`);
|
|
641
774
|
try {
|
|
642
|
-
await
|
|
775
|
+
await B.promisify(cp.exec)(cmd);
|
|
643
776
|
this.log.debug('Successfully cleaned up old chromedrivers');
|
|
644
777
|
} catch (err) {
|
|
645
778
|
this.log.warn('No old chromedrivers seem to exist');
|
|
@@ -667,13 +800,14 @@ class Chromedriver extends events.EventEmitter {
|
|
|
667
800
|
await this.adb.removePortForward(params[1].replace(/[\D]*/, ''));
|
|
668
801
|
}
|
|
669
802
|
}
|
|
670
|
-
} catch (
|
|
803
|
+
} catch (e) {
|
|
804
|
+
const err = /** @type {Error} */ (e);
|
|
671
805
|
this.log.warn(`Unable to clean forwarded ports. Error: '${err.message}'. Continuing.`);
|
|
672
806
|
}
|
|
673
807
|
}
|
|
674
808
|
}
|
|
675
809
|
|
|
676
|
-
async hasWorkingWebview
|
|
810
|
+
async hasWorkingWebview() {
|
|
677
811
|
// sometimes chromedriver stops automating webviews. this method runs a
|
|
678
812
|
// simple command to determine our state, and responds accordingly
|
|
679
813
|
try {
|
|
@@ -693,5 +827,9 @@ Chromedriver.STATE_ONLINE = 'online';
|
|
|
693
827
|
Chromedriver.STATE_STOPPING = 'stopping';
|
|
694
828
|
Chromedriver.STATE_RESTARTING = 'restarting';
|
|
695
829
|
|
|
696
|
-
export {
|
|
830
|
+
export {Chromedriver};
|
|
697
831
|
export default Chromedriver;
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* @typedef {import('./types').ChromedriverVersionMapping} ChromedriverVersionMapping
|
|
835
|
+
*/
|