mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat: validate browser dependencies before launching on Linux (#2960)
Missing dependencies is #1 problem with launching on Linux. This patch starts validating browser dependencies before launching browser on Linux. In case of a missing dependency, we will abandon launching with an error that lists all missing libs. References #2745
This commit is contained in:
parent
c51ea0afd1
commit
0b9218149f
@ -40,6 +40,20 @@ export const hostPlatform = ((): BrowserPlatform => {
|
||||
return platform as BrowserPlatform;
|
||||
})();
|
||||
|
||||
export function linuxLddDirectories(browserPath: string, browser: BrowserDescriptor): string[] {
|
||||
if (browser.name === 'chromium')
|
||||
return [path.join(browserPath, 'chrome-linux')];
|
||||
if (browser.name === 'firefox')
|
||||
return [path.join(browserPath, 'firefox')];
|
||||
if (browser.name === 'webkit') {
|
||||
return [
|
||||
path.join(browserPath, 'minibrowser-gtk'),
|
||||
path.join(browserPath, 'minibrowser-wpe'),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function executablePath(browserPath: string, browser: BrowserDescriptor): string | undefined {
|
||||
let tokens: string[] | undefined;
|
||||
if (browser.name === 'chromium') {
|
||||
|
@ -33,6 +33,7 @@ import * as types from '../types';
|
||||
import { TimeoutSettings } from '../timeoutSettings';
|
||||
import { WebSocketServer } from './webSocketServer';
|
||||
import { LoggerSink } from '../loggerSink';
|
||||
import { validateDependencies } from './validateDependencies';
|
||||
|
||||
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
|
||||
type LaunchOptions = types.LaunchOptions & { logger?: LoggerSink };
|
||||
@ -62,11 +63,13 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||
private _name: string;
|
||||
private _executablePath: string | undefined;
|
||||
private _webSocketNotPipe: WebSocketNotPipe | null;
|
||||
private _browserDescriptor: browserPaths.BrowserDescriptor;
|
||||
readonly _browserPath: string;
|
||||
|
||||
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, webSocketOrPipe: WebSocketNotPipe | null) {
|
||||
this._name = browser.name;
|
||||
const browsersPath = browserPaths.browsersPath(packagePath);
|
||||
this._browserDescriptor = browser;
|
||||
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
||||
this._executablePath = browserPaths.executablePath(this._browserPath, browser);
|
||||
this._webSocketNotPipe = webSocketOrPipe;
|
||||
@ -186,6 +189,11 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||
if (!executable)
|
||||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
||||
|
||||
if (!executablePath) {
|
||||
// We can only validate dependencies for bundled browsers.
|
||||
await validateDependencies(this._browserPath, this._browserDescriptor);
|
||||
}
|
||||
|
||||
// Note: it is important to define these variables before launchProcess, so that we don't get
|
||||
// "Cannot access 'browserServer' before initialization" if something went wrong.
|
||||
let transport: ConnectionTransport | undefined = undefined;
|
||||
|
88
src/server/validateDependencies.ts
Normal file
88
src/server/validateDependencies.ts
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import {spawn} from 'child_process';
|
||||
import {linuxLddDirectories, BrowserDescriptor} from '../install/browserPaths.js';
|
||||
|
||||
const accessAsync = util.promisify(fs.access.bind(fs));
|
||||
const checkExecutable = (filePath: string) => accessAsync(filePath, fs.constants.X_OK).then(() => true).catch(e => false);
|
||||
const statAsync = util.promisify(fs.stat.bind(fs));
|
||||
const readdirAsync = util.promisify(fs.readdir.bind(fs));
|
||||
|
||||
export async function validateDependencies(browserPath: string, browser: BrowserDescriptor) {
|
||||
// We currently only support Linux.
|
||||
if (os.platform() !== 'linux')
|
||||
return;
|
||||
const directoryPaths = linuxLddDirectories(browserPath, browser);
|
||||
const lddPaths: string[] = [];
|
||||
for (const directoryPath of directoryPaths)
|
||||
lddPaths.push(...(await executablesOrSharedLibraries(directoryPath)));
|
||||
const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependencies(lddPath)));
|
||||
const missingDeps = new Set();
|
||||
for (const deps of allMissingDeps) {
|
||||
for (const dep of deps)
|
||||
missingDeps.add(dep);
|
||||
}
|
||||
if (!missingDeps.size)
|
||||
return;
|
||||
const deps = [...missingDeps].sort().map(dep => ' ' + dep).join('\n');
|
||||
throw new Error('Host system is missing the following dependencies to run browser\n' + deps);
|
||||
}
|
||||
|
||||
async function executablesOrSharedLibraries(directoryPath: string): Promise<string[]> {
|
||||
const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file));
|
||||
const allStats = await Promise.all(allPaths.map(aPath => statAsync(aPath)));
|
||||
const filePaths = allPaths.filter((aPath, index) => allStats[index].isFile());
|
||||
|
||||
const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => {
|
||||
const basename = path.basename(filePath).toLowerCase();
|
||||
if (basename.endsWith('.so') || basename.includes('.so.'))
|
||||
return filePath;
|
||||
if (await checkExecutable(filePath))
|
||||
return filePath;
|
||||
return false;
|
||||
}))).filter(Boolean);
|
||||
|
||||
return executablersOrLibraries as string[];
|
||||
}
|
||||
|
||||
async function missingFileDependencies(filePath: string): Promise<Array<string>> {
|
||||
const {stdout} = await lddAsync(filePath);
|
||||
const missingDeps = stdout.split('\n').map(line => line.trim()).filter(line => line.endsWith('not found') && line.includes('=>')).map(line => line.split('=>')[0].trim());
|
||||
return missingDeps;
|
||||
}
|
||||
|
||||
function lddAsync(filePath: string): Promise<{stdout: string, stderr: string, code: number}> {
|
||||
const dirname = path.dirname(filePath);
|
||||
const ldd = spawn('ldd', [filePath], {
|
||||
cwd: dirname,
|
||||
env: {
|
||||
...process.env,
|
||||
LD_LIBRARY_PATH: dirname,
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
ldd.stdout.on('data', data => stdout += data);
|
||||
ldd.stderr.on('data', data => stderr += data);
|
||||
ldd.on('close', code => resolve({stdout, stderr, code}));
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user