index.js 7.7 KB

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