time-to-botec

Benchmark sampling in different programming languages
Log | Files | Refs | README

index.js (8897B)


      1 import process from 'node:process';
      2 import {Buffer} from 'node:buffer';
      3 import path from 'node:path';
      4 import {fileURLToPath} from 'node:url';
      5 import childProcess from 'node:child_process';
      6 import fs from 'node:fs/promises';
      7 import {constants as fsConstants} from 'node:fs'; // TODO: Move this to the above import when targeting Node.js 18.
      8 import isWsl from 'is-wsl';
      9 import defineLazyProperty from 'define-lazy-prop';
     10 import defaultBrowser from 'default-browser';
     11 import isInsideContainer from 'is-inside-container';
     12 
     13 // Path to included `xdg-open`.
     14 const __dirname = path.dirname(fileURLToPath(import.meta.url));
     15 const localXdgOpenPath = path.join(__dirname, 'xdg-open');
     16 
     17 const {platform, arch} = process;
     18 
     19 /**
     20 Get the mount point for fixed drives in WSL.
     21 
     22 @inner
     23 @returns {string} The mount point.
     24 */
     25 const getWslDrivesMountPoint = (() => {
     26 	// Default value for "root" param
     27 	// according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config
     28 	const defaultMountPoint = '/mnt/';
     29 
     30 	let mountPoint;
     31 
     32 	return async function () {
     33 		if (mountPoint) {
     34 			// Return memoized mount point value
     35 			return mountPoint;
     36 		}
     37 
     38 		const configFilePath = '/etc/wsl.conf';
     39 
     40 		let isConfigFileExists = false;
     41 		try {
     42 			await fs.access(configFilePath, fsConstants.F_OK);
     43 			isConfigFileExists = true;
     44 		} catch {}
     45 
     46 		if (!isConfigFileExists) {
     47 			return defaultMountPoint;
     48 		}
     49 
     50 		const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'});
     51 		const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
     52 
     53 		if (!configMountPoint) {
     54 			return defaultMountPoint;
     55 		}
     56 
     57 		mountPoint = configMountPoint.groups.mountPoint.trim();
     58 		mountPoint = mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`;
     59 
     60 		return mountPoint;
     61 	};
     62 })();
     63 
     64 const pTryEach = async (array, mapper) => {
     65 	let latestError;
     66 
     67 	for (const item of array) {
     68 		try {
     69 			return await mapper(item); // eslint-disable-line no-await-in-loop
     70 		} catch (error) {
     71 			latestError = error;
     72 		}
     73 	}
     74 
     75 	throw latestError;
     76 };
     77 
     78 const baseOpen = async options => {
     79 	options = {
     80 		wait: false,
     81 		background: false,
     82 		newInstance: false,
     83 		allowNonzeroExitCode: false,
     84 		...options,
     85 	};
     86 
     87 	if (Array.isArray(options.app)) {
     88 		return pTryEach(options.app, singleApp => baseOpen({
     89 			...options,
     90 			app: singleApp,
     91 		}));
     92 	}
     93 
     94 	let {name: app, arguments: appArguments = []} = options.app ?? {};
     95 	appArguments = [...appArguments];
     96 
     97 	if (Array.isArray(app)) {
     98 		return pTryEach(app, appName => baseOpen({
     99 			...options,
    100 			app: {
    101 				name: appName,
    102 				arguments: appArguments,
    103 			},
    104 		}));
    105 	}
    106 
    107 	if (app === 'browser' || app === 'browserPrivate') {
    108 		// IDs from default-browser for macOS and windows are the same
    109 		const ids = {
    110 			'com.google.chrome': 'chrome',
    111 			'google-chrome.desktop': 'chrome',
    112 			'org.mozilla.firefox': 'firefox',
    113 			'firefox.desktop': 'firefox',
    114 			'com.microsoft.msedge': 'edge',
    115 			'com.microsoft.edge': 'edge',
    116 			'microsoft-edge.desktop': 'edge',
    117 		};
    118 
    119 		// Incognito flags for each browser in `apps`.
    120 		const flags = {
    121 			chrome: '--incognito',
    122 			firefox: '--private-window',
    123 			edge: '--inPrivate',
    124 		};
    125 
    126 		const browser = await defaultBrowser();
    127 		if (browser.id in ids) {
    128 			const browserName = ids[browser.id];
    129 
    130 			if (app === 'browserPrivate') {
    131 				appArguments.push(flags[browserName]);
    132 			}
    133 
    134 			return baseOpen({
    135 				...options,
    136 				app: {
    137 					name: apps[browserName],
    138 					arguments: appArguments,
    139 				},
    140 			});
    141 		}
    142 
    143 		throw new Error(`${browser.name} is not supported as a default browser`);
    144 	}
    145 
    146 	let command;
    147 	const cliArguments = [];
    148 	const childProcessOptions = {};
    149 
    150 	if (platform === 'darwin') {
    151 		command = 'open';
    152 
    153 		if (options.wait) {
    154 			cliArguments.push('--wait-apps');
    155 		}
    156 
    157 		if (options.background) {
    158 			cliArguments.push('--background');
    159 		}
    160 
    161 		if (options.newInstance) {
    162 			cliArguments.push('--new');
    163 		}
    164 
    165 		if (app) {
    166 			cliArguments.push('-a', app);
    167 		}
    168 	} else if (platform === 'win32' || (isWsl && !isInsideContainer() && !app)) {
    169 		const mountPoint = await getWslDrivesMountPoint();
    170 
    171 		command = isWsl
    172 			? `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`
    173 			: `${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`;
    174 
    175 		cliArguments.push(
    176 			'-NoProfile',
    177 			'-NonInteractive',
    178 			'-ExecutionPolicy',
    179 			'Bypass',
    180 			'-EncodedCommand',
    181 		);
    182 
    183 		if (!isWsl) {
    184 			childProcessOptions.windowsVerbatimArguments = true;
    185 		}
    186 
    187 		const encodedArguments = ['Start'];
    188 
    189 		if (options.wait) {
    190 			encodedArguments.push('-Wait');
    191 		}
    192 
    193 		if (app) {
    194 			// Double quote with double quotes to ensure the inner quotes are passed through.
    195 			// Inner quotes are delimited for PowerShell interpretation with backticks.
    196 			encodedArguments.push(`"\`"${app}\`""`);
    197 			if (options.target) {
    198 				appArguments.push(options.target);
    199 			}
    200 		} else if (options.target) {
    201 			encodedArguments.push(`"${options.target}"`);
    202 		}
    203 
    204 		if (appArguments.length > 0) {
    205 			appArguments = appArguments.map(arg => `"\`"${arg}\`""`);
    206 			encodedArguments.push('-ArgumentList', appArguments.join(','));
    207 		}
    208 
    209 		// Using Base64-encoded command, accepted by PowerShell, to allow special characters.
    210 		options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
    211 	} else {
    212 		if (app) {
    213 			command = app;
    214 		} else {
    215 			// When bundled by Webpack, there's no actual package file path and no local `xdg-open`.
    216 			const isBundled = !__dirname || __dirname === '/';
    217 
    218 			// Check if local `xdg-open` exists and is executable.
    219 			let exeLocalXdgOpen = false;
    220 			try {
    221 				await fs.access(localXdgOpenPath, fsConstants.X_OK);
    222 				exeLocalXdgOpen = true;
    223 			} catch {}
    224 
    225 			const useSystemXdgOpen = process.versions.electron
    226 				?? (platform === 'android' || isBundled || !exeLocalXdgOpen);
    227 			command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
    228 		}
    229 
    230 		if (appArguments.length > 0) {
    231 			cliArguments.push(...appArguments);
    232 		}
    233 
    234 		if (!options.wait) {
    235 			// `xdg-open` will block the process unless stdio is ignored
    236 			// and it's detached from the parent even if it's unref'd.
    237 			childProcessOptions.stdio = 'ignore';
    238 			childProcessOptions.detached = true;
    239 		}
    240 	}
    241 
    242 	if (options.target) {
    243 		cliArguments.push(options.target);
    244 	}
    245 
    246 	if (platform === 'darwin' && appArguments.length > 0) {
    247 		cliArguments.push('--args', ...appArguments);
    248 	}
    249 
    250 	const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
    251 
    252 	if (options.wait) {
    253 		return new Promise((resolve, reject) => {
    254 			subprocess.once('error', reject);
    255 
    256 			subprocess.once('close', exitCode => {
    257 				if (!options.allowNonzeroExitCode && exitCode > 0) {
    258 					reject(new Error(`Exited with code ${exitCode}`));
    259 					return;
    260 				}
    261 
    262 				resolve(subprocess);
    263 			});
    264 		});
    265 	}
    266 
    267 	subprocess.unref();
    268 
    269 	return subprocess;
    270 };
    271 
    272 const open = (target, options) => {
    273 	if (typeof target !== 'string') {
    274 		throw new TypeError('Expected a `target`');
    275 	}
    276 
    277 	return baseOpen({
    278 		...options,
    279 		target,
    280 	});
    281 };
    282 
    283 export const openApp = (name, options) => {
    284 	if (typeof name !== 'string') {
    285 		throw new TypeError('Expected a `name`');
    286 	}
    287 
    288 	const {arguments: appArguments = []} = options ?? {};
    289 	if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) {
    290 		throw new TypeError('Expected `appArguments` as Array type');
    291 	}
    292 
    293 	return baseOpen({
    294 		...options,
    295 		app: {
    296 			name,
    297 			arguments: appArguments,
    298 		},
    299 	});
    300 };
    301 
    302 function detectArchBinary(binary) {
    303 	if (typeof binary === 'string' || Array.isArray(binary)) {
    304 		return binary;
    305 	}
    306 
    307 	const {[arch]: archBinary} = binary;
    308 
    309 	if (!archBinary) {
    310 		throw new Error(`${arch} is not supported`);
    311 	}
    312 
    313 	return archBinary;
    314 }
    315 
    316 function detectPlatformBinary({[platform]: platformBinary}, {wsl}) {
    317 	if (wsl && isWsl) {
    318 		return detectArchBinary(wsl);
    319 	}
    320 
    321 	if (!platformBinary) {
    322 		throw new Error(`${platform} is not supported`);
    323 	}
    324 
    325 	return detectArchBinary(platformBinary);
    326 }
    327 
    328 export const apps = {};
    329 
    330 defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({
    331 	darwin: 'google chrome',
    332 	win32: 'chrome',
    333 	linux: ['google-chrome', 'google-chrome-stable', 'chromium'],
    334 }, {
    335 	wsl: {
    336 		ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
    337 		x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'],
    338 	},
    339 }));
    340 
    341 defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({
    342 	darwin: 'firefox',
    343 	win32: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
    344 	linux: 'firefox',
    345 }, {
    346 	wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe',
    347 }));
    348 
    349 defineLazyProperty(apps, 'edge', () => detectPlatformBinary({
    350 	darwin: 'microsoft edge',
    351 	win32: 'msedge',
    352 	linux: ['microsoft-edge', 'microsoft-edge-dev'],
    353 }, {
    354 	wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
    355 }));
    356 
    357 defineLazyProperty(apps, 'browser', () => 'browser');
    358 
    359 defineLazyProperty(apps, 'browserPrivate', () => 'browserPrivate');
    360 
    361 export default open;