Skip to content

Commit 10a1a5b

Browse files
feat: Add support of the new chromedriver storage (#333)
1 parent ddc7ccf commit 10a1a5b

13 files changed

+714
-335
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ the server returns a proper list of stored drivers in response to requests havin
7474
`Accept: application/xml` header. An example XML could be retrieved from the original URL using
7575
`curl -H 'Accept: application/xml' https://chromedriver.storage.googleapis.com` command.
7676

77+
Since version 5.6 the second environment variable has been added: `CHROMELABS_URL`. By default, it points
78+
to https://googlechromelabs.github.io, and is expected to contain the actual prefix of
79+
[Chrome for Testing availability](https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints)
80+
JSON API. This API allows retrieval of chromedrivers whose versions are greater than 114.
81+
82+
Similarly to the above it could be also defined in the .npmrc file:
83+
84+
```bash
85+
chromelabs_url=https://googlechromelabs.github.io
86+
```
87+
88+
You may also want to skip checking for older Chromedriver versions by providing an
89+
empty value to the `CHROMEDRIVER_CDNURL` variable.
90+
7791
## Usage
7892

7993
```js

lib/chromedriver.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ import {
1010
getChromedriverDir,
1111
CHROMEDRIVER_CHROME_MAPPING,
1212
getChromedriverBinaryPath,
13-
CD_CDN,
1413
generateLogPrefix,
1514
} from './utils';
1615
import semver from 'semver';
1716
import _ from 'lodash';
1817
import path from 'path';
1918
import {compareVersions} from 'compare-versions';
20-
import ChromedriverStorageClient from './storage-client';
19+
import ChromedriverStorageClient from './storage-client/storage-client';
2120
import {toW3cCapNames, getCapValue} from './protocol-helpers';
2221

2322
const NEW_CD_VERSION_FORMAT_MAJOR_VERSION = 73;
@@ -349,7 +348,8 @@ export class Chromedriver extends events.EventEmitter {
349348
}
350349
const retrievedMapping = await this.storageClient.retrieveMapping();
351350
this.log.debug(
352-
'Got chromedrivers mapping from the storage: ' + JSON.stringify(retrievedMapping, null, 2)
351+
'Got chromedrivers mapping from the storage: ' +
352+
_.truncate(JSON.stringify(retrievedMapping, null, 2), {length: 500})
353353
);
354354
const driverKeys = await this.storageClient.syncDrivers({
355355
minBrowserVersion: chromeVersion.major,
@@ -447,8 +447,7 @@ export class Chromedriver extends events.EventEmitter {
447447
} catch (e) {
448448
const err = /** @type {Error} */ (e);
449449
this.log.warn(
450-
`Cannot synchronize local chromedrivers with the remote storage at ${CD_CDN}: ` +
451-
err.message
450+
`Cannot synchronize local chromedrivers with the remote storage: ${err.message}`
452451
);
453452
this.log.debug(err.stack);
454453
}

lib/constants.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const STORAGE_REQ_TIMEOUT_MS = 15000;
2+
export const GOOGLEAPIS_CDN =
3+
process.env.npm_config_chromedriver_cdnurl ||
4+
process.env.CHROMEDRIVER_CDNURL ||
5+
'https://chromedriver.storage.googleapis.com';
6+
export const USER_AGENT = 'appium';
7+
export const CHROMELABS_URL =
8+
process.env.npm_config_chromelabs_url ||
9+
process.env.CHROMELABS_URL ||
10+
'https://googlechromelabs.github.io';
11+
export const OS = {
12+
LINUX: 'linux',
13+
WINDOWS: 'win',
14+
MAC: 'mac',
15+
};
16+
export const ARCH = {
17+
X64: '64',
18+
X86: '32',
19+
};
20+
export const CPU = {
21+
INTEL: 'intel',
22+
ARM: 'arm',
23+
};
24+
export const APPLE_ARM_SUFFIXES = ['64_m1', '_arm64'];

lib/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Chromedriver} from './chromedriver';
2-
export {ChromedriverStorageClient} from './storage-client';
2+
export {ChromedriverStorageClient} from './storage-client/storage-client';
33
export default Chromedriver;
44
export {Chromedriver};
55
export type * from './types';

lib/install.js

+27-10
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
1+
import _ from 'lodash';
12
import { fs, mkdirp } from '@appium/support';
2-
import ChromedriverStorageClient from './storage-client';
3+
import ChromedriverStorageClient from './storage-client/storage-client';
4+
import {parseLatestKnownGoodVersionsJson} from './storage-client/chromelabs';
35
import {
4-
CD_CDN, CD_VER, retrieveData, getOsInfo, getChromedriverDir,
6+
CD_VER, retrieveData, getOsInfo, getChromedriverDir,
57
} from './utils';
8+
import { USER_AGENT, STORAGE_REQ_TIMEOUT_MS, CHROMELABS_URL } from './constants';
69

7-
8-
const DOWNLOAD_TIMEOUT_MS = 15 * 1000;
910
const LATEST_VERSION = 'LATEST';
11+
1012
/**
1113
*
1214
* @param {string} ver
15+
* @returns {Promise<string>}
1316
*/
1417
async function formatCdVersion (ver) {
15-
return ver === LATEST_VERSION
16-
? (await retrieveData(`${CD_CDN}/LATEST_RELEASE`, {
17-
'user-agent': 'appium',
18-
accept: '*/*',
19-
}, { timeout: DOWNLOAD_TIMEOUT_MS })).trim()
20-
: ver;
18+
if (_.toUpper(ver) !== LATEST_VERSION) {
19+
return ver;
20+
}
21+
22+
let jsonStr;
23+
const url = `${CHROMELABS_URL}/chrome-for-testing/last-known-good-versions.json`;
24+
try {
25+
jsonStr = await retrieveData(
26+
url, {
27+
'user-agent': USER_AGENT,
28+
accept: `application/json, */*`,
29+
}, {timeout: STORAGE_REQ_TIMEOUT_MS}
30+
);
31+
} catch (e) {
32+
const err = /** @type {Error} */ (e);
33+
throw new Error(`Cannot fetch the latest Chromedriver version. ` +
34+
`Make sure you can access ${url} from your machine or provide a mirror by setting ` +
35+
`a custom value to CHROMELABS_URL enironment variable. Original error: ${err.message}`);
36+
}
37+
return parseLatestKnownGoodVersionsJson(jsonStr);
2138
}
2239

2340
/**

lib/storage-client/chromelabs.js

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import _ from 'lodash';
2+
import path from 'path';
3+
import {logger} from '@appium/support';
4+
import semver from 'semver';
5+
import {ARCH, CPU} from '../constants';
6+
7+
const log = logger.getLogger('ChromedriverChromelabsStorageClient');
8+
9+
/**
10+
* Parses The output of the corresponding JSON API
11+
* that retrieves Chromedriver versions. See
12+
* https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints
13+
* for more details.
14+
*
15+
* @param {string} jsonStr
16+
* @returns {ChromedriverDetailsMapping}
17+
*/
18+
export function parseKnownGoodVersionsWithDownloadsJson(jsonStr) {
19+
let json;
20+
try {
21+
json = JSON.parse(jsonStr);
22+
} catch (e) {
23+
const err = /** @type {Error} */ (e);
24+
throw new Error(`Storage JSON cannot be parsed. Original error: ${err.message}`);
25+
}
26+
/**
27+
* Example output:
28+
* {
29+
* "timestamp":"2023-07-28T13:09:17.042Z",
30+
* "versions":[
31+
* {
32+
* "version":"113.0.5672.0",
33+
* "revision":"1121455",
34+
* "downloads":{
35+
* "chromedriver":[
36+
* {
37+
* "platform":"linux64",
38+
* "url":"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/linux64/chrome-linux64.zip"
39+
* },
40+
* {
41+
* "platform":"mac-arm64",
42+
* "url":"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-arm64/chrome-mac-arm64.zip"
43+
* },
44+
* {
45+
* "platform":"mac-x64",
46+
* "url":"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-x64/chrome-mac-x64.zip"
47+
* },
48+
* {
49+
* "platform":"win32",
50+
* "url":"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win32/chrome-win32.zip"
51+
* },
52+
* {
53+
* "platform":"win64",
54+
* "url":"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win64/chrome-win64.zip"
55+
* }
56+
* ]
57+
* }
58+
* },
59+
* {
60+
* "version":"113.0.5672.35",
61+
* ...
62+
*/
63+
/** @type {ChromedriverDetailsMapping} */
64+
const mapping = {};
65+
if (!_.isArray(json?.versions)) {
66+
log.debug(jsonStr);
67+
throw new Error('The format of the storage JSON is not supported');
68+
}
69+
for (const {version, downloads} of json.versions) {
70+
if (!_.isArray(downloads?.chromedriver)) {
71+
continue;
72+
}
73+
const versionObj = semver.parse(version, {loose: true});
74+
if (!versionObj) {
75+
continue;
76+
}
77+
for (const downloadEntry of downloads.chromedriver) {
78+
if (!downloadEntry?.url || !downloadEntry?.platform) {
79+
continue;
80+
}
81+
const osNameMatch = /^[a-z]+/i.exec(downloadEntry.platform);
82+
if (!osNameMatch) {
83+
log.debug(`The entry '${downloadEntry.url}' does not contain valid platform name. Skipping it`);
84+
continue;
85+
}
86+
const key = `${path.basename(path.dirname(path.dirname(downloadEntry.url)))}/` +
87+
`${path.basename(downloadEntry.url)}`;
88+
mapping[key] = {
89+
url: downloadEntry.url,
90+
etag: null,
91+
version,
92+
minBrowserVersion: `${versionObj.major}`,
93+
os: {
94+
name: osNameMatch[0],
95+
arch: downloadEntry.platform.includes(ARCH.X64) ? ARCH.X64 : ARCH.X86,
96+
cpu: downloadEntry.platform.includes(CPU.ARM) ? CPU.ARM : CPU.INTEL,
97+
}
98+
};
99+
}
100+
}
101+
log.info(`The total count of entries in the mapping: ${_.size(mapping)}`);
102+
return mapping;
103+
}
104+
105+
/**
106+
* Parses The output of the corresponding JSON API
107+
* that retrieves the most recent stable Chromedriver version. See
108+
* https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints
109+
* for more details.
110+
*
111+
* @param {string} jsonStr
112+
* @returns {string} The most recent available chromedriver version
113+
*/
114+
export function parseLatestKnownGoodVersionsJson(jsonStr) {
115+
let json;
116+
try {
117+
json = JSON.parse(jsonStr);
118+
} catch (e) {
119+
const err = /** @type {Error} */ (e);
120+
throw new Error(`Storage JSON cannot be parsed. Original error: ${err.message}`);
121+
}
122+
/**
123+
* Example output:
124+
* "timestamp":"2023-07-28T13:09:17.036Z",
125+
* "channels":{
126+
* "Stable":{
127+
* "channel":"Stable",
128+
* "version":"115.0.5790.102",
129+
* "revision":"1148114"
130+
* ...
131+
*/
132+
if (!json?.channels?.Stable?.version) {
133+
log.debug(jsonStr);
134+
throw new Error('The format of the storage JSON is not supported');
135+
}
136+
return json.channels.Stable.version;
137+
}
138+
139+
/**
140+
* @typedef {import('../types').ChromedriverDetailsMapping} ChromedriverDetailsMapping
141+
*/

0 commit comments

Comments
 (0)