spawn.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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. // always push cwd into the args
  58. // which additionally acts as a signal to the
  59. // binary that it was invoked through the NPM module
  60. args = [].concat(args, '--cwd', process.cwd());
  61. _.defaults(options, {
  62. dev: false,
  63. env: process.env,
  64. detached: false,
  65. stdio: getStdio(needsXvfb)
  66. });
  67. var spawn = function spawn() {
  68. var overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  69. return new Promise(function (resolve, reject) {
  70. _.defaults(overrides, {
  71. onStderrData: false,
  72. electronLogging: false
  73. });
  74. if (options.dev) {
  75. // if we're in dev then reset
  76. // the launch cmd to be 'npm run dev'
  77. executable = 'node';
  78. args.unshift(path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js'));
  79. }
  80. var onStderrData = overrides.onStderrData,
  81. electronLogging = overrides.electronLogging;
  82. var envOverrides = util.getEnvOverrides();
  83. var electronArgs = _.clone(args);
  84. var node11WindowsFix = isPlatform('win32');
  85. if (verify.needsSandbox()) {
  86. electronArgs.push('--no-sandbox');
  87. }
  88. // strip dev out of child process options
  89. var stdioOptions = _.pick(options, 'env', 'detached', 'stdio');
  90. // figure out if we're going to be force enabling or disabling colors.
  91. // also figure out whether we should force stdout and stderr into thinking
  92. // it is a tty as opposed to a pipe.
  93. stdioOptions.env = _.extend({}, stdioOptions.env, envOverrides);
  94. if (node11WindowsFix) {
  95. stdioOptions = _.extend({}, stdioOptions, { windowsHide: false });
  96. }
  97. if (electronLogging) {
  98. stdioOptions.env.ELECTRON_ENABLE_LOGGING = true;
  99. }
  100. if (util.isPossibleLinuxWithIncorrectDisplay()) {
  101. // make sure we use the latest DISPLAY variable if any
  102. debug('passing DISPLAY', process.env.DISPLAY);
  103. stdioOptions.env.DISPLAY = process.env.DISPLAY;
  104. }
  105. debug('spawning Cypress with executable: %s', executable);
  106. debug('spawn args %o %o', electronArgs, _.omit(stdioOptions, 'env'));
  107. var child = cp.spawn(executable, electronArgs, stdioOptions);
  108. function resolveOn(event) {
  109. return function (code, signal) {
  110. debug('child event fired %o', { event: event, code: code, signal: signal });
  111. if (code === null) {
  112. var errorObject = errors.errors.childProcessKilled(event, signal);
  113. return errors.getError(errorObject).then(reject);
  114. }
  115. resolve(code);
  116. };
  117. }
  118. child.on('close', resolveOn('close'));
  119. child.on('exit', resolveOn('exit'));
  120. child.on('error', reject);
  121. // if stdio options is set to 'pipe', then
  122. // we should set up pipes:
  123. // process STDIN (read stream) => child STDIN (writeable)
  124. // child STDOUT => process STDOUT
  125. // child STDERR => process STDERR with additional filtering
  126. if (child.stdin) {
  127. debug('piping process STDIN into child STDIN');
  128. process.stdin.pipe(child.stdin);
  129. }
  130. if (child.stdout) {
  131. debug('piping child STDOUT to process STDOUT');
  132. child.stdout.pipe(process.stdout);
  133. }
  134. // if this is defined then we are manually piping for linux
  135. // to filter out the garbage
  136. if (child.stderr) {
  137. debug('piping child STDERR to process STDERR');
  138. child.stderr.on('data', function (data) {
  139. var str = data.toString();
  140. // bail if this is warning line garbage
  141. if (isGarbageLineWarning(str)) {
  142. return;
  143. }
  144. // if we have a callback and this explictly returns
  145. // false then bail
  146. if (onStderrData && onStderrData(str) === false) {
  147. return;
  148. }
  149. // else pass it along!
  150. process.stderr.write(data);
  151. });
  152. }
  153. // https://github.com/cypress-io/cypress/issues/1841
  154. // https://github.com/cypress-io/cypress/issues/5241
  155. // In some versions of node, it will throw on windows
  156. // when you close the parent process after piping
  157. // into the child process. unpiping does not seem
  158. // to have any effect. so we're just catching the
  159. // error here and not doing anything.
  160. process.stdin.on('error', function (err) {
  161. if (['EPIPE', 'ENOTCONN'].includes(err.code)) {
  162. return;
  163. }
  164. throw err;
  165. });
  166. if (stdioOptions.detached) {
  167. child.unref();
  168. }
  169. });
  170. };
  171. var spawnInXvfb = function spawnInXvfb() {
  172. return xvfb.start().then(userFriendlySpawn).finally(xvfb.stop);
  173. };
  174. var userFriendlySpawn = function userFriendlySpawn(linuxWithDisplayEnv) {
  175. debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv));
  176. var brokenGtkDisplay = void 0;
  177. var overrides = {};
  178. if (linuxWithDisplayEnv) {
  179. _.extend(overrides, {
  180. electronLogging: true,
  181. onStderrData: function onStderrData(str) {
  182. // if we receive a broken pipe anywhere
  183. // then we know that's why cypress exited early
  184. if (util.isBrokenGtkDisplay(str)) {
  185. brokenGtkDisplay = true;
  186. }
  187. // we should attempt to always slurp up
  188. // the stderr logs unless we've explicitly
  189. // enabled the electron debug logging
  190. if (!debugElectron.enabled) {
  191. return false;
  192. }
  193. }
  194. });
  195. }
  196. return spawn(overrides).then(function (code) {
  197. if (code !== 0 && brokenGtkDisplay) {
  198. util.logBrokenGtkDisplayWarning();
  199. return spawnInXvfb();
  200. }
  201. return code;
  202. })
  203. // we can format and handle an error message from the code above
  204. // prevent wrapping error again by using "known: undefined" filter
  205. .catch({ known: undefined }, errors.throwFormErrorText(errors.errors.unexpected));
  206. };
  207. if (needsXvfb) {
  208. return spawnInXvfb();
  209. }
  210. // if we are on linux and there's already a DISPLAY
  211. // set, then we may need to rerun cypress after
  212. // spawning our own Xvfb server
  213. var linuxWithDisplayEnv = util.isPossibleLinuxWithIncorrectDisplay();
  214. return userFriendlySpawn(linuxWithDisplayEnv);
  215. }
  216. };