335 lines
7.7 KiB
JavaScript
335 lines
7.7 KiB
JavaScript
|
const path = require('path');
|
|||
|
const childProcess = require('child_process');
|
|||
|
const {promises: fs, constants: fsConstants} = require('fs');
|
|||
|
const isWsl = require('is-wsl');
|
|||
|
const isDocker = require('is-docker');
|
|||
|
const defineLazyProperty = require('define-lazy-prop');
|
|||
|
|
|||
|
// Path to included `xdg-open`.
|
|||
|
const localXdgOpenPath = path.join(__dirname, 'xdg-open');
|
|||
|
|
|||
|
const {platform, arch} = process;
|
|||
|
|
|||
|
// Podman detection
|
|||
|
const hasContainerEnv = () => {
|
|||
|
try {
|
|||
|
fs.statSync('/run/.containerenv');
|
|||
|
return true;
|
|||
|
} catch {
|
|||
|
return false;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
let cachedResult;
|
|||
|
function isInsideContainer() {
|
|||
|
if (cachedResult === undefined) {
|
|||
|
cachedResult = hasContainerEnv() || isDocker();
|
|||
|
}
|
|||
|
|
|||
|
return cachedResult;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
Get the mount point for fixed drives in WSL.
|
|||
|
|
|||
|
@inner
|
|||
|
@returns {string} The mount point.
|
|||
|
*/
|
|||
|
const getWslDrivesMountPoint = (() => {
|
|||
|
// Default value for "root" param
|
|||
|
// according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config
|
|||
|
const defaultMountPoint = '/mnt/';
|
|||
|
|
|||
|
let mountPoint;
|
|||
|
|
|||
|
return async function () {
|
|||
|
if (mountPoint) {
|
|||
|
// Return memoized mount point value
|
|||
|
return mountPoint;
|
|||
|
}
|
|||
|
|
|||
|
const configFilePath = '/etc/wsl.conf';
|
|||
|
|
|||
|
let isConfigFileExists = false;
|
|||
|
try {
|
|||
|
await fs.access(configFilePath, fsConstants.F_OK);
|
|||
|
isConfigFileExists = true;
|
|||
|
} catch {}
|
|||
|
|
|||
|
if (!isConfigFileExists) {
|
|||
|
return defaultMountPoint;
|
|||
|
}
|
|||
|
|
|||
|
const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'});
|
|||
|
const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
|
|||
|
|
|||
|
if (!configMountPoint) {
|
|||
|
return defaultMountPoint;
|
|||
|
}
|
|||
|
|
|||
|
mountPoint = configMountPoint.groups.mountPoint.trim();
|
|||
|
mountPoint = mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`;
|
|||
|
|
|||
|
return mountPoint;
|
|||
|
};
|
|||
|
})();
|
|||
|
|
|||
|
const pTryEach = async (array, mapper) => {
|
|||
|
let latestError;
|
|||
|
|
|||
|
for (const item of array) {
|
|||
|
try {
|
|||
|
return await mapper(item); // eslint-disable-line no-await-in-loop
|
|||
|
} catch (error) {
|
|||
|
latestError = error;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
throw latestError;
|
|||
|
};
|
|||
|
|
|||
|
const baseOpen = async options => {
|
|||
|
options = {
|
|||
|
wait: false,
|
|||
|
background: false,
|
|||
|
newInstance: false,
|
|||
|
allowNonzeroExitCode: false,
|
|||
|
...options
|
|||
|
};
|
|||
|
|
|||
|
if (Array.isArray(options.app)) {
|
|||
|
return pTryEach(options.app, singleApp => baseOpen({
|
|||
|
...options,
|
|||
|
app: singleApp
|
|||
|
}));
|
|||
|
}
|
|||
|
|
|||
|
let {name: app, arguments: appArguments = []} = options.app || {};
|
|||
|
appArguments = [...appArguments];
|
|||
|
|
|||
|
if (Array.isArray(app)) {
|
|||
|
return pTryEach(app, appName => baseOpen({
|
|||
|
...options,
|
|||
|
app: {
|
|||
|
name: appName,
|
|||
|
arguments: appArguments
|
|||
|
}
|
|||
|
}));
|
|||
|
}
|
|||
|
|
|||
|
let command;
|
|||
|
const cliArguments = [];
|
|||
|
const childProcessOptions = {};
|
|||
|
|
|||
|
if (platform === 'darwin') {
|
|||
|
command = 'open';
|
|||
|
|
|||
|
if (options.wait) {
|
|||
|
cliArguments.push('--wait-apps');
|
|||
|
}
|
|||
|
|
|||
|
if (options.background) {
|
|||
|
cliArguments.push('--background');
|
|||
|
}
|
|||
|
|
|||
|
if (options.newInstance) {
|
|||
|
cliArguments.push('--new');
|
|||
|
}
|
|||
|
|
|||
|
if (app) {
|
|||
|
cliArguments.push('-a', app);
|
|||
|
}
|
|||
|
} else if (platform === 'win32' || (isWsl && !isInsideContainer() && !app)) {
|
|||
|
const mountPoint = await getWslDrivesMountPoint();
|
|||
|
|
|||
|
command = isWsl ?
|
|||
|
`${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` :
|
|||
|
`${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`;
|
|||
|
|
|||
|
cliArguments.push(
|
|||
|
'-NoProfile',
|
|||
|
'-NonInteractive',
|
|||
|
'–ExecutionPolicy',
|
|||
|
'Bypass',
|
|||
|
'-EncodedCommand'
|
|||
|
);
|
|||
|
|
|||
|
if (!isWsl) {
|
|||
|
childProcessOptions.windowsVerbatimArguments = true;
|
|||
|
}
|
|||
|
|
|||
|
const encodedArguments = ['Start'];
|
|||
|
|
|||
|
if (options.wait) {
|
|||
|
encodedArguments.push('-Wait');
|
|||
|
}
|
|||
|
|
|||
|
if (app) {
|
|||
|
// 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');
|
|||
|
if (options.target) {
|
|||
|
appArguments.unshift(options.target);
|
|||
|
}
|
|||
|
} else if (options.target) {
|
|||
|
encodedArguments.push(`"${options.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.
|
|||
|
options.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 fs.access(localXdgOpenPath, fsConstants.X_OK);
|
|||
|
exeLocalXdgOpen = true;
|
|||
|
} catch {}
|
|||
|
|
|||
|
const useSystemXdgOpen = process.versions.electron ||
|
|||
|
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;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (options.target) {
|
|||
|
cliArguments.push(options.target);
|
|||
|
}
|
|||
|
|
|||
|
if (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;
|
|||
|
};
|
|||
|
|
|||
|
const open = (target, options) => {
|
|||
|
if (typeof target !== 'string') {
|
|||
|
throw new TypeError('Expected a `target`');
|
|||
|
}
|
|||
|
|
|||
|
return baseOpen({
|
|||
|
...options,
|
|||
|
target
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
const openApp = (name, options) => {
|
|||
|
if (typeof name !== 'string') {
|
|||
|
throw new TypeError('Expected a `name`');
|
|||
|
}
|
|||
|
|
|||
|
const {arguments: appArguments = []} = options || {};
|
|||
|
if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) {
|
|||
|
throw new TypeError('Expected `appArguments` as Array type');
|
|||
|
}
|
|||
|
|
|||
|
return baseOpen({
|
|||
|
...options,
|
|||
|
app: {
|
|||
|
name,
|
|||
|
arguments: appArguments
|
|||
|
}
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
function detectArchBinary(binary) {
|
|||
|
if (typeof binary === 'string' || Array.isArray(binary)) {
|
|||
|
return binary;
|
|||
|
}
|
|||
|
|
|||
|
const {[arch]: archBinary} = binary;
|
|||
|
|
|||
|
if (!archBinary) {
|
|||
|
throw new Error(`${arch} is not supported`);
|
|||
|
}
|
|||
|
|
|||
|
return archBinary;
|
|||
|
}
|
|||
|
|
|||
|
function detectPlatformBinary({[platform]: platformBinary}, {wsl}) {
|
|||
|
if (wsl && isWsl) {
|
|||
|
return detectArchBinary(wsl);
|
|||
|
}
|
|||
|
|
|||
|
if (!platformBinary) {
|
|||
|
throw new Error(`${platform} is not supported`);
|
|||
|
}
|
|||
|
|
|||
|
return detectArchBinary(platformBinary);
|
|||
|
}
|
|||
|
|
|||
|
const apps = {};
|
|||
|
|
|||
|
defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({
|
|||
|
darwin: 'google chrome',
|
|||
|
win32: 'chrome',
|
|||
|
linux: ['google-chrome', 'google-chrome-stable', 'chromium']
|
|||
|
}, {
|
|||
|
wsl: {
|
|||
|
ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|||
|
x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe']
|
|||
|
}
|
|||
|
}));
|
|||
|
|
|||
|
defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({
|
|||
|
darwin: 'firefox',
|
|||
|
win32: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
|
|||
|
linux: 'firefox'
|
|||
|
}, {
|
|||
|
wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe'
|
|||
|
}));
|
|||
|
|
|||
|
defineLazyProperty(apps, 'edge', () => detectPlatformBinary({
|
|||
|
darwin: 'microsoft edge',
|
|||
|
win32: 'msedge',
|
|||
|
linux: ['microsoft-edge', 'microsoft-edge-dev']
|
|||
|
}, {
|
|||
|
wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
|
|||
|
}));
|
|||
|
|
|||
|
open.apps = apps;
|
|||
|
open.openApp = openApp;
|
|||
|
|
|||
|
module.exports = open;
|