spawn.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. 'use strict';
  2. var _ = require('lodash');
  3. var os = require('os');
  4. var cp = require('child_process');
  5. var path = require('path');
  6. var Promise = require('bluebird');
  7. var debug = require('debug')('cypress:cli');
  8. var debugElectron = require('debug')('cypress:electron');
  9. var util = require('../util');
  10. var state = require('../tasks/state');
  11. var xvfb = require('./xvfb');
  12. var verify = require('../tasks/verify');
  13. var errors = require('../errors');
  14. var isXlibOrLibudevRe = /^(?:Xlib|libudev)/;
  15. var isHighSierraWarningRe = /\*\*\* WARNING/;
  16. var isRenderWorkerRe = /\.RenderWorker-/;
  17. var GARBAGE_WARNINGS = [isXlibOrLibudevRe, isHighSierraWarningRe, isRenderWorkerRe];
  18. var isGarbageLineWarning = function isGarbageLineWarning(str) {
  19. return _.some(GARBAGE_WARNINGS, function (re) {
  20. return re.test(str);
  21. });
  22. };
  23. function isPlatform(platform) {
  24. return os.platform() === platform;
  25. }
  26. function needsStderrPiped(needsXvfb) {
  27. return _.some([isPlatform('darwin'), needsXvfb && isPlatform('linux'), util.isPossibleLinuxWithIncorrectDisplay()]);
  28. }
  29. function needsEverythingPipedDirectly() {
  30. return isPlatform('win32');
  31. }
  32. function getStdio(needsXvfb) {
  33. if (needsEverythingPipedDirectly()) {
  34. return 'pipe';
  35. }
  36. // https://github.com/cypress-io/cypress/issues/921
  37. // https://github.com/cypress-io/cypress/issues/1143
  38. // https://github.com/cypress-io/cypress/issues/1745
  39. if (needsStderrPiped(needsXvfb)) {
  40. // returning pipe here so we can massage stderr
  41. // and remove garbage from Xlib and libuv
  42. // due to starting the Xvfb process on linux
  43. return ['inherit', 'inherit', 'pipe'];
  44. }
  45. return 'inherit';
  46. }
  47. module.exports = {
  48. isGarbageLineWarning: isGarbageLineWarning,
  49. start: function start(args) {
  50. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  51. var needsXvfb = xvfb.isNeeded();
  52. var executable = state.getPathToExecutable(state.getBinaryDir());
  53. if (util.getEnv('CYPRESS_RUN_BINARY')) {
  54. executable = path.resolve(util.getEnv('CYPRESS_RUN_BINARY'));
  55. }
  56. debug('needs to start own Xvfb?', needsXvfb);
  57. // 1. Start arguments with "--" so Electron knows these are OUR
  58. // arguments and does not try to sanitize them. Otherwise on Windows
  59. // an url in one of the arguments crashes it :(
  60. // https://github.com/cypress-io/cypress/issues/5466
  61. // 2. Always push cwd into the args
  62. // which additionally acts as a signal to the
  63. // binary that it was invoked through the NPM module
  64. args = ['--'].concat(args, '--cwd', process.cwd());
  65. _.defaults(options, {
  66. dev: false,
  67. env: process.env,
  68. detached: false,
  69. stdio: getStdio(needsXvfb)
  70. });
  71. var spawn = function spawn() {
  72. var overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  73. return new Promise(function (resolve, reject) {
  74. _.defaults(overrides, {
  75. onStderrData: false,
  76. electronLogging: false
  77. });
  78. if (options.dev) {
  79. // if we're in dev then reset
  80. // the launch cmd to be 'npm run dev'
  81. executable = 'node';
  82. args.unshift(path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js'));
  83. debug('in dev mode the args became %o', args);
  84. }
  85. var onStderrData = overrides.onStderrData,
  86. electronLogging = overrides.electronLogging;
  87. var envOverrides = util.getEnvOverrides();
  88. var electronArgs = _.clone(args);
  89. var node11WindowsFix = isPlatform('win32');
  90. if (!options.dev && verify.needsSandbox()) {
  91. // this is one of the Electron's command line switches
  92. // thus it needs to be before "--" separator
  93. electronArgs.unshift('--no-sandbox');
  94. }
  95. // strip dev out of child process options
  96. var stdioOptions = _.pick(options, 'env', 'detached', 'stdio');
  97. // figure out if we're going to be force enabling or disabling colors.
  98. // also figure out whether we should force stdout and stderr into thinking
  99. // it is a tty as opposed to a pipe.
  100. stdioOptions.env = _.extend({}, stdioOptions.env, envOverrides);
  101. if (node11WindowsFix) {
  102. stdioOptions = _.extend({}, stdioOptions, { windowsHide: false });
  103. }
  104. if (electronLogging) {
  105. stdioOptions.env.ELECTRON_ENABLE_LOGGING = true;
  106. }
  107. if (util.isPossibleLinuxWithIncorrectDisplay()) {
  108. // make sure we use the latest DISPLAY variable if any
  109. debug('passing DISPLAY', process.env.DISPLAY);
  110. stdioOptions.env.DISPLAY = process.env.DISPLAY;
  111. }
  112. debug('spawning Cypress with executable: %s', executable);
  113. debug('spawn args %o %o', electronArgs, _.omit(stdioOptions, 'env'));
  114. var child = cp.spawn(executable, electronArgs, stdioOptions);
  115. function resolveOn(event) {
  116. return function (code, signal) {
  117. debug('child event fired %o', { event: event, code: code, signal: signal });
  118. if (code === null) {
  119. var errorObject = errors.errors.childProcessKilled(event, signal);
  120. return errors.getError(errorObject).then(reject);
  121. }
  122. resolve(code);
  123. };
  124. }
  125. child.on('close', resolveOn('close'));
  126. child.on('exit', resolveOn('exit'));
  127. child.on('error', reject);
  128. // if stdio options is set to 'pipe', then
  129. // we should set up pipes:
  130. // process STDIN (read stream) => child STDIN (writeable)
  131. // child STDOUT => process STDOUT
  132. // child STDERR => process STDERR with additional filtering
  133. if (child.stdin) {
  134. debug('piping process STDIN into child STDIN');
  135. process.stdin.pipe(child.stdin);
  136. }
  137. if (child.stdout) {
  138. debug('piping child STDOUT to process STDOUT');
  139. child.stdout.pipe(process.stdout);
  140. }
  141. // if this is defined then we are manually piping for linux
  142. // to filter out the garbage
  143. if (child.stderr) {
  144. debug('piping child STDERR to process STDERR');
  145. child.stderr.on('data', function (data) {
  146. var str = data.toString();
  147. // bail if this is warning line garbage
  148. if (isGarbageLineWarning(str)) {
  149. return;
  150. }
  151. // if we have a callback and this explictly returns
  152. // false then bail
  153. if (onStderrData && onStderrData(str) === false) {
  154. return;
  155. }
  156. // else pass it along!
  157. process.stderr.write(data);
  158. });
  159. }
  160. // https://github.com/cypress-io/cypress/issues/1841
  161. // https://github.com/cypress-io/cypress/issues/5241
  162. // In some versions of node, it will throw on windows
  163. // when you close the parent process after piping
  164. // into the child process. unpiping does not seem
  165. // to have any effect. so we're just catching the
  166. // error here and not doing anything.
  167. process.stdin.on('error', function (err) {
  168. if (['EPIPE', 'ENOTCONN'].includes(err.code)) {
  169. return;
  170. }
  171. throw err;
  172. });
  173. if (stdioOptions.detached) {
  174. child.unref();
  175. }
  176. });
  177. };
  178. var spawnInXvfb = function spawnInXvfb() {
  179. return xvfb.start().then(userFriendlySpawn).finally(xvfb.stop);
  180. };
  181. var userFriendlySpawn = function userFriendlySpawn(linuxWithDisplayEnv) {
  182. debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv));
  183. var brokenGtkDisplay = void 0;
  184. var overrides = {};
  185. if (linuxWithDisplayEnv) {
  186. _.extend(overrides, {
  187. electronLogging: true,
  188. onStderrData: function onStderrData(str) {
  189. // if we receive a broken pipe anywhere
  190. // then we know that's why cypress exited early
  191. if (util.isBrokenGtkDisplay(str)) {
  192. brokenGtkDisplay = true;
  193. }
  194. // we should attempt to always slurp up
  195. // the stderr logs unless we've explicitly
  196. // enabled the electron debug logging
  197. if (!debugElectron.enabled) {
  198. return false;
  199. }
  200. }
  201. });
  202. }
  203. return spawn(overrides).then(function (code) {
  204. if (code !== 0 && brokenGtkDisplay) {
  205. util.logBrokenGtkDisplayWarning();
  206. return spawnInXvfb();
  207. }
  208. return code;
  209. })
  210. // we can format and handle an error message from the code above
  211. // prevent wrapping error again by using "known: undefined" filter
  212. .catch({ known: undefined }, errors.throwFormErrorText(errors.errors.unexpected));
  213. };
  214. if (needsXvfb) {
  215. return spawnInXvfb();
  216. }
  217. // if we are on linux and there's already a DISPLAY
  218. // set, then we may need to rerun cypress after
  219. // spawning our own Xvfb server
  220. var linuxWithDisplayEnv = util.isPossibleLinuxWithIncorrectDisplay();
  221. return userFriendlySpawn(linuxWithDisplayEnv);
  222. }
  223. };