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, skipVerify: util.getEnv('CYPRESS_SKIP_VERIFY') === 'true', }) if (options.skipVerify) { debug('skipping verification of the Cypress app') return Promise.resolve() } 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, }