'use strict'; const {promisify} = require('util'); const path = require('path'); const childProcess = require('child_process'); const fs = require('fs'); const isWsl = require('is-wsl'); const isDocker = require('is-docker'); const pAccess = promisify(fs.access); const pExecFile = promisify(childProcess.execFile); // Path to included `xdg-open`. const localXdgOpenPath = path.join(__dirname, 'xdg-open'); // Convert a path from WSL format to Windows format: // `/mnt/c/Program Files/Example/MyApp.exe` → `C:\Program Files\Example\MyApp.exe` const wslToWindowsPath = async path => { const {stdout} = await pExecFile('wslpath', ['-w', path]); return stdout.trim(); }; // Convert a path from Windows format to WSL format const windowsToWslPath = async path => { const {stdout} = await pExecFile('wslpath', [path]); return stdout.trim(); }; // Get an environment variable from Windows const wslGetWindowsEnvVar = async envVar => { const {stdout} = await pExecFile('wslvar', [envVar]); return stdout.trim(); }; module.exports = async (target, options) => { if (typeof target !== 'string') { throw new TypeError('Expected a `target`'); } options = { wait: false, background: false, allowNonzeroExitCode: false, ...options }; let command; let {app} = options; let appArguments = []; const cliArguments = []; const childProcessOptions = {}; if (Array.isArray(app)) { appArguments = app.slice(1); app = app[0]; } if (process.platform === 'darwin') { command = 'open'; if (options.wait) { cliArguments.push('--wait-apps'); } if (options.background) { cliArguments.push('--background'); } if (app) { cliArguments.push('-a', app); } } else if (process.platform === 'win32' || (isWsl && !isDocker())) { const windowsRoot = isWsl ? await wslGetWindowsEnvVar('systemroot') : process.env.SYSTEMROOT; command = String.raw`${windowsRoot}\System32\WindowsPowerShell\v1.0\powershell${isWsl ? '.exe' : ''}`; cliArguments.push( '-NoProfile', '-NonInteractive', '–ExecutionPolicy', 'Bypass', '-EncodedCommand' ); if (isWsl) { command = await windowsToWslPath(command); } else { childProcessOptions.windowsVerbatimArguments = true; } const encodedArguments = ['Start']; if (options.wait) { encodedArguments.push('-Wait'); } if (app) { if (isWsl && app.startsWith('/mnt/')) { const windowsPath = await wslToWindowsPath(app); app = windowsPath; } // Double quote with double quotes to ensure the inner quotes are passed through. // Inner quotes are delimited for PowerShell interpretation with backticks. encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList'); appArguments.unshift(target); } else { encodedArguments.push(`"\`"${target}\`""`); } if (appArguments.length > 0) { appArguments = appArguments.map(arg => `"\`"${arg}\`""`); encodedArguments.push(appArguments.join(',')); } // Using Base64-encoded command, accepted by PowerShell, to allow special characters. target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64'); } else { if (app) { command = app; } else { // When bundled by Webpack, there's no actual package file path and no local `xdg-open`. const isBundled = !__dirname || __dirname === '/'; // Check if local `xdg-open` exists and is executable. let exeLocalXdgOpen = false; try { await pAccess(localXdgOpenPath, fs.constants.X_OK); exeLocalXdgOpen = true; } catch (_) {} const useSystemXdgOpen = process.versions.electron || process.platform === 'android' || isBundled || !exeLocalXdgOpen; command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath; } if (appArguments.length > 0) { cliArguments.push(...appArguments); } if (!options.wait) { // `xdg-open` will block the process unless stdio is ignored // and it's detached from the parent even if it's unref'd. childProcessOptions.stdio = 'ignore'; childProcessOptions.detached = true; } } cliArguments.push(target); if (process.platform === 'darwin' && appArguments.length > 0) { cliArguments.push('--args', ...appArguments); } const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions); if (options.wait) { return new Promise((resolve, reject) => { subprocess.once('error', reject); subprocess.once('close', exitCode => { if (options.allowNonzeroExitCode && exitCode > 0) { reject(new Error(`Exited with code ${exitCode}`)); return; } resolve(subprocess); }); }); } subprocess.unref(); return subprocess; };