Skip to content

Commit 87674e7

Browse files
committed
Use a mutex to avoid race conditions corrupting browsers.json
The specific race here is only hit when no browers.json exists. In that case, JBL attempts to detect & write the browser config twice, in parallel. If that happens, and the config doesn't have exactly the same length in both cases, the write ends up with spurious data appended, and the config file no longer parses. We can recover from that, but it breaks in the meantime. With this change, it should (in theory) never happen.
1 parent 8d58432 commit 87674e7

File tree

3 files changed

+21
-5
lines changed

3 files changed

+21
-5
lines changed

package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@oclif/config": "^1.8.8",
3030
"@oclif/plugin-help": "^2.1.3",
3131
"@oclif/plugin-update": "^1.3.8",
32+
"async-mutex": "^0.1.3",
3233
"env-paths": "^1.0.0",
3334
"graphql": "^14.0.2",
3435
"graphql-yoga": "^1.16.7",

src/browsers.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3-
import * as getBrowserLauncher from '@james-proxy/james-browser-launcher';
4-
import { LaunchOptions, BrowserInstance } from '@james-proxy/james-browser-launcher';
53
import { promisify } from 'util';
64

5+
import { Mutex } from 'async-mutex';
6+
7+
import * as getBrowserLauncherCb from '@james-proxy/james-browser-launcher';
8+
import { LaunchOptions, BrowserInstance } from '@james-proxy/james-browser-launcher';
9+
710
const readFile = promisify(fs.readFile);
811
const deleteFile = promisify(fs.unlink);
12+
const getBrowserLauncher = promisify(getBrowserLauncherCb);
913

1014
const browserConfigPath = (configPath: string) => path.join(configPath, 'browsers.json');
1115

@@ -28,8 +32,14 @@ export async function checkBrowserConfig(configPath: string) {
2832
});
2933
}
3034

31-
async function getLauncher(configPath: string) {
32-
return await promisify(getBrowserLauncher)(browserConfigPath(configPath));
35+
// It's not safe to call getBrowserLauncher in parallel, as config files can
36+
// get corrupted. We use a mutex to serialize it.
37+
const getLauncherMutex = new Mutex();
38+
39+
function getLauncher(configPath: string) {
40+
return getLauncherMutex.runExclusive(() =>
41+
getBrowserLauncher(browserConfigPath(configPath))
42+
);
3343
}
3444

3545
export const getAvailableBrowsers = async (configPath: string) => {

0 commit comments

Comments
 (0)