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;
|
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 {
|
export function executablePath(browserPath: string, browser: BrowserDescriptor): string | undefined {
|
||||||
let tokens: string[] | undefined;
|
let tokens: string[] | undefined;
|
||||||
if (browser.name === 'chromium') {
|
if (browser.name === 'chromium') {
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import * as types from '../types';
|
|||||||
import { TimeoutSettings } from '../timeoutSettings';
|
import { TimeoutSettings } from '../timeoutSettings';
|
||||||
import { WebSocketServer } from './webSocketServer';
|
import { WebSocketServer } from './webSocketServer';
|
||||||
import { LoggerSink } from '../loggerSink';
|
import { LoggerSink } from '../loggerSink';
|
||||||
|
import { validateDependencies } from './validateDependencies';
|
||||||
|
|
||||||
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
|
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
|
||||||
type LaunchOptions = types.LaunchOptions & { logger?: LoggerSink };
|
type LaunchOptions = types.LaunchOptions & { logger?: LoggerSink };
|
||||||
@ -62,11 +63,13 @@ export abstract class BrowserTypeBase implements BrowserType {
|
|||||||
private _name: string;
|
private _name: string;
|
||||||
private _executablePath: string | undefined;
|
private _executablePath: string | undefined;
|
||||||
private _webSocketNotPipe: WebSocketNotPipe | null;
|
private _webSocketNotPipe: WebSocketNotPipe | null;
|
||||||
|
private _browserDescriptor: browserPaths.BrowserDescriptor;
|
||||||
readonly _browserPath: string;
|
readonly _browserPath: string;
|
||||||
|
|
||||||
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, webSocketOrPipe: WebSocketNotPipe | null) {
|
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, webSocketOrPipe: WebSocketNotPipe | null) {
|
||||||
this._name = browser.name;
|
this._name = browser.name;
|
||||||
const browsersPath = browserPaths.browsersPath(packagePath);
|
const browsersPath = browserPaths.browsersPath(packagePath);
|
||||||
|
this._browserDescriptor = browser;
|
||||||
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
|
||||||
this._executablePath = browserPaths.executablePath(this._browserPath, browser);
|
this._executablePath = browserPaths.executablePath(this._browserPath, browser);
|
||||||
this._webSocketNotPipe = webSocketOrPipe;
|
this._webSocketNotPipe = webSocketOrPipe;
|
||||||
@ -186,6 +189,11 @@ export abstract class BrowserTypeBase implements BrowserType {
|
|||||||
if (!executable)
|
if (!executable)
|
||||||
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
|
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
|
// 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.
|
// "Cannot access 'browserServer' before initialization" if something went wrong.
|
||||||
let transport: ConnectionTransport | undefined = undefined;
|
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