Skip to content

Commit

Permalink
feat: validate browser dependencies before launching on Linux
Browse files Browse the repository at this point in the history
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 microsoft#2745
  • Loading branch information
aslushnikov committed Jul 15, 2020
1 parent 0aff9be commit dabd4fb
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 0 deletions.
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, 'linux', 'minibrowser-gtk'),
path.join(browserPath, 'linux', '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
75 changes: 75 additions & 0 deletions src/server/validateDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 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});
});
});
}

0 comments on commit dabd4fb

Please sign in to comment.