From 8697929e39861c5912ccc2ec07a776bd605cd4fe Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 7 Jan 2020 16:15:07 -0800 Subject: [PATCH] chore: move webkit server code to src/server (#415) --- index.d.ts | 1 + index.js | 2 + src/api.ts | 1 + src/server/ffPlaywright.ts | 4 +- src/server/wkPlaywright.ts | 231 +++++++++++++++++++++++++++++++++++++ src/webkit/wkApi.ts | 1 - src/webkit/wkBrowser.ts | 20 +++- src/webkit/wkLauncher.ts | 182 ----------------------------- src/webkit/wkPlaywright.ts | 71 ------------ test/fixtures/dumpio.js | 2 +- 10 files changed, 257 insertions(+), 258 deletions(-) create mode 100644 src/server/wkPlaywright.ts delete mode 100644 src/webkit/wkLauncher.ts delete mode 100644 src/webkit/wkPlaywright.ts diff --git a/index.d.ts b/index.d.ts index 858413f594..877cc45e28 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,3 +20,4 @@ export function playwright(browser: 'firefox'): import('./lib/api').FirefoxPlayw export function playwright(browser: 'webkit'): import('./lib/api').WebKitPlaywright; export function connect(browser: 'chromium'): import('./lib/api').ChromiumBrowser.connect; export function connect(browser: 'firefox'): import('./lib/api').FirefoxBrowser.connect; +export function connect(browser: 'webkit'): import('./lib/api').WebKitBrowser.connect; diff --git a/index.js b/index.js index 8d7e577620..0b6e5b3ae9 100644 --- a/index.js +++ b/index.js @@ -39,5 +39,7 @@ module.exports.connect = browser => { return api.ChromiumBrowser.connect; if (browser === 'firefox') return api.FirefoxBrowser.connect; + if (browser === 'webkit') + return api.WebKitBrowser.connect; throw new Error(`Unsupported browser "${browser}"`); }; diff --git a/src/api.ts b/src/api.ts index 97080c7817..ca959795d0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -30,6 +30,7 @@ export { Coverage, FileChooser, Page, Worker } from './page'; export { BrowserFetcher } from './server/browserFetcher'; export { CRPlaywright as ChromiumPlaywright, CRBrowserServer as ChromiumBrowserServer } from './server/crPlaywright'; export { FFPlaywright as FirefoxPlaywright, FFBrowserServer as FirefoxBrowserServer } from './server/ffPlaywright'; +export { WKPlaywright as WebKitPlaywright, WKBrowserServer as WebKitBrowserServer } from './server/wkPlaywright'; export * from './chromium/crApi'; export * from './firefox/ffApi'; diff --git a/src/server/ffPlaywright.ts b/src/server/ffPlaywright.ts index 8a7da196a0..613c10f38d 100644 --- a/src/server/ffPlaywright.ts +++ b/src/server/ffPlaywright.ts @@ -16,9 +16,9 @@ */ import { FFBrowser, FFConnectOptions, createTransport } from '../firefox/ffBrowser'; -import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../server/browserFetcher'; +import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from './browserFetcher'; import { DeviceDescriptors } from '../deviceDescriptors'; -import { launchProcess, waitForLine } from '../server/processLauncher'; +import { launchProcess, waitForLine } from './processLauncher'; import * as Errors from '../errors'; import * as types from '../types'; import * as platform from '../platform'; diff --git a/src/server/wkPlaywright.ts b/src/server/wkPlaywright.ts new file mode 100644 index 0000000000..eee4fbb7f6 --- /dev/null +++ b/src/server/wkPlaywright.ts @@ -0,0 +1,231 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * 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. + */ + +import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from './browserFetcher'; +import { DeviceDescriptors } from '../deviceDescriptors'; +import * as Errors from '../errors'; +import * as types from '../types'; +import { WKBrowser } from '../webkit/wkBrowser'; +import { WKConnectOptions, createTransport } from '../webkit/wkBrowser'; +import { WKConnection } from '../webkit/wkConnection'; +import { execSync, ChildProcess } from 'child_process'; +import { PipeTransport } from './pipeTransport'; +import { launchProcess } from './processLauncher'; +import * as path from 'path'; +import * as util from 'util'; +import * as os from 'os'; +import { assert } from '../helper'; + +export type LaunchOptions = { + ignoreDefaultArgs?: boolean, + args?: string[], + executablePath?: string, + handleSIGINT?: boolean, + handleSIGTERM?: boolean, + handleSIGHUP?: boolean, + headless?: boolean, + timeout?: number, + dumpio?: boolean, + env?: {[key: string]: string} | undefined, + slowMo?: number, +}; + +export class WKBrowserServer { + private _process: ChildProcess; + private _connectOptions: WKConnectOptions; + + constructor(process: ChildProcess, connectOptions: WKConnectOptions) { + this._process = process; + this._connectOptions = connectOptions; + } + + async connect(): Promise { + return WKBrowser.connect(this._connectOptions); + } + + process(): ChildProcess { + return this._process; + } + + connectOptions(): WKConnectOptions { + return this._connectOptions; + } + + async close(): Promise { + const transport = await createTransport(this._connectOptions); + const connection = new WKConnection(transport); + await connection.send('Browser.close'); + connection.dispose(); + } +} + +export class WKPlaywright { + private _projectRoot: string; + readonly _revision: string; + + constructor(projectRoot: string, preferredRevision: string) { + this._projectRoot = projectRoot; + this._revision = preferredRevision; + } + + async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise { + const fetcher = this.createBrowserFetcher(options); + const revisionInfo = fetcher.revisionInfo(this._revision); + await fetcher.download(this._revision, options ? options.onProgress : undefined); + return revisionInfo; + } + + async launch(options?: LaunchOptions): Promise { + const server = await this.launchServer(options); + return server.connect(); + } + + async launchServer(options: LaunchOptions = {}): Promise { + const { + ignoreDefaultArgs = false, + args = [], + dumpio = false, + executablePath = null, + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + slowMo = 0, + } = options; + + const webkitArguments = []; + if (!ignoreDefaultArgs) + webkitArguments.push(...this.defaultArgs(options)); + else + webkitArguments.push(...args); + + let webkitExecutable = executablePath; + if (!executablePath) { + const {missingText, executablePath} = this._resolveExecutablePath(); + if (missingText) + throw new Error(missingText); + webkitExecutable = executablePath; + } + webkitArguments.push('--inspector-pipe'); + // Headless options is only implemented on Mac at the moment. + if (process.platform === 'darwin' && options.headless !== false) + webkitArguments.push('--headless'); + + const launchedProcess = await launchProcess({ + executablePath: webkitExecutable, + args: webkitArguments, + env, + handleSIGINT, + handleSIGTERM, + handleSIGHUP, + dumpio, + pipe: true, + tempDir: null + }, () => { + if (!server) + return Promise.reject(); + server.close(); + }); + + let server: WKBrowserServer | undefined; + try { + const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream); + server = new WKBrowserServer(launchedProcess, { transport, slowMo }); + return server; + } catch (e) { + if (server) + await server.close(); + throw e; + } + } + + executablePath(): string { + return this._resolveExecutablePath().executablePath; + } + + get devices(): types.Devices { + return DeviceDescriptors; + } + + get errors(): any { + return Errors; + } + + defaultArgs(options: any = {}): string[] { + const { + args = [], + } = options; + const webkitArguments = [...DEFAULT_ARGS]; + webkitArguments.push(...args); + return webkitArguments; + } + + createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher { + const downloadURLs = { + linux: '%s/builds/webkit/%s/minibrowser-linux.zip', + mac: '%s/builds/webkit/%s/minibrowser-mac-%s.zip', + }; + + const defaultOptions = { + path: path.join(this._projectRoot, '.local-webkit'), + host: 'https://playwrightaccount.blob.core.windows.net', + platform: (() => { + const platform = os.platform(); + if (platform === 'darwin') + return 'mac'; + if (platform === 'linux') + return 'linux'; + if (platform === 'win32') + return 'linux'; // Windows gets linux binaries and uses WSL + return platform; + })() + }; + options = { + ...defaultOptions, + ...options, + }; + assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform); + + return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { + return { + downloadUrl: (platform === 'mac') ? + util.format(downloadURLs[platform], options.host, revision, getMacVersion()) : + util.format(downloadURLs[platform], options.host, revision), + executablePath: 'pw_run.sh', + }; + }); + } + + _resolveExecutablePath(): { executablePath: string; missingText: string | null; } { + const browserFetcher = this.createBrowserFetcher(); + const revisionInfo = browserFetcher.revisionInfo(this._revision); + const missingText = !revisionInfo.local ? `WebKit revision is not downloaded. Run "npm install" or "yarn install"` : null; + return { executablePath: revisionInfo.executablePath, missingText }; + } +} + +const DEFAULT_ARGS = []; + +let cachedMacVersion = undefined; +function getMacVersion() { + if (!cachedMacVersion) { + const [major, minor] = execSync('sw_vers -productVersion').toString('utf8').trim().split('.'); + cachedMacVersion = major + '.' + minor; + } + return cachedMacVersion; +} + diff --git a/src/webkit/wkApi.ts b/src/webkit/wkApi.ts index 0b0825c530..9eab7adf3f 100644 --- a/src/webkit/wkApi.ts +++ b/src/webkit/wkApi.ts @@ -15,4 +15,3 @@ */ export { WKBrowser as WebKitBrowser } from './wkBrowser'; -export { WKPlaywright as WebKitPlaywright } from './wkPlaywright'; diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 9afd169199..3e2d6a0949 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -20,12 +20,17 @@ import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { assert, helper, RegisteredListener } from '../helper'; import * as network from '../network'; import { Page } from '../page'; -import { ConnectionTransport } from '../transport'; +import { ConnectionTransport, SlowMoTransport } from '../transport'; import * as types from '../types'; import { Protocol } from './protocol'; import { WKConnection, WKConnectionEvents, WKPageProxySession } from './wkConnection'; import { WKPageProxy } from './wkPageProxy'; +export type WKConnectOptions = { + slowMo?: number, + transport: ConnectionTransport; +}; + export class WKBrowser extends browser.Browser { readonly _connection: WKConnection; private readonly _defaultContext: BrowserContext; @@ -36,6 +41,14 @@ export class WKBrowser extends browser.Browser { private _firstPageProxyCallback?: () => void; private readonly _firstPageProxyPromise: Promise; + static async connect(options: WKConnectOptions): Promise { + const transport = await createTransport(options); + const browser = new WKBrowser(transport); + // TODO: figure out the timeout. + await browser._waitForFirstPageTarget(30000); + return browser; + } + constructor(transport: ConnectionTransport) { super(); this._connection = new WKConnection(transport); @@ -179,3 +192,8 @@ export class WKBrowser extends browser.Browser { return context; } } + +export async function createTransport(options: WKConnectOptions): Promise { + assert(!!options.transport, 'Transport must be passed to connect'); + return SlowMoTransport.wrap(options.transport, options.slowMo); +} diff --git a/src/webkit/wkLauncher.ts b/src/webkit/wkLauncher.ts deleted file mode 100644 index e3acb7c0fa..0000000000 --- a/src/webkit/wkLauncher.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * 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. - */ - -import { assert } from '../helper'; -import { WKBrowser } from './wkBrowser'; -import { BrowserFetcher, BrowserFetcherOptions } from '../server/browserFetcher'; -import { SlowMoTransport } from '../transport'; -import { execSync } from 'child_process'; -import * as path from 'path'; -import * as util from 'util'; -import * as os from 'os'; -import { launchProcess } from '../server/processLauncher'; -import { BrowserServer } from '../browser'; -import { PipeTransport } from '../server/pipeTransport'; - -const DEFAULT_ARGS = [ -]; - -export class WKLauncher { - private _projectRoot: string; - private _preferredRevision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._preferredRevision = preferredRevision; - } - - defaultArgs(options: any = {}) { - const { - args = [], - } = options; - const webkitArguments = [...DEFAULT_ARGS]; - webkitArguments.push(...args); - return webkitArguments; - } - - async launch(options: LauncherLaunchOptions = {}): Promise> { - const { - ignoreDefaultArgs = false, - args = [], - dumpio = false, - executablePath = null, - env = process.env, - handleSIGINT = true, - handleSIGTERM = true, - handleSIGHUP = true, - slowMo = 0, - timeout = 30000 - } = options; - - const webkitArguments = []; - if (!ignoreDefaultArgs) - webkitArguments.push(...this.defaultArgs(options)); - else - webkitArguments.push(...args); - - let webkitExecutable = executablePath; - if (!executablePath) { - const {missingText, executablePath} = this._resolveExecutablePath(); - if (missingText) - throw new Error(missingText); - webkitExecutable = executablePath; - } - webkitArguments.push('--inspector-pipe'); - // Headless options is only implemented on Mac at the moment. - if (process.platform === 'darwin' && options.headless !== false) - webkitArguments.push('--headless'); - - const launchedProcess = await launchProcess({ - executablePath: webkitExecutable, - args: webkitArguments, - env, - handleSIGINT, - handleSIGTERM, - handleSIGHUP, - dumpio, - pipe: true, - tempDir: null - }, () => { - if (!browser) - return Promise.reject(); - browser.close(); - }); - - let browser: WKBrowser | undefined; - try { - const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream); - browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo)); - await browser._waitForFirstPageTarget(timeout); - return new BrowserServer(browser, launchedProcess, ''); - } catch (e) { - if (browser) - await browser.close(); - throw e; - } - } - - executablePath(): string { - return this._resolveExecutablePath().executablePath; - } - - _resolveExecutablePath(): { executablePath: string; missingText: string | null; } { - const browserFetcher = createBrowserFetcher(this._projectRoot); - const revisionInfo = browserFetcher.revisionInfo(this._preferredRevision); - const missingText = !revisionInfo.local ? `WebKit revision is not downloaded. Run "npm install" or "yarn install"` : null; - return {executablePath: revisionInfo.executablePath, missingText}; - } - -} - -export type LauncherLaunchOptions = { - ignoreDefaultArgs?: boolean, - args?: string[], - executablePath?: string, - handleSIGINT?: boolean, - handleSIGTERM?: boolean, - handleSIGHUP?: boolean, - headless?: boolean, - timeout?: number, - dumpio?: boolean, - env?: {[key: string]: string} | undefined, - slowMo?: number, -}; - -let cachedMacVersion = undefined; -function getMacVersion() { - if (!cachedMacVersion) { - const [major, minor] = execSync('sw_vers -productVersion').toString('utf8').trim().split('.'); - cachedMacVersion = major + '.' + minor; - } - return cachedMacVersion; -} - -export function createBrowserFetcher(projectRoot: string, options: BrowserFetcherOptions = {}): BrowserFetcher { - const downloadURLs = { - linux: '%s/builds/webkit/%s/minibrowser-linux.zip', - mac: '%s/builds/webkit/%s/minibrowser-mac-%s.zip', - }; - - const defaultOptions = { - path: path.join(projectRoot, '.local-webkit'), - host: 'https://playwrightaccount.blob.core.windows.net', - platform: (() => { - const platform = os.platform(); - if (platform === 'darwin') - return 'mac'; - if (platform === 'linux') - return 'linux'; - if (platform === 'win32') - return 'linux'; // Windows gets linux binaries and uses WSL - return platform; - })() - }; - options = { - ...defaultOptions, - ...options, - }; - assert(!!downloadURLs[options.platform], 'Unsupported platform: ' + options.platform); - - return new BrowserFetcher(options.path, options.platform, (platform: string, revision: string) => { - return { - downloadUrl: (platform === 'mac') ? - util.format(downloadURLs[platform], options.host, revision, getMacVersion()) : - util.format(downloadURLs[platform], options.host, revision), - executablePath: 'pw_run.sh', - }; - }); -} diff --git a/src/webkit/wkPlaywright.ts b/src/webkit/wkPlaywright.ts deleted file mode 100644 index 0f673ed557..0000000000 --- a/src/webkit/wkPlaywright.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * 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. - */ -import * as browsers from '../browser'; -import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetcherRevisionInfo } from '../server/browserFetcher'; -import { DeviceDescriptors } from '../deviceDescriptors'; -import * as Errors from '../errors'; -import * as types from '../types'; -import { WKLauncher, LauncherLaunchOptions, createBrowserFetcher } from './wkLauncher'; -import { WKBrowser } from './wkBrowser'; - -export class WKPlaywright { - private _projectRoot: string; - private _launcher: WKLauncher; - readonly _revision: string; - - constructor(projectRoot: string, preferredRevision: string) { - this._projectRoot = projectRoot; - this._launcher = new WKLauncher(projectRoot, preferredRevision); - this._revision = preferredRevision; - } - - async downloadBrowser(options?: BrowserFetcherOptions & { onProgress?: OnProgressCallback }): Promise { - const fetcher = this.createBrowserFetcher(options); - const revisionInfo = fetcher.revisionInfo(this._revision); - await fetcher.download(this._revision, options ? options.onProgress : undefined); - return revisionInfo; - } - - async launch(options: (LauncherLaunchOptions) | undefined): Promise { - const server = await this._launcher.launch(options); - return server.connect(); - } - - async launchServer(options: (LauncherLaunchOptions) | undefined): Promise> { - return this._launcher.launch(options); - } - - executablePath(): string { - return this._launcher.executablePath(); - } - - get devices(): types.Devices { - return DeviceDescriptors; - } - - get errors(): any { - return Errors; - } - - defaultArgs(options: any | undefined): string[] { - return this._launcher.defaultArgs(options); - } - - createBrowserFetcher(options?: BrowserFetcherOptions): BrowserFetcher { - return createBrowserFetcher(this._projectRoot, options); - } -} diff --git a/test/fixtures/dumpio.js b/test/fixtures/dumpio.js index ea5302b557..1ae8547736 100644 --- a/test/fixtures/dumpio.js +++ b/test/fixtures/dumpio.js @@ -17,7 +17,7 @@ if (playwrightRoot.includes('firefox')) options.args.push('-juggler', '-profile'); try { - await require(playwrightRoot).launch(options); + await require(playwrightRoot).launchServer(options); console.error('Browser launch unexpectedly succeeded.'); } catch (e) { }