-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: auto ubuntu packages download for local browsers #1036
Conversation
@@ -8,7 +8,6 @@ export default { | |||
79: 707231, | |||
80: 722374, | |||
81: 737198, | |||
82: 750023, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed as API is not responding to request "which version of chromedriver should i use with this version of chrome": https://chromedriver.storage.googleapis.com/LATEST_RELEASE_82
name: Collect ubuntu browser dependencies | ||
on: | ||
schedule: | ||
- cron: 0 0 1 * * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
once per month
package.json
Outdated
@@ -9,10 +9,11 @@ | |||
], | |||
"scripts": { | |||
"build": "tsc --build && npm run copy-static && npm run build-bundles", | |||
"copy-static": "copyfiles 'src/browser/client-scripts/*' build", | |||
"copy-static": "copyfiles 'src/browser/client-scripts/*' 'src/**/*.json' build", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also copy these autogenerated json
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks dangerous because it can be any json file, for example, stub for unit-tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restricted to "src//[!cache]*/autogenerated//*.json"
So, jsons need to be in "autogenerated" directory
const [chromeDriverPath] = await Promise.all([ | ||
installChromeDriver(chromeVersion), | ||
installChrome(chromeVersion), | ||
shouldInstallUbuntuPackageDependencies ? installUbuntuPackageDependencies() : null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure ubuntu packages are ready before spawning child process
src/browser-installer/install.ts
Outdated
return await Promise.all([ | ||
installChrome(browserVersion, { force }), | ||
shouldInstallWebDriver && installChromeDriver(browserVersion, { force }), | ||
needToInstallUbuntuPackages && installUbuntuPackageDependencies(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure ubuntu package are ready (e.g. for devtools)
await fs.outputJSON(this._sharedObjectsMapPath, sortObject(sharedObjectsMap), { spaces: 4 }); | ||
await fs.outputJSON(this._processedBrowsersCachePath, sortObject(processedBrowsers), { spaces: 4 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using 4 spaces because the linter wants it
// Those are couldn't be seen with readelf -d | ||
export const EXTRA_FIREFOX_SHARED_OBJECTS = ["libdbus-glib-1.so.2", "libXt.so.6"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually there is a note in dynamic section of an elf file "which libraries does it load", but for a long time, starting from the very first versions, firefox needs these 2 shared objects, and they are not recorded in dynamic section of an elf file
|
||
logger.log(`Fetched ${browserVersions.length} browser milestones`); | ||
|
||
const browsersToDownload = cache.filterProcessedBrowsers(browserVersions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Browsers are only downloaded once
If we would like to add new ubuntu release, we would not need to download any browser
return "libc6"; | ||
} | ||
|
||
const relevantPackageName = _.minBy(packages, packageName => calcLevenshtein(sharedObject, packageName)) as string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using levenshtein in order to resolve shared objects to their main packages, and not just to some package, which is more popular (in example, if libnss3.so is used in firefox, it would return "libnss3" even if firefox is more popular)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Directory with ubuntu binaries call wrappers
@@ -0,0 +1,43 @@ | |||
name: Collect ubuntu browser dependencies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scheduled action:
- Collects ubuntu dependencies for new browser versions
- Creates a PR to add dependency if there is any new library
- If there are no extra dependencies, do nothing.
- If there is extra browser, but no new dependencies, creates PR to update cache, so it would not need to download the browser later
dbacdc9
to
6f9aeaf
Compare
package.json
Outdated
@@ -9,10 +9,11 @@ | |||
], | |||
"scripts": { | |||
"build": "tsc --build && npm run copy-static && npm run build-bundles", | |||
"copy-static": "copyfiles 'src/browser/client-scripts/*' build", | |||
"copy-static": "copyfiles 'src/browser/client-scripts/*' 'src/**/*.json' build", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks dangerous because it can be any json file, for example, stub for unit-tests
async function main(): Promise<void> { | ||
const ubuntuMilestone = await getUbuntuMilestone(); | ||
|
||
logger.log(`Detected ubuntu release: "${ubuntuMilestone}"`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't it be undefined/null here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It relies on VERSION_ID
in /etc/os-release
If it is ubuntu, VERSION_ID
exists and defined
getUbuntuMilestone
implies it is ubuntu
So, no
import { getUbuntuMilestone, writeUbuntuPackageDependencies } from "../browser-installer/ubuntu-packages"; | ||
import logger from "../utils/logger"; | ||
|
||
const createResolveSharedObjectToPackageName = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so create
or resolve
? mb, createSharedObjectToPackageNameResolver
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createSharedObjectToPackageNameResolver
Resolver is a noun. We could call what is returned as Resolver
only if its object with some method, but its function
This function creates function "resolveSharedObjectToPackageName", so it is called "createResolve..."
}; | ||
|
||
export const getBinarySharedObjectDependencies = async (binaryPath: string): Promise<string[]> => { | ||
const sharedObjectRegExp = /^\s*\dx\d+\s\(NEEDED\)\s*Shared library: \[(.*)\]/gm; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please, add an example for readElf
to understand what is going on here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In code section?
There is in test section
while (regExpResult && regExpResult[1]) { | ||
sharedObjectDependencies.push(regExpResult[1]); | ||
|
||
regExpResult = sharedObjectRegExp.exec(readElfResult); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does it work? why does the second call return a different value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works just like it does in C (where you dont expect magic like this)
Because of the "g" flag, regular expression state is saved (in C it was saved in the global variable, i assume in js there is some kind of closure)
describe("getBinarySharedObjectDependencies", () => { | ||
it("should return binary direct shared object deps", async () => { | ||
readElfStub.resolves(` | ||
Dynamic section at offset 0xb00 contains 26 entries: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
o_O
642e41f
to
4c3fd27
Compare
|
||
const [chromeDriverPath] = await Promise.all([ | ||
const [chromeDriverPath, randomPort, chromeDriverEnv] = await Promise.all([ | ||
installChromeDriver(chromeVersion), | ||
installChrome(chromeVersion), | ||
shouldInstallUbuntuPackageDependencies ? installUbuntuPackageDependencies() : null, | ||
getPort(), | ||
isUbuntu() | ||
.then(isUbuntu => (isUbuntu ? getUbuntuLinkerEnv() : null)) | ||
.then(extraEnv => (extraEnv ? { ...process.env, ...extraEnv } : process.env)), | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now we dont install browser and ubuntu packages here, and instead we assume it is already installed by "browser-installer/run.ts", which runs "browser-installer/install.ts" before calling "run*Driver"
export const runBrowserDriver = async ( | ||
driverName: SupportedDriver, | ||
browserName: SupportedBrowser, | ||
browserVersion: string, | ||
{ debug = false } = {}, | ||
): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { | ||
switch (driverName) { | ||
case Driver.CHROMEDRIVER: | ||
const installBrowserOpts = { shouldInstallWebDriver: true, shouldInstallUbuntuPackages: true }; | ||
|
||
await installBrowser(browserName, browserVersion, installBrowserOpts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As now we need to install browser before running its driver, we expect "browserName" instead of "driverName" in order to pass "browserName" to "installBrowser"
export const getDriverNameForBrowserName = (browserName: SupportedBrowser): SupportedDriver | null => { | ||
if (browserName === Browser.CHROME || browserName === Browser.CHROMIUM) { | ||
return Driver.CHROMEDRIVER; | ||
export const getNormalizedBrowserName = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As now we dont map "browserName" to "driverName" for "runBrowserDriver", we dont need "getDriverNameForBrowserName", and instead i created this function, so in "installBrowser" we can be sure it only receives valid "browserName" of SupportedBrowser
type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way we can handle cases with invalid browser name outside of "install", so we know, what exactly user wanted to do, and we can provide relevant error message:
- invalid browser name was used in order to run local browser
- invalid browser name was used in order to download a browser
async getWebdriver( | ||
browserName: SupportedBrowser, | ||
browserVersion: string, | ||
browserName?: string, | ||
browserVersion?: string, | ||
{ debug = false } = {}, | ||
): ReturnType<typeof this.createWebdriverProcess> { | ||
const driverName = getDriverNameForBrowserName(browserName); | ||
const browserNameNormalized = getNormalizedBrowserName(browserName); | ||
|
||
if (!driverName) { | ||
if (!browserNameNormalized) { | ||
throw new Error( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we now dont want to map "browserName" to "driverName", we still need to check if "browserName" is valid (because this method is called inside NewBrowser with its config's browserName, which is user provided value, which can be invalid)
const executablePath = await installBrowser( | ||
this._config.desiredCapabilities?.browserName as SupportedBrowser, | ||
this._config.desiredCapabilities?.browserVersion as string, | ||
normalizedBrowserName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"installBrowser" now only wants normalized browser name (so we could pull out "invalid browser name" handling in order to throw relevant errors)
this._config.desiredCapabilities?.browserVersion as string, | ||
normalizedBrowserName, | ||
this._config.desiredCapabilities?.browserVersion, | ||
{ shouldInstallWebDriver: false, shouldInstallUbuntuPackages: true }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicitly setting "should install" options, as they are all set to "false" now by default.
In "_addExecutablePath" we dont need to install webdriver as user might want to run browser in "devtools" protocol
907c990
to
8c6d6c6
Compare
const downloadProgressCallback: DownloadProgressCallback = (done, total = 100) => { | ||
if (!bar) { | ||
const totalMB = Math.round((totalBytes / BYTES_PER_MEGABYTE) * 100) / 100; | ||
bar = progressBar.create(totalMB, 0, { filename: `${browserName}@${browserVersion}` }); | ||
bar = progressBar.create(100, 0, { filename: `${browserName}@${browserVersion}` }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now use percents as more versatile progress bar unit.
This way we can also use the same progress bar for ubuntu packages download
src/browser-installer/utils.ts
Outdated
@@ -129,13 +142,42 @@ const getCacheDir = (envValueOverride = process.env.TESTPLANE_BROWSERS_PATH): st | |||
export const getRegistryPath = (envValueOverride?: string): string => | |||
path.join(getCacheDir(envValueOverride), "registry.json"); | |||
|
|||
export const readRegistry = (registryPath: string): Registry => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided to extend registry so it would store not only binary paths, but could store anything else
This way we can manage os packages by this registry file too.
8c6d6c6
to
bd17127
Compare
return Driver.CHROMEDRIVER; | ||
export const getNormalizedBrowserName = ( | ||
browserName?: string, | ||
): Exclude<SupportedBrowser, typeof Browser.CHROMIUM> | null => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need exclude here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We dont need to, i just wanted to show "chromium can't be returned here", because chromium
maps to chrome
, as installing "chrome" and "chromium" has a single entry point, being "chrome", as "chromium" is not a valid W3C browserName
27c49dd
to
e43d792
Compare
…r-installer/ubuntu-packages/
e43d792
to
bde19e0
Compare
CI side (collect-ubuntu-browser-dependencies, is not being built):
Cache is stored in VCS and is splitted to 2 files:
Client side (src/browser-installer/ubuntu-packages, being packed):
LD_LIBRARY_PATH
when launching webdriver so it could populate to browser, so browser would use downloaded packages