index.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. 'use strict';
  2. const {promisify} = require('util');
  3. const path = require('path');
  4. const childProcess = require('child_process');
  5. const fs = require('fs');
  6. const isWsl = require('is-wsl');
  7. const isDocker = require('is-docker');
  8. const pAccess = promisify(fs.access);
  9. const pReadFile = promisify(fs.readFile);
  10. // Path to included `xdg-open`.
  11. const localXdgOpenPath = path.join(__dirname, 'xdg-open');
  12. /**
  13. Get the mount point for fixed drives in WSL.
  14. @inner
  15. @returns {string} The mount point.
  16. */
  17. const getWslDrivesMountPoint = (() => {
  18. // Default value for "root" param
  19. // according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config
  20. const defaultMountPoint = '/mnt/';
  21. let mountPoint;
  22. return async function () {
  23. if (mountPoint) {
  24. // Return memoized mount point value
  25. return mountPoint;
  26. }
  27. const configFilePath = '/etc/wsl.conf';
  28. let isConfigFileExists = false;
  29. try {
  30. await pAccess(configFilePath, fs.constants.F_OK);
  31. isConfigFileExists = true;
  32. } catch (_) {}
  33. if (!isConfigFileExists) {
  34. return defaultMountPoint;
  35. }
  36. const configContent = await pReadFile(configFilePath, {encoding: 'utf8'});
  37. const configMountPoint = /root\s*=\s*(.*)/g.exec(configContent);
  38. if (!configMountPoint) {
  39. return defaultMountPoint;
  40. }
  41. mountPoint = configMountPoint[1].trim();
  42. mountPoint = mountPoint.endsWith('/') ? mountPoint : mountPoint + '/';
  43. return mountPoint;
  44. };
  45. })();
  46. module.exports = async (target, options) => {
  47. if (typeof target !== 'string') {
  48. throw new TypeError('Expected a `target`');
  49. }
  50. options = {
  51. wait: false,
  52. background: false,
  53. allowNonzeroExitCode: false,
  54. ...options
  55. };
  56. let command;
  57. let {app} = options;
  58. let appArguments = [];
  59. const cliArguments = [];
  60. const childProcessOptions = {};
  61. if (Array.isArray(app)) {
  62. appArguments = app.slice(1);
  63. app = app[0];
  64. }
  65. if (process.platform === 'darwin') {
  66. command = 'open';
  67. if (options.wait) {
  68. cliArguments.push('--wait-apps');
  69. }
  70. if (options.background) {
  71. cliArguments.push('--background');
  72. }
  73. if (app) {
  74. cliArguments.push('-a', app);
  75. }
  76. } else if (process.platform === 'win32' || (isWsl && !isDocker())) {
  77. const mountPoint = await getWslDrivesMountPoint();
  78. command = isWsl ?
  79. `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` :
  80. `${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`;
  81. cliArguments.push(
  82. '-NoProfile',
  83. '-NonInteractive',
  84. '–ExecutionPolicy',
  85. 'Bypass',
  86. '-EncodedCommand'
  87. );
  88. if (!isWsl) {
  89. childProcessOptions.windowsVerbatimArguments = true;
  90. }
  91. const encodedArguments = ['Start'];
  92. if (options.wait) {
  93. encodedArguments.push('-Wait');
  94. }
  95. if (app) {
  96. // Double quote with double quotes to ensure the inner quotes are passed through.
  97. // Inner quotes are delimited for PowerShell interpretation with backticks.
  98. encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList');
  99. appArguments.unshift(target);
  100. } else {
  101. encodedArguments.push(`"${target}"`);
  102. }
  103. if (appArguments.length > 0) {
  104. appArguments = appArguments.map(arg => `"\`"${arg}\`""`);
  105. encodedArguments.push(appArguments.join(','));
  106. }
  107. // Using Base64-encoded command, accepted by PowerShell, to allow special characters.
  108. target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
  109. } else {
  110. if (app) {
  111. command = app;
  112. } else {
  113. // When bundled by Webpack, there's no actual package file path and no local `xdg-open`.
  114. const isBundled = !__dirname || __dirname === '/';
  115. // Check if local `xdg-open` exists and is executable.
  116. let exeLocalXdgOpen = false;
  117. try {
  118. await pAccess(localXdgOpenPath, fs.constants.X_OK);
  119. exeLocalXdgOpen = true;
  120. } catch (_) {}
  121. const useSystemXdgOpen = process.versions.electron ||
  122. process.platform === 'android' || isBundled || !exeLocalXdgOpen;
  123. command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
  124. }
  125. if (appArguments.length > 0) {
  126. cliArguments.push(...appArguments);
  127. }
  128. if (!options.wait) {
  129. // `xdg-open` will block the process unless stdio is ignored
  130. // and it's detached from the parent even if it's unref'd.
  131. childProcessOptions.stdio = 'ignore';
  132. childProcessOptions.detached = true;
  133. }
  134. }
  135. cliArguments.push(target);
  136. if (process.platform === 'darwin' && appArguments.length > 0) {
  137. cliArguments.push('--args', ...appArguments);
  138. }
  139. const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
  140. if (options.wait) {
  141. return new Promise((resolve, reject) => {
  142. subprocess.once('error', reject);
  143. subprocess.once('close', exitCode => {
  144. if (options.allowNonzeroExitCode && exitCode > 0) {
  145. reject(new Error(`Exited with code ${exitCode}`));
  146. return;
  147. }
  148. resolve(subprocess);
  149. });
  150. });
  151. }
  152. subprocess.unref();
  153. return subprocess;
  154. };