appium-chromedriver 5.5.2 → 5.6.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +14 -0
  3. package/build/lib/chromedriver.d.ts +1 -1
  4. package/build/lib/chromedriver.d.ts.map +1 -1
  5. package/build/lib/chromedriver.js +4 -4
  6. package/build/lib/chromedriver.js.map +1 -1
  7. package/build/lib/constants.d.ts +19 -0
  8. package/build/lib/constants.d.ts.map +1 -0
  9. package/build/lib/constants.js +26 -0
  10. package/build/lib/constants.js.map +1 -0
  11. package/build/lib/index.d.ts +1 -1
  12. package/build/lib/index.d.ts.map +1 -1
  13. package/build/lib/index.js +1 -1
  14. package/build/lib/index.js.map +1 -1
  15. package/build/lib/install.d.ts.map +1 -1
  16. package/build/lib/install.js +23 -8
  17. package/build/lib/install.js.map +1 -1
  18. package/build/lib/storage-client/chromelabs.d.ts +22 -0
  19. package/build/lib/storage-client/chromelabs.d.ts.map +1 -0
  20. package/build/lib/storage-client/chromelabs.js +148 -0
  21. package/build/lib/storage-client/chromelabs.js.map +1 -0
  22. package/build/lib/storage-client/googleapis.d.ts +32 -0
  23. package/build/lib/storage-client/googleapis.d.ts.map +1 -0
  24. package/build/lib/storage-client/googleapis.js +188 -0
  25. package/build/lib/storage-client/googleapis.js.map +1 -0
  26. package/build/lib/{storage-client.d.ts → storage-client/storage-client.d.ts} +14 -36
  27. package/build/lib/storage-client/storage-client.d.ts.map +1 -0
  28. package/build/lib/{storage-client.js → storage-client/storage-client.js} +105 -181
  29. package/build/lib/storage-client/storage-client.js.map +1 -0
  30. package/build/lib/types.d.ts +9 -3
  31. package/build/lib/types.d.ts.map +1 -1
  32. package/build/lib/utils.d.ts +6 -9
  33. package/build/lib/utils.d.ts.map +1 -1
  34. package/build/lib/utils.js +20 -21
  35. package/build/lib/utils.js.map +1 -1
  36. package/lib/chromedriver.js +4 -5
  37. package/lib/constants.js +24 -0
  38. package/lib/index.ts +1 -1
  39. package/lib/install.js +27 -10
  40. package/lib/storage-client/chromelabs.js +141 -0
  41. package/lib/storage-client/googleapis.js +209 -0
  42. package/lib/{storage-client.js → storage-client/storage-client.js} +124 -213
  43. package/lib/types.ts +9 -3
  44. package/lib/utils.js +20 -20
  45. package/package.json +2 -2
  46. package/build/lib/storage-client.d.ts.map +0 -1
  47. package/build/lib/storage-client.js.map +0 -1
@@ -0,0 +1,209 @@
1
+ import _ from 'lodash';
2
+ import xpath from 'xpath';
3
+ import {util, logger} from '@appium/support';
4
+ import {retrieveData} from '../utils';
5
+ import B from 'bluebird';
6
+ import {
7
+ STORAGE_REQ_TIMEOUT_MS,
8
+ GOOGLEAPIS_CDN,
9
+ ARCH,
10
+ CPU,
11
+ APPLE_ARM_SUFFIXES,
12
+ } from '../constants';
13
+ import {DOMParser} from '@xmldom/xmldom';
14
+ import path from 'path';
15
+
16
+
17
+ const log = logger.getLogger('ChromedriverGoogleapisStorageClient');
18
+ const MAX_PARALLEL_DOWNLOADS = 5;
19
+
20
+ /**
21
+ *
22
+ * @param {Node|Attr} parent
23
+ * @param {string?} childName
24
+ * @param {string?} text
25
+ * @returns
26
+ */
27
+ export function findChildNode(parent, childName = null, text = null) {
28
+ if (!childName && !text) {
29
+ return null;
30
+ }
31
+ if (!parent.hasChildNodes()) {
32
+ return null;
33
+ }
34
+
35
+ for (let childNodeIdx = 0; childNodeIdx < parent.childNodes.length; childNodeIdx++) {
36
+ const childNode = /** @type {Element|Attr} */ (parent.childNodes[childNodeIdx]);
37
+ if (childName && !text && childName === childNode.localName) {
38
+ return childNode;
39
+ }
40
+ if (text) {
41
+ const childText = extractNodeText(childNode);
42
+ if (!childText) {
43
+ continue;
44
+ }
45
+ if (childName && childName === childNode.localName && text === childText) {
46
+ return childNode;
47
+ }
48
+ if (!childName && text === childText) {
49
+ return childNode;
50
+ }
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ *
58
+ * @param {Node?} node
59
+ * @returns
60
+ */
61
+ function extractNodeText(node) {
62
+ return !node || !node.firstChild || !util.hasValue(node.firstChild.nodeValue)
63
+ ? null
64
+ : node.firstChild.nodeValue;
65
+ }
66
+
67
+ /**
68
+ * Gets additional chromedriver details from chromedriver
69
+ * release notes
70
+ *
71
+ * @param {string} content - Release notes of the corresponding chromedriver
72
+ * @returns {import('../types').AdditionalDriverDetails}
73
+ */
74
+ export function parseNotes(content) {
75
+ const result = {};
76
+ const versionMatch = /^\s*[-]+ChromeDriver[\D]+([\d.]+)/im.exec(content);
77
+ if (versionMatch) {
78
+ result.version = versionMatch[1];
79
+ }
80
+ const minBrowserVersionMatch = /^\s*Supports Chrome[\D]+(\d+)/im.exec(content);
81
+ if (minBrowserVersionMatch) {
82
+ result.minBrowserVersion = minBrowserVersionMatch[1];
83
+ }
84
+ return result;
85
+ }
86
+
87
+ /**
88
+ * Parses chromedriver storage XML and returns
89
+ * the parsed results
90
+ *
91
+ * @param {string} xml - The chromedriver storage XML
92
+ * @param {boolean} shouldParseNotes [true] - If set to `true`
93
+ * then additional drivers information is going to be parsed
94
+ * and assigned to `this.mapping`
95
+ * @returns {Promise<ChromedriverDetailsMapping>}
96
+ */
97
+ export async function parseGoogleapiStorageXml(xml, shouldParseNotes = true) {
98
+ const doc = new DOMParser().parseFromString(xml);
99
+ const driverNodes = /** @type {Array<Node|Attr>} */ (
100
+ xpath.select(`//*[local-name(.)='Contents']`, doc)
101
+ );
102
+ log.debug(`Parsed ${driverNodes.length} entries from storage XML`);
103
+ if (_.isEmpty(driverNodes)) {
104
+ throw new Error('Cannot retrieve any valid Chromedriver entries from the storage config');
105
+ }
106
+
107
+ const promises = [];
108
+ const chunk = [];
109
+ /** @type {ChromedriverDetailsMapping} */
110
+ const mapping = {};
111
+ for (const driverNode of driverNodes) {
112
+ const k = extractNodeText(findChildNode(driverNode, 'Key'));
113
+ if (!_.includes(k, '/chromedriver_')) {
114
+ continue;
115
+ }
116
+ const key = String(k);
117
+
118
+ const etag = extractNodeText(findChildNode(driverNode, 'ETag'));
119
+ if (!etag) {
120
+ log.debug(`The entry '${key}' does not contain the checksum. Skipping it`);
121
+ continue;
122
+ }
123
+
124
+ const filename = path.basename(key);
125
+ const osNameMatch = /_([a-z]+)/i.exec(filename);
126
+ if (!osNameMatch) {
127
+ log.debug(`The entry '${key}' does not contain valid OS name. Skipping it`);
128
+ continue;
129
+ }
130
+
131
+ /** @type {ChromedriverDetails} */
132
+ const cdInfo = {
133
+ url: `${GOOGLEAPIS_CDN}/${key}`,
134
+ etag: _.trim(etag, '"'),
135
+ version: /** @type {string} */ (_.first(key.split('/'))),
136
+ minBrowserVersion: null,
137
+ os: {
138
+ name: osNameMatch[1],
139
+ arch: filename.includes(ARCH.X64) ? ARCH.X64 : ARCH.X86,
140
+ cpu: APPLE_ARM_SUFFIXES.some((suffix) => filename.includes(suffix)) ? CPU.ARM : CPU.INTEL,
141
+ }
142
+ };
143
+ mapping[key] = cdInfo;
144
+
145
+ const notesPath = `${cdInfo.version}/notes.txt`;
146
+ const isNotesPresent = !!driverNodes.reduce(
147
+ (acc, node) => Boolean(acc || findChildNode(node, 'Key', notesPath)),
148
+ false
149
+ );
150
+ if (!isNotesPresent) {
151
+ cdInfo.minBrowserVersion = null;
152
+ if (shouldParseNotes) {
153
+ log.info(`The entry '${key}' does not contain any notes. Skipping it`);
154
+ }
155
+ continue;
156
+ } else if (!shouldParseNotes) {
157
+ continue;
158
+ }
159
+
160
+ const promise = B.resolve(retrieveAdditionalDriverInfo(key, `${GOOGLEAPIS_CDN}/${notesPath}`, cdInfo));
161
+ promises.push(promise);
162
+ chunk.push(promise);
163
+ if (chunk.length >= MAX_PARALLEL_DOWNLOADS) {
164
+ await B.any(chunk);
165
+ }
166
+ _.remove(chunk, (p) => p.isFulfilled());
167
+ }
168
+ await B.all(promises);
169
+ log.info(`The total count of entries in the mapping: ${_.size(mapping)}`);
170
+ return mapping;
171
+ }
172
+
173
+ /**
174
+ * Downloads chromedriver release notes and puts them
175
+ * into the dictionary argument
176
+ *
177
+ * The method call mutates by merging `AdditionalDriverDetails`
178
+ * @param {string} driverKey - Driver version plus archive name
179
+ * @param {string} notesUrl - The URL of chromedriver notes
180
+ * @param {ChromedriverDetails} infoDict - The dictionary containing driver info.
181
+ * @param {number} timeout
182
+ * @throws {Error} if the release notes cannot be downloaded
183
+ */
184
+ async function retrieveAdditionalDriverInfo(driverKey, notesUrl, infoDict, timeout = STORAGE_REQ_TIMEOUT_MS) {
185
+ const notes = await retrieveData(
186
+ notesUrl,
187
+ {
188
+ 'user-agent': 'appium',
189
+ accept: '*/*',
190
+ },
191
+ {timeout}
192
+ );
193
+ const {minBrowserVersion} = parseNotes(notes);
194
+ if (!minBrowserVersion) {
195
+ log.debug(
196
+ `The driver '${driverKey}' does not contain valid release notes at ${notesUrl}. ` +
197
+ `Skipping it`
198
+ );
199
+ return;
200
+ }
201
+ infoDict.minBrowserVersion = minBrowserVersion;
202
+ }
203
+
204
+ /**
205
+ * @typedef {import('../types').SyncOptions} SyncOptions
206
+ * @typedef {import('../types').OSInfo} OSInfo
207
+ * @typedef {import('../types').ChromedriverDetails} ChromedriverDetails
208
+ * @typedef {import('../types').ChromedriverDetailsMapping} ChromedriverDetailsMapping
209
+ */
@@ -1,23 +1,28 @@
1
1
  import {
2
2
  getChromedriverDir,
3
- CD_CDN,
4
3
  retrieveData,
5
4
  getOsInfo,
6
- OS,
7
- X64,
8
- X86,
9
- APPLE_ARM_SUFFIXES,
10
5
  convertToInt,
11
- } from './utils';
6
+ getCpuType,
7
+ } from '../utils';
12
8
  import _ from 'lodash';
13
- import xpath from 'xpath';
14
- import {DOMParser} from '@xmldom/xmldom';
15
9
  import B from 'bluebird';
16
10
  import path from 'path';
17
- import os from 'os';
18
11
  import {system, fs, logger, tempDir, zip, util, net} from '@appium/support';
12
+ import {
13
+ STORAGE_REQ_TIMEOUT_MS,
14
+ GOOGLEAPIS_CDN,
15
+ USER_AGENT,
16
+ CHROMELABS_URL,
17
+ ARCH,
18
+ OS,
19
+ CPU,
20
+ } from '../constants';
21
+ import {parseGoogleapiStorageXml} from './googleapis';
22
+ import {parseKnownGoodVersionsWithDownloadsJson} from './chromelabs';
23
+ import {compareVersions} from 'compare-versions';
24
+ import semver from 'semver';
19
25
 
20
- const TIMEOUT_MS = 15000;
21
26
  const MAX_PARALLEL_DOWNLOADS = 5;
22
27
 
23
28
  const log = logger.getLogger('ChromedriverStorageClient');
@@ -33,182 +38,19 @@ async function isCrcOk(src, checksum) {
33
38
  return _.toLower(md5) === _.toLower(checksum);
34
39
  }
35
40
 
36
- /**
37
- *
38
- * @param {Node|Attr} parent
39
- * @param {string?} childName
40
- * @param {string?} text
41
- * @returns
42
- */
43
- function findChildNode(parent, childName = null, text = null) {
44
- if (!childName && !text) {
45
- return null;
46
- }
47
- if (!parent.hasChildNodes()) {
48
- return null;
49
- }
50
-
51
- for (let childNodeIdx = 0; childNodeIdx < parent.childNodes.length; childNodeIdx++) {
52
- const childNode = /** @type {Element|Attr} */ (parent.childNodes[childNodeIdx]);
53
- if (childName && !text && childName === childNode.localName) {
54
- return childNode;
55
- }
56
- if (text) {
57
- const childText = extractNodeText(childNode);
58
- if (!childText) {
59
- continue;
60
- }
61
- if (childName && childName === childNode.localName && text === childText) {
62
- return childNode;
63
- }
64
- if (!childName && text === childText) {
65
- return childNode;
66
- }
67
- }
68
- }
69
- return null;
70
- }
71
-
72
- /**
73
- *
74
- * @param {Node?} node
75
- * @returns
76
- */
77
- function extractNodeText(node) {
78
- return !node || !node.firstChild || !util.hasValue(node.firstChild.nodeValue)
79
- ? null
80
- : node.firstChild.nodeValue;
81
- }
82
-
83
41
  export class ChromedriverStorageClient {
84
42
  /**
85
43
  *
86
- * @param {import('./types').ChromedriverStorageClientOpts} args
44
+ * @param {import('../types').ChromedriverStorageClientOpts} args
87
45
  */
88
46
  constructor(args = {}) {
89
- const {chromedriverDir = getChromedriverDir(), timeout = TIMEOUT_MS} = args;
47
+ const {chromedriverDir = getChromedriverDir(), timeout = STORAGE_REQ_TIMEOUT_MS} = args;
90
48
  this.chromedriverDir = chromedriverDir;
91
49
  this.timeout = timeout;
92
50
  /** @type {ChromedriverDetailsMapping} */
93
51
  this.mapping = {};
94
52
  }
95
53
 
96
- /**
97
- * Gets additional chromedriver details from chromedriver
98
- * release notes
99
- *
100
- * @param {string} content - Release notes of the corresponding chromedriver
101
- * @returns {import('./types').AdditionalDriverDetails}
102
- */
103
- parseNotes(content) {
104
- const result = {};
105
- const versionMatch = /^\s*[-]+ChromeDriver[\D]+([\d.]+)/im.exec(content);
106
- if (versionMatch) {
107
- result.version = versionMatch[1];
108
- }
109
- const minBrowserVersionMatch = /^\s*Supports Chrome[\D]+(\d+)/im.exec(content);
110
- if (minBrowserVersionMatch) {
111
- result.minBrowserVersion = minBrowserVersionMatch[1];
112
- }
113
- return result;
114
- }
115
-
116
- /**
117
- * Downloads chromedriver release notes and puts them
118
- * into the dictionary argument
119
- *
120
- * The method call mutates by merging `AdditionalDriverDetails`
121
- * @param {string} driverKey - Driver version plus archive name
122
- * @param {string} notesUrl - The URL of chromedriver notes
123
- * @param {ChromedriverDetails} infoDict - The dictionary containing driver info.
124
- * @throws {Error} if the release notes cannot be downloaded
125
- */
126
- async retrieveAdditionalDriverInfo(driverKey, notesUrl, infoDict) {
127
- const notes = await retrieveData(
128
- notesUrl,
129
- {
130
- 'user-agent': 'appium',
131
- accept: '*/*',
132
- },
133
- {timeout: this.timeout}
134
- );
135
- const {minBrowserVersion} = this.parseNotes(notes);
136
- if (!minBrowserVersion) {
137
- log.debug(
138
- `The driver '${driverKey}' does not contain valid release notes at ${notesUrl}. ` +
139
- `Skipping it`
140
- );
141
- return;
142
- }
143
- infoDict.minBrowserVersion = minBrowserVersion;
144
- }
145
-
146
- /**
147
- * Parses chromedriver storage XML and stores
148
- * the parsed results into `this.mapping`
149
- *
150
- * @param {Document} doc - The DOM representation
151
- * of the chromedriver storage XML
152
- * @param {boolean} shouldParseNotes [true] - If set to `true`
153
- * then additional drivers information is going to be parsed
154
- * and assigned to `this.mapping`
155
- */
156
- async parseStorageXml(doc, shouldParseNotes = true) {
157
- const driverNodes = /** @type {Array<Node|Attr>} */ (
158
- xpath.select(`//*[local-name(.)='Contents']`, doc)
159
- );
160
- log.debug(`Parsed ${driverNodes.length} entries from storage XML`);
161
- if (_.isEmpty(driverNodes)) {
162
- return;
163
- }
164
-
165
- const promises = [];
166
- for (const driverNode of driverNodes) {
167
- const k = extractNodeText(findChildNode(driverNode, 'Key'));
168
- if (!_.includes(k, '/chromedriver_')) {
169
- continue;
170
- }
171
- const key = String(k);
172
-
173
- const etag = extractNodeText(findChildNode(driverNode, 'ETag'));
174
- if (!etag) {
175
- log.debug(`The entry '${key}' does not contain the checksum. Skipping it`);
176
- continue;
177
- }
178
-
179
- /** @type {ChromedriverDetails} */
180
- const cdInfo = {
181
- url: `${CD_CDN}/${key}`,
182
- etag: _.trim(etag, '"'),
183
- version: /** @type {string} */ (_.first(key.split('/'))),
184
- minBrowserVersion: null,
185
- };
186
- this.mapping[key] = cdInfo;
187
-
188
- const notesPath = `${cdInfo.version}/notes.txt`;
189
- const isNotesPresent = !!driverNodes.reduce(
190
- (acc, node) => Boolean(acc || findChildNode(node, 'Key', notesPath)),
191
- false
192
- );
193
- if (!isNotesPresent) {
194
- cdInfo.minBrowserVersion = null;
195
- if (shouldParseNotes) {
196
- log.info(`The entry '${key}' does not contain any notes. Skipping it`);
197
- }
198
- continue;
199
- } else if (!shouldParseNotes) {
200
- continue;
201
- }
202
-
203
- promises.push(this.retrieveAdditionalDriverInfo(key, `${CD_CDN}/${notesPath}`, cdInfo));
204
- if (promises.length % MAX_PARALLEL_DOWNLOADS === 0) {
205
- await B.all(promises);
206
- }
207
- }
208
- await B.all(promises);
209
- log.info(`The total count of entries in the mapping: ${_.size(this.mapping)}`);
210
- }
211
-
212
54
  /**
213
55
  * Retrieves chromedriver mapping from the storage
214
56
  *
@@ -218,17 +60,20 @@ export class ChromedriverStorageClient {
218
60
  * @returns {Promise<ChromedriverDetailsMapping>}
219
61
  */
220
62
  async retrieveMapping(shouldParseNotes = true) {
221
- const xml = await retrieveData(
222
- CD_CDN,
223
- {
224
- 'user-agent': 'appium',
225
- accept: 'application/xml, */*',
226
- },
227
- {timeout: this.timeout}
228
- );
229
- const doc = new DOMParser().parseFromString(xml);
230
- await this.parseStorageXml(doc, shouldParseNotes);
231
- return _.cloneDeep(this.mapping);
63
+ const [xmlStr, jsonStr] = await B.all([
64
+ [GOOGLEAPIS_CDN, 'application/xml'],
65
+ [`${CHROMELABS_URL}/chrome-for-testing/known-good-versions-with-downloads.json`, 'application/json'],
66
+ ]
67
+ .map(([url, contentType]) => url
68
+ ? retrieveData(url, {
69
+ 'user-agent': USER_AGENT,
70
+ accept: `${contentType}, */*`,
71
+ }, {timeout: this.timeout})
72
+ : B.resolve()
73
+ ));
74
+ this.mapping = xmlStr ? await parseGoogleapiStorageXml(xmlStr, shouldParseNotes) : {};
75
+ Object.assign(this.mapping, parseKnownGoodVersionsWithDownloadsJson(jsonStr));
76
+ return this.mapping;
232
77
  }
233
78
 
234
79
  /**
@@ -328,30 +173,93 @@ export class ChromedriverStorageClient {
328
173
 
329
174
  if (!_.isEmpty(osInfo)) {
330
175
  // Filter out drivers for unsupported system architectures
331
- let {name, arch} = osInfo;
332
- if (arch === X64 && !driversToSync.some((cdName) => cdName.includes(`_${name}${X64}`))) {
333
- // Fall back to x86 build if x64 one is not available for the given OS
334
- arch = X86;
176
+ const {name, arch, cpu = getCpuType()} = osInfo;
177
+ log.debug(`Selecting chromedrivers whose platform matches to ${name}:${cpu}${arch}`);
178
+ let result = driversToSync.filter((cdName) => this.doesMatchForOsInfo(cdName, osInfo));
179
+ if (_.isEmpty(result) && arch === ARCH.X64 && cpu === CPU.INTEL) {
180
+ // Fallback to X86 if X64 architecture is not available for this driver
181
+ result = driversToSync.filter((cdName) => this.doesMatchForOsInfo(cdName, {
182
+ name, arch: ARCH.X86, cpu
183
+ }));
335
184
  }
336
- // https://stackoverflow.com/questions/65146751/detecting-apple-silicon-mac-in-javascript
337
- if (name === OS.mac && _.includes(_.toLower(os.cpus()[0].model), 'apple')) {
338
- for (const armSuffix of APPLE_ARM_SUFFIXES) {
339
- if (driversToSync.some((cdName) => cdName.includes(armSuffix))) {
340
- // prefer executable for ARM arch if present
341
- arch = armSuffix;
342
- break;
343
- }
344
- }
185
+ if (_.isEmpty(result) && name === OS.MAC && cpu === CPU.ARM) {
186
+ // Fallback to Intel/Rosetta if ARM architecture is not available for this driver
187
+ result = driversToSync.filter((cdName) => this.doesMatchForOsInfo(cdName, {
188
+ name, arch, cpu: CPU.INTEL
189
+ }));
345
190
  }
346
- log.debug(`Selecting chromedrivers whose platform matches to ${name}${arch}`);
347
- const platformRe = new RegExp(`(\\b|_)${name}${arch}\\b`);
348
- driversToSync = driversToSync.filter((cdName) => platformRe.test(cdName));
191
+ driversToSync = result;
349
192
  log.debug(`Got ${util.pluralize('item', driversToSync.length, true)}`);
350
193
  }
351
194
 
195
+ if (!_.isEmpty(driversToSync)) {
196
+ log.debug('Excluding older patches if present');
197
+ /** @type {{[key: string]: string[]}} */
198
+ const patchesMap = {};
199
+ // Older chromedrivers must not be excluded as they follow a different
200
+ // versioning pattern
201
+ const versionWithPatchPattern = /\d+\.\d+\.\d+\.\d+/;
202
+ const selectedVersions = new Set();
203
+ for (const cdName of driversToSync) {
204
+ const cdVersion = this.mapping[cdName].version;
205
+ if (!versionWithPatchPattern.test(cdVersion)) {
206
+ selectedVersions.add(cdVersion);
207
+ continue;
208
+ }
209
+ const verObj = semver.parse(cdVersion, {loose: true});
210
+ if (!verObj) {
211
+ continue;
212
+ }
213
+ if (!_.isArray(patchesMap[verObj.major])) {
214
+ patchesMap[verObj.major] = [];
215
+ }
216
+ patchesMap[verObj.major].push(cdVersion);
217
+ }
218
+ for (const majorVersion of _.keys(patchesMap)) {
219
+ if (patchesMap[majorVersion].length <= 1) {
220
+ continue;
221
+ }
222
+ patchesMap[majorVersion].sort(
223
+ (/** @type {string} */ a, /** @type {string}} */ b) => compareVersions(b, a)
224
+ );
225
+ }
226
+ if (!_.isEmpty(patchesMap)) {
227
+ log.debug('Versions mapping: ' + JSON.stringify(patchesMap, null, 2));
228
+ for (const sortedVersions of _.values(patchesMap)) {
229
+ selectedVersions.add(sortedVersions[0]);
230
+ }
231
+ driversToSync = driversToSync.filter(
232
+ (cdName) => selectedVersions.has(this.mapping[cdName].version)
233
+ );
234
+ }
235
+ }
236
+
352
237
  return driversToSync;
353
238
  }
354
239
 
240
+ /**
241
+ * Checks whether the given chromedriver matches the operating system to run on
242
+ *
243
+ * @param {string} cdName
244
+ * @param {OSInfo} osInfo
245
+ * @returns {boolean}
246
+ */
247
+ doesMatchForOsInfo(cdName, {name, arch, cpu}) {
248
+ const cdInfo = this.mapping[cdName];
249
+ if (!cdInfo) {
250
+ return false;
251
+ }
252
+
253
+ if (cdInfo.os.name !== name || cdInfo.os.arch !== arch) {
254
+ return false;
255
+ }
256
+ if (cpu && cdInfo.os.cpu && this.mapping[cdName].os.cpu !== cpu) {
257
+ return false;
258
+ }
259
+
260
+ return true;
261
+ }
262
+
355
263
  /**
356
264
  * Retrieves the given chromedriver from the storage
357
265
  * and unpacks it into `this.chromedriverDir` folder
@@ -374,7 +282,7 @@ export class ChromedriverStorageClient {
374
282
  try {
375
283
  await net.downloadFile(url, archivePath, {
376
284
  isMetered: false,
377
- timeout: TIMEOUT_MS,
285
+ timeout: STORAGE_REQ_TIMEOUT_MS,
378
286
  });
379
287
  } catch (e) {
380
288
  const err = /** @type {Error} */ (e);
@@ -385,7 +293,7 @@ export class ChromedriverStorageClient {
385
293
  log.error(msg);
386
294
  return false;
387
295
  }
388
- if (!(await isCrcOk(archivePath, etag))) {
296
+ if (etag && !(await isCrcOk(archivePath, etag))) {
389
297
  const msg = `The checksum for the downloaded chromedriver '${driverKey}' did not match`;
390
298
  if (isStrict) {
391
299
  throw new Error(msg);
@@ -442,20 +350,23 @@ export class ChromedriverStorageClient {
442
350
  */
443
351
  const synchronizedDrivers = [];
444
352
  const promises = [];
353
+ const chunk = [];
445
354
  const archivesRoot = await tempDir.openDir();
446
355
  try {
447
356
  for (const [idx, driverKey] of driversToSync.entries()) {
448
- promises.push(
357
+ const promise = B.resolve(
449
358
  (async () => {
450
359
  if (await this.retrieveDriver(idx, driverKey, archivesRoot, !_.isEmpty(opts))) {
451
360
  synchronizedDrivers.push(driverKey);
452
361
  }
453
362
  })()
454
363
  );
455
-
456
- if (promises.length % MAX_PARALLEL_DOWNLOADS === 0) {
457
- await B.all(promises);
364
+ promises.push(promise);
365
+ chunk.push(promise);
366
+ if (chunk.length >= MAX_PARALLEL_DOWNLOADS) {
367
+ await B.any(chunk);
458
368
  }
369
+ _.remove(chunk, (p) => p.isFulfilled());
459
370
  }
460
371
  await B.all(promises);
461
372
  } finally {
@@ -464,7 +375,7 @@ export class ChromedriverStorageClient {
464
375
  if (!_.isEmpty(synchronizedDrivers)) {
465
376
  log.info(
466
377
  `Successfully synchronized ` +
467
- `${util.pluralize('chromedriver', synchronizedDrivers.length, true)}`
378
+ `${util.pluralize('chromedriver', synchronizedDrivers.length, true)}`
468
379
  );
469
380
  } else {
470
381
  log.info(`No chromedrivers were synchronized`);
@@ -476,8 +387,8 @@ export class ChromedriverStorageClient {
476
387
  export default ChromedriverStorageClient;
477
388
 
478
389
  /**
479
- * @typedef {import('./types').SyncOptions} SyncOptions
480
- * @typedef {import('./types').OSInfo} OSInfo
481
- * @typedef {import('./types').ChromedriverDetails} ChromedriverDetails
482
- * @typedef {import('./types').ChromedriverDetailsMapping} ChromedriverDetailsMapping
390
+ * @typedef {import('../types').SyncOptions} SyncOptions
391
+ * @typedef {import('../types').OSInfo} OSInfo
392
+ * @typedef {import('../types').ChromedriverDetails} ChromedriverDetails
393
+ * @typedef {import('../types').ChromedriverDetailsMapping} ChromedriverDetailsMapping
483
394
  */
package/lib/types.ts CHANGED
@@ -46,15 +46,20 @@ export interface SyncOptions {
46
46
  export interface OSInfo {
47
47
  /**
48
48
  * The architecture of the host OS.
49
- * Can be either `32` or `64`
49
+ * Can be either `32`, `64``
50
50
  */
51
51
  arch: string;
52
52
  /**
53
53
  *
54
54
  * The name of the host OS.
55
- * Can be either `mac`, `windows` or `linux`
55
+ * Can be either `mac`, `win` or `linux`
56
56
  */
57
57
  name: string;
58
+ /**
59
+ * The cpu type of the host OS.
60
+ * Can be either `intel`, `arm`. `intel` is assumed by default
61
+ */
62
+ cpu?: string;
58
63
  }
59
64
 
60
65
  /**
@@ -68,12 +73,13 @@ export interface ChromedriverDetails {
68
73
  /**
69
74
  * CRC of driver archive
70
75
  */
71
- etag: string;
76
+ etag: string | null;
72
77
  /**
73
78
  * Chromedriver version
74
79
  */
75
80
  version: string;
76
81
  minBrowserVersion: string | null;
82
+ os: OSInfo;
77
83
  }
78
84
 
79
85
  /**