index.js 7.3 KB

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