-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(webkit): allow running WebKit via WSL on Windows #36358
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
$ErrorActionPreference = 'Stop' | ||
|
||
# WebKit WSL Installation Script | ||
# See webkit-wsl-transport-server.ts for the complete architecture diagram. | ||
# This script sets up a WSL distribution that will be used to run WebKit. | ||
|
||
$Distribution = "playwright" | ||
$Username = "pwuser" | ||
|
||
$distributions = (wsl --list --quiet) -split "\r?\n" | ||
if ($distributions -contains $Distribution) { | ||
Write-Host "WSL distribution '$Distribution' already exists. Skipping installation." | ||
} else { | ||
Write-Host "Installing new WSL distribution '$Distribution'..." | ||
$VhdSize = "10GB" | ||
wsl --install -d Ubuntu-24.04 --name $Distribution --no-launch --vhd-size $VhdSize | ||
wsl -d $Distribution -u root adduser --gecos GECOS --disabled-password $Username | ||
} | ||
|
||
$pwshDirname = (Resolve-Path -Path $PSScriptRoot).Path; | ||
$playwrightCoreRoot = Resolve-Path (Join-Path $pwshDirname "..") | ||
|
||
$initScript = @" | ||
if [ ! -f "/home/$Username/node/bin/node" ]; then | ||
mkdir -p /home/$Username/node | ||
curl -fsSL https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz -o /home/$Username/node/node-v22.17.0-linux-x64.tar.xz | ||
tar -xJf /home/$Username/node/node-v22.17.0-linux-x64.tar.xz -C /home/$Username/node --strip-components=1 | ||
fi | ||
/home/$Username/node/bin/node cli.js install-deps webkit | ||
cp lib/server/webkit/wsl/webkit-wsl-transport-client.js /home/$Username/ | ||
sudo -u $Username PLAYWRIGHT_SKIP_BROWSER_GC=1 /home/$Username/node/bin/node cli.js install webkit | ||
"@ -replace "\r\n", "`n" | ||
|
||
wsl -d $Distribution --cd $playwrightCoreRoot -u root -- bash -c "$initScript" | ||
Write-Host "Done!" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,8 @@ import { kBrowserCloseMessageId } from './wkConnection'; | |
import { wrapInASCIIBox } from '../utils/ascii'; | ||
import { BrowserType, kNoXServerRunningError } from '../browserType'; | ||
import { WKBrowser } from '../webkit/wkBrowser'; | ||
import { spawnAsync } from '../utils/spawnAsync'; | ||
import { registry } from '../registry'; | ||
|
||
import type { BrowserOptions } from '../browser'; | ||
import type { SdkObject } from '../instrumentation'; | ||
|
@@ -37,10 +39,11 @@ export class WebKit extends BrowserType { | |
return WKBrowser.connect(this.attribution.playwright, transport, options); | ||
} | ||
|
||
override amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean): NodeJS.ProcessEnv { | ||
override amendEnvironment(env: NodeJS.ProcessEnv, userDataDir: string, isPersistent: boolean, options: types.LaunchOptions): NodeJS.ProcessEnv { | ||
return { | ||
...env, | ||
CURL_COOKIE_JAR_PATH: process.platform === 'win32' && isPersistent ? path.join(userDataDir, 'cookiejar.db') : undefined, | ||
WEBKIT_EXECUTABLE: options.channel === 'webkit-wsl' ? registry.findExecutable('webkit-wsl')!.wslExecutablePath! : undefined | ||
}; | ||
} | ||
|
||
|
@@ -57,20 +60,29 @@ export class WebKit extends BrowserType { | |
transport.send({ method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId }); | ||
} | ||
|
||
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] { | ||
override async defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): Promise<string[]> { | ||
const { args = [], headless } = options; | ||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); | ||
if (userDataDirArg) | ||
throw this._createUserDataDirArgMisuseError('--user-data-dir'); | ||
if (args.find(arg => !arg.startsWith('-'))) | ||
throw new Error('Arguments can not specify page to be opened'); | ||
const webkitArguments = ['--inspector-pipe']; | ||
if (process.platform === 'win32') | ||
|
||
if (options.channel === 'webkit-wsl') { | ||
if (options.executablePath) | ||
throw new Error('Cannot specify executablePath when using the "webkit-wsl" channel.'); | ||
webkitArguments.unshift( | ||
path.join(__dirname, 'wsl/webkit-wsl-transport-server.js'), | ||
); | ||
} | ||
|
||
if (process.platform === 'win32' && options.channel !== 'webkit-wsl') | ||
webkitArguments.push('--disable-accelerated-compositing'); | ||
if (headless) | ||
webkitArguments.push('--headless'); | ||
if (isPersistent) | ||
webkitArguments.push(`--user-data-dir=${userDataDir}`); | ||
webkitArguments.push(`--user-data-dir=${options.channel === 'webkit-wsl' ? await translatePathToWSL(userDataDir) : userDataDir}`); | ||
else | ||
webkitArguments.push(`--no-startup-window`); | ||
const proxy = options.proxyOverride || options.proxy; | ||
|
@@ -79,7 +91,7 @@ export class WebKit extends BrowserType { | |
webkitArguments.push(`--proxy=${proxy.server}`); | ||
if (proxy.bypass) | ||
webkitArguments.push(`--proxy-bypass-list=${proxy.bypass}`); | ||
} else if (process.platform === 'linux') { | ||
} else if (process.platform === 'linux' || (process.platform === 'win32' && options.channel === 'webkit-wsl')) { | ||
webkitArguments.push(`--proxy=${proxy.server}`); | ||
if (proxy.bypass) | ||
webkitArguments.push(...proxy.bypass.split(',').map(t => `--ignore-host=${t}`)); | ||
|
@@ -97,3 +109,8 @@ export class WebKit extends BrowserType { | |
return webkitArguments; | ||
} | ||
} | ||
|
||
export async function translatePathToWSL(path: string): Promise<string> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed before, perhaps do this once, save somewhere on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The input |
||
const { stdout } = await spawnAsync('wsl.exe', ['-d', 'playwright', '--cd', '/home/pwuser', 'wslpath', path.replace(/\\/g, '\\\\')]); | ||
return stdout.toString().trim(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/** | ||
* 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. | ||
*/ | ||
// @ts-check | ||
/* eslint-disable no-restricted-properties */ | ||
/* eslint-disable no-console */ | ||
|
||
// WebKit WSL Transport Client - runs inside WSL/Linux | ||
// See webkit-wsl-transport-server.ts for the complete architecture diagram. | ||
// This client connects to the TCP server and bridges it to WebKit via fd3/fd4 pipes. | ||
|
||
import net from 'net'; | ||
import fs from 'fs'; | ||
import { spawn, spawnSync } from 'child_process'; | ||
|
||
(async () => { | ||
const { PW_WSL_BRIDGE_PORT: socketPort, ...childEnv } = process.env; | ||
if (!socketPort) | ||
throw new Error('PW_WSL_BRIDGE_PORT env var is not set'); | ||
|
||
const [executable, ...args] = process.argv.slice(2); | ||
|
||
if (!(await fs.promises.stat(executable)).isFile()) | ||
throw new Error(`Executable does not exist. Did you update Playwright recently? Make sure to run npx playwright install webkit-wsl`); | ||
|
||
const address = (() => { | ||
const res = spawnSync('/usr/bin/wslinfo', ['--networking-mode'], { encoding: 'utf8' }); | ||
if (res.error || res.status !== 0) | ||
throw new Error(`Failed to run /usr/bin/wslinfo --networking-mode: ${res.error?.message || res.stderr || res.status}`); | ||
if (res.stdout.trim() === 'nat') { | ||
mxschmitt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const ipRes = spawnSync('/usr/sbin/ip', ['route', 'show'], { encoding: 'utf8' }); | ||
if (ipRes.error || ipRes.status !== 0) | ||
throw new Error(`Failed to run ip route show: ${ipRes.error?.message || ipRes.stderr || ipRes.status}`); | ||
const ip = ipRes.stdout.trim().split('\n').find(line => line.includes('default'))?.split(' ')[2]; | ||
if (!ip) | ||
throw new Error('Could not determine WSL IP address (NAT mode).'); | ||
return ip; | ||
} | ||
return '127.0.0.1'; | ||
})(); | ||
|
||
const socket = net.createConnection(parseInt(socketPort, 10), address); | ||
// Disable Nagle's algorithm to reduce latency for small, frequent messages. | ||
socket.setNoDelay(true); | ||
|
||
await new Promise((resolve, reject) => { | ||
socket.on('connect', resolve); | ||
socket.on('error', reject); | ||
}); | ||
|
||
const child = spawn(executable, args, { | ||
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe'], | ||
env: childEnv, | ||
}); | ||
|
||
const [childOutput, childInput] = [child.stdio[3] as NodeJS.WritableStream, child.stdio[4] as NodeJS.ReadableStream]; | ||
socket.pipe(childOutput); | ||
childInput.pipe(socket); | ||
|
||
socket.on('end', () => child.kill()); | ||
|
||
child.on('exit', exitCode => { | ||
socket.end(); | ||
process.exit(exitCode || 0); | ||
}); | ||
|
||
await new Promise((resolve, reject) => { | ||
child.on('exit', resolve); | ||
child.on('error', reject); | ||
}); | ||
})().catch(error => { | ||
console.error('Error occurred:', error); | ||
process.exit(1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we try to preserve the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The child's exit code is already preserved on lines 67-70. The |
||
}); |
Uh oh!
There was an error while loading. Please reload this page.