dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

index.js (7515B)


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