Skip to content

Commit dabd4fb

Browse files
committed
feat: validate browser dependencies before launching on Linux
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
1 parent 0aff9be commit dabd4fb

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

src/install/browserPaths.ts

+14
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ export const hostPlatform = ((): BrowserPlatform => {
4040
return platform as BrowserPlatform;
4141
})();
4242

43+
export function linuxLddDirectories(browserPath: string, browser: BrowserDescriptor): string[] {
44+
if (browser.name === 'chromium')
45+
return [path.join(browserPath, 'chrome-linux')];
46+
if (browser.name === 'firefox')
47+
return [path.join(browserPath, 'firefox')];
48+
if (browser.name === 'webkit') {
49+
return [
50+
path.join(browserPath, 'linux', 'minibrowser-gtk'),
51+
path.join(browserPath, 'linux', 'minibrowser-wpe'),
52+
];
53+
}
54+
return [];
55+
}
56+
4357
export function executablePath(browserPath: string, browser: BrowserDescriptor): string | undefined {
4458
let tokens: string[] | undefined;
4559
if (browser.name === 'chromium') {

src/server/browserType.ts

+8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import * as types from '../types';
3333
import { TimeoutSettings } from '../timeoutSettings';
3434
import { WebSocketServer } from './webSocketServer';
3535
import { LoggerSink } from '../loggerSink';
36+
import { validateDependencies } from './validateDependencies';
3637

3738
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
3839
type LaunchOptions = types.LaunchOptions & { logger?: LoggerSink };
@@ -62,11 +63,13 @@ export abstract class BrowserTypeBase implements BrowserType {
6263
private _name: string;
6364
private _executablePath: string | undefined;
6465
private _webSocketNotPipe: WebSocketNotPipe | null;
66+
private _browserDescriptor: browserPaths.BrowserDescriptor;
6567
readonly _browserPath: string;
6668

6769
constructor(packagePath: string, browser: browserPaths.BrowserDescriptor, webSocketOrPipe: WebSocketNotPipe | null) {
6870
this._name = browser.name;
6971
const browsersPath = browserPaths.browsersPath(packagePath);
72+
this._browserDescriptor = browser;
7073
this._browserPath = browserPaths.browserDirectory(browsersPath, browser);
7174
this._executablePath = browserPaths.executablePath(this._browserPath, browser);
7275
this._webSocketNotPipe = webSocketOrPipe;
@@ -186,6 +189,11 @@ export abstract class BrowserTypeBase implements BrowserType {
186189
if (!executable)
187190
throw new Error(`No executable path is specified. Pass "executablePath" option directly.`);
188191

192+
if (!executablePath) {
193+
// We can only validate dependencies for bundled browsers.
194+
await validateDependencies(this._browserPath, this._browserDescriptor);
195+
}
196+
189197
// Note: it is important to define these variables before launchProcess, so that we don't get
190198
// "Cannot access 'browserServer' before initialization" if something went wrong.
191199
let transport: ConnectionTransport | undefined = undefined;

src/server/validateDependencies.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as fs from 'fs';
2+
import * as util from 'util';
3+
import * as path from 'path';
4+
import * as os from 'os';
5+
import {spawn} from 'child_process';
6+
import {linuxLddDirectories, BrowserDescriptor} from '../install/browserPaths.js';
7+
8+
const accessAsync = util.promisify(fs.access.bind(fs));
9+
const checkExecutable = (filePath: string) => accessAsync(filePath, fs.constants.X_OK).then(() => true).catch(e => false);
10+
const statAsync = util.promisify(fs.stat.bind(fs));
11+
const readdirAsync = util.promisify(fs.readdir.bind(fs));
12+
13+
export async function validateDependencies(browserPath: string, browser: BrowserDescriptor) {
14+
// We currently only support Linux.
15+
if (os.platform() !== 'linux')
16+
return;
17+
const directoryPaths = linuxLddDirectories(browserPath, browser);
18+
const lddPaths: string[] = [];
19+
for (const directoryPath of directoryPaths)
20+
lddPaths.push(...(await executablesOrSharedLibraries(directoryPath)));
21+
const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependencies(lddPath)));
22+
const missingDeps = new Set();
23+
for (const deps of allMissingDeps) {
24+
for (const dep of deps)
25+
missingDeps.add(dep);
26+
}
27+
if (!missingDeps.size)
28+
return;
29+
const deps = [...missingDeps].sort().map(dep => ' ' + dep).join('\n');
30+
throw new Error('Host system is missing dependencies to run browser:\n' + deps);
31+
}
32+
33+
async function executablesOrSharedLibraries(directoryPath: string): Promise<string[]> {
34+
const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file));
35+
const allStats = await Promise.all(allPaths.map(aPath => statAsync(aPath)));
36+
const filePaths = allPaths.filter((aPath, index) => allStats[index].isFile());
37+
38+
const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => {
39+
const basename = path.basename(filePath).toLowerCase();
40+
if (basename.endsWith('.so') || basename.includes('.so.'))
41+
return filePath;
42+
if (await checkExecutable(filePath))
43+
return filePath;
44+
return false;
45+
}))).filter(Boolean);
46+
47+
return executablersOrLibraries as string[];
48+
}
49+
50+
async function missingFileDependencies(filePath: string): Promise<Array<string>> {
51+
const {stdout} = await lddAsync(filePath);
52+
const missingDeps = stdout.split('\n').map(line => line.trim()).filter(line => line.endsWith('not found') && line.includes('=>')).map(line => line.split('=>')[0].trim());
53+
return missingDeps;
54+
}
55+
56+
function lddAsync(filePath: string): Promise<{stdout: string, stderr: string, code: number}> {
57+
const dirname = path.dirname(filePath);
58+
const ldd = spawn('ldd', [filePath], {
59+
cwd: dirname,
60+
env: {
61+
...process.env,
62+
LD_LIBRARY_PATH: dirname,
63+
},
64+
});
65+
66+
return new Promise((resolve) => {
67+
let stdout = '';
68+
let stderr = '';
69+
ldd.stdout.on('data', data => stdout += data);
70+
ldd.stderr.on('data', data => stderr += data);
71+
ldd.on('close', (code) => {
72+
resolve({stdout, stderr, code});
73+
});
74+
});
75+
}

0 commit comments

Comments
 (0)