From dec48a95923e21a054c1d70d22c14001a0150293 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 14 Mar 2023 12:01:11 +0100 Subject: [PATCH] feat: implement system channels for chrome in browsers (#9844) --- packages/browsers/src/CLI.ts | 39 +++++++++++---- packages/browsers/src/browsers/browsers.ts | 24 +++++++++- packages/browsers/src/browsers/chrome.ts | 48 ++++++++++++++++++- packages/browsers/src/browsers/types.ts | 7 +++ packages/browsers/src/launcher.ts | 46 +++++++++++++++++- packages/browsers/src/main.ts | 26 ++++++++++ .../browsers/test/src/chrome-data.spec.ts | 34 +++++++++++++ packages/browsers/test/src/fetch.spec.ts | 2 +- 8 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 packages/browsers/src/main.ts diff --git a/packages/browsers/src/CLI.ts b/packages/browsers/src/CLI.ts index 710d48bc6c2..4fc6d75e674 100644 --- a/packages/browsers/src/CLI.ts +++ b/packages/browsers/src/CLI.ts @@ -19,10 +19,18 @@ import yargs from 'yargs'; import {hideBin} from 'yargs/helpers'; import {resolveBuildId} from './browsers/browsers.js'; -import {Browser, BrowserPlatform} from './browsers/types.js'; +import { + Browser, + BrowserPlatform, + ChromeReleaseChannel, +} from './browsers/types.js'; import {detectBrowserPlatform} from './detectPlatform.js'; import {fetch} from './fetch.js'; -import {computeExecutablePath, launch} from './launcher.js'; +import { + computeExecutablePath, + computeSystemExecutablePath, + launch, +} from './launcher.js'; type InstallArgs = { browser: { @@ -41,6 +49,7 @@ type LaunchArgs = { path?: string; platform?: BrowserPlatform; detached: boolean; + system: boolean; }; export class CLI { @@ -132,18 +141,30 @@ export class CLI { this.#definePathParameter(yargs); yargs.option('detached', { type: 'boolean', - desc: 'Whether to detach the child process.', + desc: 'Detach the child process.', + default: false, + }); + yargs.option('system', { + type: 'boolean', + desc: 'Search for a browser installed on the system instead of the cache folder.', default: false, }); }, async argv => { const args = argv as unknown as LaunchArgs; - const executablePath = computeExecutablePath({ - browser: args.browser.name, - buildId: args.browser.buildId, - cacheDir: args.path ?? this.#cachePath, - platform: args.platform, - }); + const executablePath = args.system + ? computeSystemExecutablePath({ + browser: args.browser.name, + // TODO: throw an error if not a ChromeReleaseChannel is provided. + channel: args.browser.buildId as ChromeReleaseChannel, + platform: args.platform, + }) + : computeExecutablePath({ + browser: args.browser.name, + buildId: args.browser.buildId, + cacheDir: args.path ?? this.#cachePath, + platform: args.platform, + }); launch({ executablePath, detached: args.detached, diff --git a/packages/browsers/src/browsers/browsers.ts b/packages/browsers/src/browsers/browsers.ts index 26a43c3e37c..9707c65b0f4 100644 --- a/packages/browsers/src/browsers/browsers.ts +++ b/packages/browsers/src/browsers/browsers.ts @@ -16,7 +16,13 @@ import * as chrome from './chrome.js'; import * as firefox from './firefox.js'; -import {Browser, BrowserPlatform, BrowserTag, ProfileOptions} from './types.js'; +import { + Browser, + BrowserPlatform, + BrowserTag, + ChromeReleaseChannel, + ProfileOptions, +} from './types.js'; export const downloadUrls = { [Browser.CHROME]: chrome.resolveDownloadUrl, @@ -66,3 +72,19 @@ export async function createProfile( throw new Error(`Profile creation is not support for ${browser} yet`); } } + +export function resolveSystemExecutablePath( + browser: Browser, + platform: BrowserPlatform, + channel: ChromeReleaseChannel +): string { + switch (browser) { + case Browser.FIREFOX: + throw new Error( + 'System browser detection is not supported for Firefox yet.' + ); + case Browser.CHROME: + case Browser.CHROMIUM: + return chrome.resolveSystemExecutablePath(platform, channel); + } +} diff --git a/packages/browsers/src/browsers/chrome.ts b/packages/browsers/src/browsers/chrome.ts index af880ce7133..4f000c79280 100644 --- a/packages/browsers/src/browsers/chrome.ts +++ b/packages/browsers/src/browsers/chrome.ts @@ -18,7 +18,7 @@ import path from 'path'; import {httpRequest} from '../httpUtil.js'; -import {BrowserPlatform} from './types.js'; +import {BrowserPlatform, ChromeReleaseChannel} from './types.js'; function archive(platform: BrowserPlatform, buildId: string): string { switch (platform) { @@ -81,7 +81,6 @@ export function relativeExecutablePath( return path.join('chrome-win', 'chrome.exe'); } } - export async function resolveBuildId( platform: BrowserPlatform, // We will need it for other channels/keywords. @@ -118,3 +117,48 @@ export async function resolveBuildId( }); }); } + +export function resolveSystemExecutablePath( + platform: BrowserPlatform, + channel: ChromeReleaseChannel +): string { + switch (platform) { + case BrowserPlatform.WIN64: + case BrowserPlatform.WIN32: + switch (channel) { + case ChromeReleaseChannel.STABLE: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`; + case ChromeReleaseChannel.BETA: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`; + case ChromeReleaseChannel.CANARY: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`; + case ChromeReleaseChannel.DEV: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe`; + } + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.MAC: + switch (channel) { + case ChromeReleaseChannel.STABLE: + return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + case ChromeReleaseChannel.BETA: + return '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta'; + case ChromeReleaseChannel.CANARY: + return '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'; + case ChromeReleaseChannel.DEV: + return '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev'; + } + case BrowserPlatform.LINUX: + switch (channel) { + case ChromeReleaseChannel.STABLE: + return '/opt/google/chrome/chrome'; + case ChromeReleaseChannel.BETA: + return '/opt/google/chrome-beta/chrome'; + case ChromeReleaseChannel.DEV: + return '/opt/google/chrome-unstable/chrome'; + } + } + + throw new Error( + `Unable to detect browser executable path for '${channel}' on ${platform}.` + ); +} diff --git a/packages/browsers/src/browsers/types.ts b/packages/browsers/src/browsers/types.ts index fb7ad05d647..78844d2c208 100644 --- a/packages/browsers/src/browsers/types.ts +++ b/packages/browsers/src/browsers/types.ts @@ -52,3 +52,10 @@ export interface ProfileOptions { preferences: Record; path: string; } + +export enum ChromeReleaseChannel { + STABLE = 'stable', + DEV = 'dev', + CANARY = 'canary', + BETA = 'beta', +} diff --git a/packages/browsers/src/launcher.ts b/packages/browsers/src/launcher.ts index 120b7247e51..2f2bd31acf4 100644 --- a/packages/browsers/src/launcher.ts +++ b/packages/browsers/src/launcher.ts @@ -15,6 +15,7 @@ */ import childProcess from 'child_process'; +import {accessSync} from 'fs'; import os from 'os'; import path from 'path'; import readline from 'readline'; @@ -23,7 +24,9 @@ import { Browser, BrowserPlatform, executablePathByBrowser, + resolveSystemExecutablePath, } from './browsers/browsers.js'; +import {ChromeReleaseChannel} from './browsers/types.js'; import {CacheStructure} from './CacheStructure.js'; import {debug} from './debug.js'; import {detectBrowserPlatform} from './detectPlatform.js'; @@ -49,7 +52,7 @@ export interface Options { */ browser: Browser; /** - * Determines which buildId to dowloand. BuildId should uniquely identify + * Determines which buildId to download. BuildId should uniquely identify * binaries and they are used for caching. */ buildId: string; @@ -73,6 +76,47 @@ export function computeExecutablePath(options: Options): string { ); } +/** + * @public + */ +export interface SystemOptions { + /** + * Determines which platform the browser will be suited for. + * + * @defaultValue Auto-detected. + */ + platform?: BrowserPlatform; + /** + * Determines which browser to fetch. + */ + browser: Browser; + /** + * Release channel to look for on the system. + */ + channel: ChromeReleaseChannel; +} +export function computeSystemExecutablePath(options: SystemOptions): string { + options.platform ??= detectBrowserPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + const path = resolveSystemExecutablePath( + options.browser, + options.platform, + options.channel + ); + try { + accessSync(path); + } catch (error) { + throw new Error( + `Could not find Google Chrome executable for channel '${options.channel}' at '${path}'.` + ); + } + return path; +} + type LaunchOptions = { executablePath: string; pipe?: boolean; diff --git a/packages/browsers/src/main.ts b/packages/browsers/src/main.ts new file mode 100644 index 00000000000..4b27590e70b --- /dev/null +++ b/packages/browsers/src/main.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + launch, + computeExecutablePath, + computeSystemExecutablePath, + CDP_WEBSOCKET_ENDPOINT_REGEX, + WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, +} from './launcher.js'; +export {fetch, canFetch} from './fetch.js'; +export {detectBrowserPlatform} from './detectPlatform.js'; +export {Browser, BrowserPlatform} from './browsers/browsers.js'; diff --git a/packages/browsers/test/src/chrome-data.spec.ts b/packages/browsers/test/src/chrome-data.spec.ts index 2f201ddc3b7..60ed13d2deb 100644 --- a/packages/browsers/test/src/chrome-data.spec.ts +++ b/packages/browsers/test/src/chrome-data.spec.ts @@ -21,7 +21,9 @@ import {BrowserPlatform} from '../../lib/cjs/browsers/browsers.js'; import { resolveDownloadUrl, relativeExecutablePath, + resolveSystemExecutablePath, } from '../../lib/cjs/browsers/chrome.js'; +import {ChromeReleaseChannel} from '../../lib/cjs/browsers/types.js'; describe('Chrome', () => { it('should resolve download URLs', () => { @@ -69,4 +71,36 @@ describe('Chrome', () => { path.join('chrome-win', 'chrome.exe') ); }); + + it('should resolve system executable path', () => { + process.env['PROGRAMFILES'] = 'C:\\ProgramFiles'; + try { + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.WIN32, + ChromeReleaseChannel.DEV + ), + 'C:\\ProgramFiles\\Google\\Chrome Dev\\Application\\chrome.exe' + ); + } finally { + delete process.env['PROGRAMFILES']; + } + + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.MAC, + ChromeReleaseChannel.BETA + ), + '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta' + ); + assert.throws(() => { + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.LINUX, + ChromeReleaseChannel.CANARY + ), + path.join('chrome-linux', 'chrome') + ); + }, new Error(`Unable to detect browser executable path for 'canary' on linux.`)); + }); }); diff --git a/packages/browsers/test/src/fetch.spec.ts b/packages/browsers/test/src/fetch.spec.ts index 31450e96ae8..248620a18ea 100644 --- a/packages/browsers/test/src/fetch.spec.ts +++ b/packages/browsers/test/src/fetch.spec.ts @@ -92,7 +92,7 @@ describe('fetch', () => { }); it('should download a buildId that is a bzip2 archive', async function () { - this.timeout(60000); + this.timeout(90000); const expectedOutputPath = path.join( tmpDir, 'firefox',