mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-25 01:49:06 -05:00
39a7f2a743
* wip: graceful exit * add types for signal-exit * use graceful exit; properly forward signals to forked process from gulp * rm extraneous * wait for child processes to close before exiting parent processes * do not teardown twice; allow for multiple will-quit messages * wait for async child to exit, clean up * bring back env vars rather than inlined undefines * updated tests * fix exit codes * pingpong code * system test * rm extraneous debug entry * fix various * fix operator precedence w/ promise * improper treekill usage, coerce null exitCode * fix concurrent duplicate .close() on watchers * changelog * clear timeout * refinements * refine * infer exit code from signal * if graceful exit fails in before-quit handler, exit with code 1 * v8 build errors * revert lockfile teardown to previous pattern, keep on graceful-exit * exit code 1 no matter what the signal is * system test snapshots * snapshot * integration test timing fixes, more robust teardown * no eventmap type huh * universal reset of graceful exit * rm all listeners from studio lifecycle * euid on windows * fix posix code 112 exit criterion * mock process.exit in interactive spec * fix graceful exit step * better handling of return types for exit codes * fixes an issue where system tests were not tearing down correctly * rm dead code * 112 * Update record_spec.js * better messaging, debugging * additional debugging for child process; fix CT teardown * changelog * fix run child fixture failing to exit * rm console * add proper error listener so test does not fail prematurely * system test snapshots; no longer emit err from async child * add back snapshots that got removed? * only resolve on signals, not reject * shut down gql ws server correctly * properly await project shutdown in unit test * prevent repeated graphql-ws .dispose() calls * fix spawn unit test * debounce signals in graceful exit signal handlers to account for duplicate kills from signal-exit * only emit sigint/term message once, in case signal-exit goes overboard * changelog * Apply suggestion from @cacieprins * do not double-resolve * rm unused types * rm all listeners on child process even if already killed * sigkill = exit with 137 * prevent zombie plugin child processes * prevent morgan output during shutdown * surround sigint messaging with newlines to improve visual experience * correct changelog * fix ci-only issues * enable mockery for morgan in before each instead of outside of test closure * changelog * fix out of order * ensure stdin is no longer set to raw mode when signals are received * fix spawn unit tests for setRawMode(false) * check tty in gulp script * lock, cache * wsp * prevent orphaned steps in graceful shutdown * dont stack sigint/term handlers in pkgs/electron open * changelog
375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
import _ from 'lodash'
|
|
import os from 'os'
|
|
import cp from 'child_process'
|
|
import path from 'path'
|
|
import Debug from 'debug'
|
|
import util from '../util'
|
|
import state from '../tasks/state'
|
|
import xvfb from './xvfb'
|
|
import { needsSandbox } from '../tasks/verify'
|
|
import { throwFormErrorText, getErrorSync, errors } from '../errors'
|
|
import readline from 'readline'
|
|
import process, { stdin, stdout, stderr } from 'process'
|
|
import { relativeToRepoRoot } from '../relative-to-repo-root'
|
|
import { filter, DEBUG_PREFIX } from '@packages/stderr-filtering'
|
|
import { PassThrough } from 'stream'
|
|
|
|
const debug = Debug('cypress:cli')
|
|
const debugElectron = Debug('cypress:electron')
|
|
const debugStderr = Debug('cypress:internal-stderr')
|
|
|
|
function isPlatform (platform: string): boolean {
|
|
return os.platform() === platform
|
|
}
|
|
|
|
function needsStderrPiped (needsXvfb: boolean): boolean {
|
|
return _.some([
|
|
isPlatform('darwin'),
|
|
|
|
(needsXvfb && isPlatform('linux')),
|
|
|
|
util.isPossibleLinuxWithIncorrectDisplay(),
|
|
])
|
|
}
|
|
|
|
function needsEverythingPipedDirectly (): boolean {
|
|
return isPlatform('win32')
|
|
}
|
|
|
|
function getStdioStrategy (needsXvfb: boolean): string | string[] {
|
|
if (needsEverythingPipedDirectly()) {
|
|
return 'pipe'
|
|
}
|
|
|
|
// https://github.com/cypress-io/cypress/issues/921
|
|
// https://github.com/cypress-io/cypress/issues/1143
|
|
// https://github.com/cypress-io/cypress/issues/1745
|
|
if (needsStderrPiped(needsXvfb)) {
|
|
// returning pipe here so we can massage stderr
|
|
// and remove garbage from Xlib and libuv
|
|
// due to starting the Xvfb process on linux
|
|
return ['inherit', 'inherit', 'pipe']
|
|
}
|
|
|
|
return 'inherit'
|
|
}
|
|
|
|
function createSpawnFunction (
|
|
executable: string,
|
|
args: string[],
|
|
options: any,
|
|
) {
|
|
return (overrides: any = {}): any => {
|
|
return new Promise(async (resolve: any, reject: any) => {
|
|
_.defaults(overrides, {
|
|
onStderrData: false,
|
|
})
|
|
|
|
const { onStderrData } = overrides
|
|
const envOverrides = util.getEnvOverrides(options)
|
|
const electronArgs: string[] = []
|
|
const node11WindowsFix = isPlatform('win32')
|
|
|
|
let startScriptPath: string | undefined
|
|
|
|
if (options.dev) {
|
|
executable = 'node'
|
|
// if we're in dev then reset
|
|
// the launch cmd to be 'npm run dev'
|
|
// This path is correct in the build output, but not the source code. This file gets bundled into
|
|
// `dist/spawn-<hash>.js`, which makes this resolution appear incorrect at first glance.
|
|
startScriptPath = relativeToRepoRoot('scripts/start.js')
|
|
if (!startScriptPath) {
|
|
throw new Error(`Cypress start script (scripts/start.js) not found in parent directory of ${__dirname}`)
|
|
}
|
|
}
|
|
|
|
if (!options.dev && needsSandbox()) {
|
|
electronArgs.push('--no-sandbox')
|
|
}
|
|
|
|
// strip dev out of child process options
|
|
/**
|
|
* @type {import('child_process').ForkOptions}
|
|
*/
|
|
let stdioOptions: any = _.pick(options, 'env', 'detached', 'stdio')
|
|
|
|
// figure out if we're going to be force enabling or disabling colors.
|
|
// also figure out whether we should force stdout and stderr into thinking
|
|
// it is a tty as opposed to a pipe.
|
|
stdioOptions.env = _.extend({}, stdioOptions.env, envOverrides)
|
|
|
|
if (node11WindowsFix) {
|
|
stdioOptions = _.extend({}, stdioOptions, { windowsHide: false })
|
|
}
|
|
|
|
if (util.isPossibleLinuxWithIncorrectDisplay()) {
|
|
// make sure we use the latest DISPLAY variable if any
|
|
debug('passing DISPLAY', process.env.DISPLAY)
|
|
stdioOptions.env.DISPLAY = process.env.DISPLAY
|
|
}
|
|
|
|
if (stdioOptions.env.ELECTRON_RUN_AS_NODE) {
|
|
// Since we are running electron as node, we need to add an entry point file.
|
|
startScriptPath = path.join(state.getBinaryPkgPath(path.dirname(executable)), '..', 'index.js')
|
|
} else {
|
|
// Start arguments with "--" so Electron knows these are OUR
|
|
// arguments and does not try to sanitize them. Otherwise on Windows
|
|
// an url in one of the arguments crashes it :(
|
|
// https://github.com/cypress-io/cypress/issues/5466
|
|
args = [...electronArgs, '--', ...args]
|
|
}
|
|
|
|
if (startScriptPath) {
|
|
args.unshift(startScriptPath)
|
|
}
|
|
|
|
if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) {
|
|
args.unshift(process.env.CYPRESS_INTERNAL_DEV_DEBUG)
|
|
}
|
|
|
|
debug('spawn args %o %o', args, _.omit(stdioOptions, 'env'))
|
|
debug('spawning Cypress with executable: %s', executable)
|
|
|
|
const platform = await util.getPlatformInfo().catch((e) => reject(e))
|
|
|
|
if (!platform) {
|
|
return
|
|
}
|
|
|
|
function resolveOn (event: any): any {
|
|
return function (code: any, signal: NodeJS.Signals): void {
|
|
debug('child event fired %o', { event, code, signal })
|
|
|
|
if (signal) {
|
|
if (signal === 'SIGINT') {
|
|
resolve(0)
|
|
} else {
|
|
resolve(128 + os.constants.signals[signal])
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
resolve(code ?? 1)
|
|
}
|
|
}
|
|
|
|
const child = cp.spawn(executable, args, stdioOptions)
|
|
|
|
child.on('close', resolveOn('close'))
|
|
|
|
child.on('exit', resolveOn('exit'))
|
|
|
|
child.on('error', reject)
|
|
|
|
if (isPlatform('win32')) {
|
|
const rl = readline.createInterface({
|
|
input: stdin,
|
|
output: stdout,
|
|
})
|
|
|
|
// on windows, SIGINT does not propagate to the child process when ctrl+c is pressed
|
|
// this makes sure all nested processes are closed(ex: firefox inside the server)
|
|
rl.on('SIGINT', async function () {
|
|
const kill = (await import('tree-kill')).default
|
|
|
|
kill(child.pid as number, 'SIGINT')
|
|
})
|
|
} else {
|
|
// Adding listeners here prevents immediate process.exit() for these signals.
|
|
// Exiting when the child process exits instead will allow the child process
|
|
// to log during the exit process.
|
|
|
|
// Unlike in windows, we do not need to propagate these signals to the child process
|
|
// tree.
|
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
debug('adding message for signal listener for %s', signal)
|
|
process.once(signal, async function () {
|
|
console.log(`\n\n${signal} received; Attempting to exit gracefully. Force exit with ^C again if needed.\n\n`)
|
|
if (process.stdin.isTTY) {
|
|
process.stdin.setRawMode(false)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// if stdio options is set to 'pipe', then
|
|
// we should set up pipes:
|
|
// process STDIN (read stream) => child STDIN (writeable)
|
|
// child STDOUT => process STDOUT
|
|
// child STDERR => process STDERR with additional filtering
|
|
if (child.stdin) {
|
|
debug('piping process STDIN into child STDIN')
|
|
stdin.pipe(child.stdin)
|
|
}
|
|
|
|
if (child.stdout) {
|
|
debug('piping child STDOUT to process STDOUT')
|
|
child.stdout.pipe(stdout)
|
|
}
|
|
|
|
// if this is defined then we are manually piping for linux
|
|
// to filter out the garbage
|
|
if (child.stderr) {
|
|
debug('piping child STDERR to process STDERR')
|
|
|
|
const sourceStream = new PassThrough()
|
|
|
|
child.on('close', () => {
|
|
sourceStream.end()
|
|
})
|
|
|
|
child.stderr.on('data', (data: any) => {
|
|
const str = data.toString()
|
|
|
|
if (onStderrData && onStderrData(str)) {
|
|
return
|
|
}
|
|
|
|
if (sourceStream.writable) {
|
|
sourceStream.write(data)
|
|
}
|
|
})
|
|
|
|
if (
|
|
(process.env.ELECTRON_ENABLE_LOGGING ?? '') === '1' ||
|
|
debugElectron.enabled ||
|
|
(process.env.CYPRESS_INTERNAL_ENV ?? '') === 'development'
|
|
) {
|
|
sourceStream.pipe(stderr, { end: false })
|
|
} else {
|
|
sourceStream.pipe(filter(stderr, debugStderr, DEBUG_PREFIX))
|
|
}
|
|
}
|
|
|
|
// https://github.com/cypress-io/cypress/issues/1841
|
|
// https://github.com/cypress-io/cypress/issues/5241
|
|
// In some versions of node, it will throw on windows
|
|
// when you close the parent process after piping
|
|
// into the child process. unpiping does not seem
|
|
// to have any effect. so we're just catching the
|
|
// error here and not doing anything.
|
|
stdin.on('error', (err: any) => {
|
|
debug('error on stdin', err)
|
|
if (['EPIPE', 'ENOTCONN'].includes(err.code)) {
|
|
return
|
|
}
|
|
|
|
reject(err)
|
|
})
|
|
|
|
if (stdioOptions.detached) {
|
|
child.unref()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
async function spawnInXvfb (spawn: ReturnType<typeof createSpawnFunction>): Promise<number> {
|
|
try {
|
|
await xvfb.start()
|
|
|
|
debug('xvfb started')
|
|
const code = await userFriendlySpawn(spawn)
|
|
|
|
return code
|
|
} finally {
|
|
await xvfb.stop()
|
|
}
|
|
}
|
|
|
|
async function userFriendlySpawn (spawn: ReturnType<typeof createSpawnFunction>, linuxWithDisplayEnv?: any): Promise<any> {
|
|
debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv))
|
|
|
|
let brokenGtkDisplay: boolean = false
|
|
|
|
const overrides: any = {}
|
|
|
|
if (linuxWithDisplayEnv) {
|
|
_.extend(overrides, {
|
|
electronLogging: true,
|
|
onStderrData (str: string): any {
|
|
// if we receive a broken pipe anywhere
|
|
// then we know that's why cypress exited early
|
|
if (util.isBrokenGtkDisplay(str)) {
|
|
brokenGtkDisplay = true
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
try {
|
|
const code: number = await spawn(overrides)
|
|
|
|
debug('tried spawning without xvfb, code', code, brokenGtkDisplay)
|
|
if (code !== 0 && brokenGtkDisplay) {
|
|
util.logBrokenGtkDisplayWarning()
|
|
|
|
return spawnInXvfb(spawn)
|
|
}
|
|
|
|
return code
|
|
} catch (error: any) {
|
|
debug('error in userFriendlySpawn', error)
|
|
// we can format and handle an error message from the code above
|
|
// prevent wrapping error again by using "known: undefined" filter
|
|
if ((error as any).known === undefined) {
|
|
const raiseErrorFn = throwFormErrorText(errors.unexpected)
|
|
|
|
await raiseErrorFn(error.message)
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
interface StartOptions {
|
|
dev?: boolean
|
|
env?: Record<string, string | undefined>
|
|
detached?: boolean
|
|
stdio?: string | string[]
|
|
}
|
|
|
|
export async function start (args: string | string[], options: StartOptions = {}): Promise<any> {
|
|
let executable = util.getEnv('CYPRESS_RUN_BINARY') ?
|
|
path.resolve(util.getEnv('CYPRESS_RUN_BINARY') as string) :
|
|
state.getPathToExecutable(state.getBinaryDir())
|
|
|
|
// Always push cwd into the args
|
|
// which additionally acts as a signal to the
|
|
// binary that it was invoked through the NPM module
|
|
const baseArgs = args ? (typeof args === 'string' ? [args] : args) : []
|
|
const decoratedArgs = baseArgs.concat([
|
|
'--cwd', process.cwd(),
|
|
'--userNodePath', process.execPath,
|
|
'--userNodeVersion', process.versions.node,
|
|
])
|
|
|
|
const needsXvfb = xvfb.isNeeded()
|
|
|
|
debug('needs to start own Xvfb?', needsXvfb)
|
|
|
|
const stdio = options.stdio ?? getStdioStrategy(needsXvfb)
|
|
const dev = options.dev ?? false
|
|
const detached = options.detached ?? false
|
|
const env = options.env ?? process.env
|
|
|
|
const spawn = createSpawnFunction(executable, decoratedArgs, { stdio, dev, detached, env })
|
|
|
|
if (needsXvfb) {
|
|
debug('starting xvfb')
|
|
|
|
return spawnInXvfb(spawn)
|
|
}
|
|
|
|
// 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()
|
|
|
|
debug('linuxWithDisplayEnv', linuxWithDisplayEnv)
|
|
|
|
return userFriendlySpawn(spawn, linuxWithDisplayEnv)
|
|
}
|