You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
361 lines
8.7 KiB
361 lines
8.7 KiB
import process from 'node:process'; |
|
import {Buffer} from 'node:buffer'; |
|
import path from 'node:path'; |
|
import {fileURLToPath} from 'node:url'; |
|
import childProcess from 'node:child_process'; |
|
import fs, {constants as fsConstants} from 'node:fs/promises'; |
|
import isWsl from 'is-wsl'; |
|
import defineLazyProperty from 'define-lazy-prop'; |
|
import defaultBrowser from 'default-browser'; |
|
import isInsideContainer from 'is-inside-container'; |
|
|
|
// Path to included `xdg-open`. |
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
|
const localXdgOpenPath = path.join(__dirname, 'xdg-open'); |
|
|
|
const {platform, arch} = process; |
|
|
|
/** |
|
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, |
|
}, |
|
})); |
|
} |
|
|
|
if (app === 'browser' || app === 'browserPrivate') { |
|
// IDs from default-browser for macOS and windows are the same |
|
const ids = { |
|
'com.google.chrome': 'chrome', |
|
'google-chrome.desktop': 'chrome', |
|
'org.mozilla.firefox': 'firefox', |
|
'firefox.desktop': 'firefox', |
|
'com.microsoft.msedge': 'edge', |
|
'com.microsoft.edge': 'edge', |
|
'microsoft-edge.desktop': 'edge', |
|
}; |
|
|
|
// Incognito flags for each browser in `apps`. |
|
const flags = { |
|
chrome: '--incognito', |
|
firefox: '--private-window', |
|
edge: '--inPrivate', |
|
}; |
|
|
|
const browser = await defaultBrowser(); |
|
if (browser.id in ids) { |
|
const browserName = ids[browser.id]; |
|
|
|
if (app === 'browserPrivate') { |
|
appArguments.push(flags[browserName]); |
|
} |
|
|
|
return baseOpen({ |
|
...options, |
|
app: { |
|
name: apps[browserName], |
|
arguments: appArguments, |
|
}, |
|
}); |
|
} |
|
|
|
throw new Error(`${browser.name} is not supported as a default browser`); |
|
} |
|
|
|
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 || process.env.windir || 'C:\\Windows'}\\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}\`""`); |
|
if (options.target) { |
|
appArguments.push(options.target); |
|
} |
|
} else if (options.target) { |
|
encodedArguments.push(`"${options.target}"`); |
|
} |
|
|
|
if (appArguments.length > 0) { |
|
appArguments = appArguments.map(argument => `"\`"${argument}\`""`); |
|
encodedArguments.push('-ArgumentList', 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 (platform === 'darwin' && appArguments.length > 0) { |
|
cliArguments.push('--args', ...appArguments); |
|
} |
|
|
|
// This has to come after `--args`. |
|
if (options.target) { |
|
cliArguments.push(options.target); |
|
} |
|
|
|
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, |
|
}); |
|
}; |
|
|
|
export const openApp = (name, options) => { |
|
if (typeof name !== 'string' && !Array.isArray(name)) { |
|
throw new TypeError('Expected a valid `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); |
|
} |
|
|
|
export 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', |
|
})); |
|
|
|
defineLazyProperty(apps, 'browser', () => 'browser'); |
|
|
|
defineLazyProperty(apps, 'browserPrivate', () => 'browserPrivate'); |
|
|
|
export default open;
|
|
|