Skip to content

Commit 0b92181

Browse files
authored
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
1 parent c51ea0a commit 0b92181

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

src/install/browserPaths.ts

Lines changed: 14 additions & 0 deletions
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, 'minibrowser-gtk'),
51+
path.join(browserPath, '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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import * as fs from 'fs';
17+
import * as util from 'util';
18+
import * as path from 'path';
19+
import * as os from 'os';
20+
import {spawn} from 'child_process';
21+
import {linuxLddDirectories, BrowserDescriptor} from '../install/browserPaths.js';
22+
23+
const accessAsync = util.promisify(fs.access.bind(fs));
24+
const checkExecutable = (filePath: string) => accessAsync(filePath, fs.constants.X_OK).then(() => true).catch(e => false);
25+
const statAsync = util.promisify(fs.stat.bind(fs));
26+
const readdirAsync = util.promisify(fs.readdir.bind(fs));
27+
28+
export async function validateDependencies(browserPath: string, browser: BrowserDescriptor) {
29+
// We currently only support Linux.
30+
if (os.platform() !== 'linux')
31+
return;
32+
const directoryPaths = linuxLddDirectories(browserPath, browser);
33+
const lddPaths: string[] = [];
34+
for (const directoryPath of directoryPaths)
35+
lddPaths.push(...(await executablesOrSharedLibraries(directoryPath)));
36+
const allMissingDeps = await Promise.all(lddPaths.map(lddPath => missingFileDependencies(lddPath)));
37+
const missingDeps = new Set();
38+
for (const deps of allMissingDeps) {
39+
for (const dep of deps)
40+
missingDeps.add(dep);
41+
}
42+
if (!missingDeps.size)
43+
return;
44+
const deps = [...missingDeps].sort().map(dep => ' ' + dep).join('\n');
45+
throw new Error('Host system is missing the following dependencies to run browser\n' + deps);
46+
}
47+
48+
async function executablesOrSharedLibraries(directoryPath: string): Promise<string[]> {
49+
const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file));
50+
const allStats = await Promise.all(allPaths.map(aPath => statAsync(aPath)));
51+
const filePaths = allPaths.filter((aPath, index) => allStats[index].isFile());
52+
53+
const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => {
54+
const basename = path.basename(filePath).toLowerCase();
55+
if (basename.endsWith('.so') || basename.includes('.so.'))
56+
return filePath;
57+
if (await checkExecutable(filePath))
58+
return filePath;
59+
return false;
60+
}))).filter(Boolean);
61+
62+
return executablersOrLibraries as string[];
63+
}
64+
65+
async function missingFileDependencies(filePath: string): Promise<Array<string>> {
66+
const {stdout} = await lddAsync(filePath);
67+
const missingDeps = stdout.split('\n').map(line => line.trim()).filter(line => line.endsWith('not found') && line.includes('=>')).map(line => line.split('=>')[0].trim());
68+
return missingDeps;
69+
}
70+
71+
function lddAsync(filePath: string): Promise<{stdout: string, stderr: string, code: number}> {
72+
const dirname = path.dirname(filePath);
73+
const ldd = spawn('ldd', [filePath], {
74+
cwd: dirname,
75+
env: {
76+
...process.env,
77+
LD_LIBRARY_PATH: dirname,
78+
},
79+
});
80+
81+
return new Promise(resolve => {
82+
let stdout = '';
83+
let stderr = '';
84+
ldd.stdout.on('data', data => stdout += data);
85+
ldd.stderr.on('data', data => stderr += data);
86+
ldd.on('close', code => resolve({stdout, stderr, code}));
87+
});
88+
}

0 commit comments

Comments
 (0)