"use strict"; const _ = require('lodash'); const chalk = require('chalk'); const { Listr } = require('listr2'); const debug = require('debug')('cypress:cli'); const { stripIndent } = require('common-tags'); const Promise = require('bluebird'); const logSymbols = require('log-symbols'); const path = require('path'); const os = require('os'); const verbose = require('../VerboseRenderer'); const { throwFormErrorText, errors } = require('../errors'); const util = require('../util'); const logger = require('../logger'); const xvfb = require('../exec/xvfb'); const state = require('./state'); const VERIFY_TEST_RUNNER_TIMEOUT_MS = +util.getEnv('CYPRESS_VERIFY_TIMEOUT') || 30000; const checkExecutable = binaryDir => { const executable = state.getPathToExecutable(binaryDir); debug('checking if executable exists', executable); return util.isExecutableAsync(executable).then(isExecutable => { debug('Binary is executable? :', isExecutable); if (!isExecutable) { return throwFormErrorText(errors.binaryNotExecutable(executable))(); } }).catch({ code: 'ENOENT' }, () => { if (util.isCi()) { return throwFormErrorText(errors.notInstalledCI(executable))(); } return throwFormErrorText(errors.missingApp(binaryDir))(stripIndent` Cypress executable not found at: ${chalk.cyan(executable)} `); }); }; const runSmokeTest = (binaryDir, options) => { let executable = state.getPathToExecutable(binaryDir); const onSmokeTestError = (smokeTestCommand, linuxWithDisplayEnv) => { return err => { debug('Smoke test failed:', err); let errMessage = err.stderr || err.message; debug('error message:', errMessage); if (err.timedOut) { debug('error timedOut is true'); return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, true))(errMessage); } if (linuxWithDisplayEnv && util.isBrokenGtkDisplay(errMessage)) { util.logBrokenGtkDisplayWarning(); return throwFormErrorText(errors.invalidSmokeTestDisplayError)(errMessage); } return throwFormErrorText(errors.missingDependency)(errMessage); }; }; const needsXvfb = xvfb.isNeeded(); debug('needs Xvfb?', needsXvfb); /** * Spawn Cypress running smoke test to check if all operating system * dependencies are good. */ const spawn = linuxWithDisplayEnv => { const random = _.random(0, 1000); const args = ['--smoke-test', `--ping=${random}`]; if (needsSandbox()) { // electron requires --no-sandbox to run as root debug('disabling Electron sandbox'); args.unshift('--no-sandbox'); } if (options.dev) { executable = 'node'; args.unshift(path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js')); } const smokeTestCommand = `${executable} ${args.join(' ')}`; debug('running smoke test'); debug('using Cypress executable %s', executable); debug('smoke test command:', smokeTestCommand); debug('smoke test timeout %d ms', options.smokeTestTimeout); const stdioOptions = _.extend({}, { env: { ...process.env, FORCE_COLOR: 0 }, timeout: options.smokeTestTimeout }); return Promise.resolve(util.exec(executable, args, stdioOptions)).catch(onSmokeTestError(smokeTestCommand, linuxWithDisplayEnv)).then(result => { // TODO: when execa > 1.1 is released // change this to `result.all` for both stderr and stdout // use lodash to be robust during tests against null result or missing stdout const smokeTestStdout = _.get(result, 'stdout', ''); debug('smoke test stdout "%s"', smokeTestStdout); if (!util.stdoutLineMatches(String(random), smokeTestStdout)) { debug('Smoke test failed because could not find %d in:', random, result); const smokeTestStderr = _.get(result, 'stderr', ''); const errorText = smokeTestStderr || smokeTestStdout; return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, false))(errorText); } }); }; const spawnInXvfb = linuxWithDisplayEnv => { return xvfb.start().then(() => { return spawn(linuxWithDisplayEnv); }).finally(xvfb.stop); }; const userFriendlySpawn = linuxWithDisplayEnv => { debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv)); return spawn(linuxWithDisplayEnv).catch({ code: 'INVALID_SMOKE_TEST_DISPLAY_ERROR' }, () => { return spawnInXvfb(linuxWithDisplayEnv); }); }; if (needsXvfb) { return spawnInXvfb(); } // if we are on linux and there's already a DISPLAY // set, then we may need to rerun cypress after // spawning our own Xvfb server const linuxWithDisplayEnv = util.isPossibleLinuxWithIncorrectDisplay(); return userFriendlySpawn(linuxWithDisplayEnv); }; function testBinary(version, binaryDir, options) { debug('running binary verification check', version); // if running from 'cypress verify', don't print this message if (!options.force) { logger.log(stripIndent` It looks like this is your first time using Cypress: ${chalk.cyan(version)} `); } logger.log(); // if we are running in CI then use // the verbose renderer else use // the default let renderer = util.isCi() ? verbose : 'default'; if (logger.logLevel() === 'silent') renderer = 'silent'; const rendererOptions = { renderer }; const tasks = new Listr([{ options: { title: util.titleize('Verifying Cypress can run', chalk.gray(binaryDir)) }, task: (ctx, task) => { debug('clearing out the verified version'); return state.clearBinaryStateAsync(binaryDir).then(() => { return Promise.all([runSmokeTest(binaryDir, options), Promise.resolve().delay(1500) // good user experience ]); }).then(() => { debug('write verified: true'); return state.writeBinaryVerifiedAsync(true, binaryDir); }).then(() => { util.setTaskTitle(task, util.titleize(chalk.green('Verified Cypress!'), chalk.gray(binaryDir)), rendererOptions.renderer); }); } }], { rendererOptions }); return tasks.run(); } const maybeVerify = (installedVersion, binaryDir, options) => { return state.getBinaryVerifiedAsync(binaryDir).then(isVerified => { debug('is Verified ?', isVerified); let shouldVerify = !isVerified; // force verify if options.force if (options.force) { debug('force verify'); shouldVerify = true; } if (shouldVerify) { return testBinary(installedVersion, binaryDir, options).then(() => { if (options.welcomeMessage) { logger.log(); logger.log('Opening Cypress...'); } }); } }); }; const start = (options = {}) => { debug('verifying Cypress app'); const packageVersion = util.pkgVersion(); let binaryDir = state.getBinaryDir(packageVersion); _.defaults(options, { dev: false, force: false, welcomeMessage: true, smokeTestTimeout: VERIFY_TEST_RUNNER_TIMEOUT_MS }); if (options.dev) { return runSmokeTest('', options); } const parseBinaryEnvVar = () => { const envBinaryPath = util.getEnv('CYPRESS_RUN_BINARY'); debug('CYPRESS_RUN_BINARY exists, =', envBinaryPath); logger.log(stripIndent` ${chalk.yellow('Note:')} You have set the environment variable: ${chalk.white('CYPRESS_RUN_BINARY=')}${chalk.cyan(envBinaryPath)} This overrides the default Cypress binary path used. `); logger.log(); return util.isExecutableAsync(envBinaryPath).then(isExecutable => { debug('CYPRESS_RUN_BINARY is executable? :', isExecutable); if (!isExecutable) { return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(stripIndent` The supplied binary path is not executable `); } }).then(() => { return state.parseRealPlatformBinaryFolderAsync(envBinaryPath); }).then(envBinaryDir => { if (!envBinaryDir) { return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(); } debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir); binaryDir = envBinaryDir; }).catch({ code: 'ENOENT' }, err => { return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(err.message); }); }; return Promise.try(() => { debug('checking environment variables'); if (util.getEnv('CYPRESS_RUN_BINARY')) { return parseBinaryEnvVar(); } }).then(() => { return checkExecutable(binaryDir); }).tap(() => { return debug('binaryDir is ', binaryDir); }).then(() => { return state.getBinaryPkgAsync(binaryDir); }).then(pkg => { return state.getBinaryPkgVersion(pkg); }).then(binaryVersion => { if (!binaryVersion) { debug('no Cypress binary found for cli version ', packageVersion); return throwFormErrorText(errors.missingApp(binaryDir))(` Cannot read binary version from: ${chalk.cyan(state.getBinaryPkgPath(binaryDir))} `); } debug(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`); if (binaryVersion !== packageVersion) { // warn if we installed with CYPRESS_INSTALL_BINARY or changed version // in the package.json logger.log(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`); logger.log(); logger.warn(stripIndent` ${logSymbols.warning} Warning: Binary version ${chalk.green(binaryVersion)} does not match the expected package version ${chalk.green(packageVersion)} These versions may not work properly together. `); logger.log(); } return maybeVerify(binaryVersion, binaryDir, options); }).catch(err => { if (err.known) { throw err; } return throwFormErrorText(errors.unexpected)(err.stack); }); }; const isLinuxLike = () => os.platform() !== 'win32'; /** * Returns true if running on a system where Electron needs "--no-sandbox" flag. * @see https://crbug.com/638180 * * On Debian we had problems running in sandbox even for non-root users. * @see https://github.com/cypress-io/cypress/issues/5434 * Seems there is a lot of discussion around this issue among Electron users * @see https://github.com/electron/electron/issues/17972 */ const needsSandbox = () => isLinuxLike(); module.exports = { start, VERIFY_TEST_RUNNER_TIMEOUT_MS, needsSandbox };