Skip to content
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

feat: validate browser dependencies before launching on Linux #2960

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/install/browserPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
8 changes: 8 additions & 0 deletions src/server/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
88 changes: 88 additions & 0 deletions src/server/validateDependencies.ts
Original file line number Diff line number Diff line change
@@ -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}));
});
}