Files
cypress/packages/server/lib/makeDataContext.ts
T
Cacie Prins 39a7f2a743 fix: graceful teardown of file watchers and spawned processes when exiting (#33542)
* 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
2026-05-15 12:36:16 -04:00

233 lines
7.1 KiB
TypeScript

import { DataContext, getCtx, clearCtx, setCtx } from '@packages/data-context'
// tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined
import electron, { OpenDialogOptions, SaveDialogOptions, BrowserWindow } from 'electron'
import { isListening } from './util/ensure-url'
import { isMainWindowFocused, focusMainWindow } from './gui/windows'
import type {
AllModeOptions,
AllowedState,
OpenProjectLaunchOpts,
FoundBrowser,
InitializeProjectOptions,
OpenProjectLaunchOptions,
Preferences,
} from '@packages/types'
import browserUtils from './browsers/utils'
import auth from './cloud/auth'
import user from './cloud/user'
import * as cohorts from './cohorts'
import { openProject } from './open_project'
import { cache } from './cache'
import { graphqlSchema } from '@packages/data-context/graphql/schema'
import { openExternal } from './gui/links'
import { getUserEditor } from './util/editors'
import * as savedState from './saved_state'
import appData from './util/app_data'
import browsers from './browsers'
import devServer from './plugins/dev-server'
import { remoteSchemaWrapped } from '@packages/data-context/graphql'
import { GracefulExit } from './util/graceful-exit'
const { getBrowsers, ensureAndGetByNameOrPath } = browserUtils
interface MakeDataContextOptions {
mode: 'run' | 'open'
modeOptions: Partial<AllModeOptions>
}
export { getCtx, setCtx, clearCtx }
export function makeDataContext (options: MakeDataContextOptions): DataContext {
const ctx = new DataContext({
schema: graphqlSchema,
schemaCloud: remoteSchemaWrapped,
...options,
browserApi: {
close: browsers.close,
getBrowsers,
async ensureAndGetByNameOrPath (nameOrPath: string) {
const browsers = await ctx.browser.allBrowsers()
return await ensureAndGetByNameOrPath(nameOrPath, false, browsers)
},
async focusActiveBrowserWindow () {
return openProject.sendFocusBrowserMessage()
},
async relaunchBrowser () {
await openProject.relaunchBrowser()
},
},
appApi: {
appData,
},
authApi: {
getUser () {
return user.get()
},
logIn (onMessage, utmSource, utmMedium, utmContent) {
return auth.start(onMessage, utmSource, utmMedium, utmContent)
},
logOut () {
return user.logOut()
},
resetAuthState () {
auth.stopServer()
},
},
projectApi: {
async launchProject (browser: FoundBrowser, spec: Cypress.Spec, options: OpenProjectLaunchOpts) {
await openProject.launch({ ...browser }, spec, options)
},
openProjectCreate (args: InitializeProjectOptions, options: OpenProjectLaunchOptions) {
return openProject.create(args.projectRoot, args, options)
},
insertProjectToCache (projectRoot: string) {
return cache.insertProject(projectRoot)
},
async getProjectRootsFromCache () {
return cache.getProjectRoots().then((roots) => {
return Promise.all(roots.map(async (projectRoot: string) => {
return {
projectRoot,
savedState: () => savedState.create(projectRoot).then((s) => s.get()),
}
}))
})
},
clearLatestProjectsCache () {
return cache.removeLatestProjects()
},
getProjectPreferencesFromCache () {
return cache.getProjectPreferences()
},
clearProjectPreferences (projectTitle: string) {
return cache.removeProjectPreferences(projectTitle)
},
clearAllProjectPreferences () {
return cache.removeAllProjectPreferences()
},
insertProjectPreferencesToCache (projectTitle: string, preferences: Preferences) {
return cache.insertProjectPreferences(projectTitle, preferences)
},
removeProjectFromCache (path: string) {
return cache.removeProject(path)
},
closeActiveProject () {
return openProject.closeActiveProject()
},
getCurrentBrowser () {
return (openProject?.getProject()?.browser) ?? undefined
},
getConfig () {
return openProject.getConfig()
},
getRemoteStates () {
return openProject.getRemoteStates()
},
getCurrentProjectSavedState () {
// TODO: See if this is the best way we should be getting this config,
// shouldn't we have this already in the DataContext?
try {
return openProject.getConfig()?.state
} catch {
return {}
}
},
setPromptShown (slug) {
return openProject.getProject()
?.saveState({
promptsShown: {
...(openProject.getProject()?.state?.promptsShown ?? {}),
[slug]: Date.now(),
},
})
},
setProjectPreferences (state) {
return openProject.getProject()?.saveState(state)
},
makeProjectSavedState (projectRoot: string) {
return () => savedState.create(projectRoot).then((s) => s.get())
},
getDevServer () {
return devServer
},
isListening,
resetBrowserTabsForNextSpec (shouldKeepTabOpen: boolean) {
return openProject.resetBrowserTabsForNextSpec(shouldKeepTabOpen)
},
resetServer () {
return openProject.getProject()?.server.reset()
},
async runSpec (spec: Cypress.Spec): Promise<void> {
openProject.changeUrlToSpec(spec)
},
routeToDebug (runNumber: number) {
openProject.changeUrlToDebug(runNumber)
},
},
electronApi: {
openExternal (url: string) {
openExternal(url).catch((e) => {
ctx.logTraceError(e)
})
},
showItemInFolder (folder: string) {
electron.shell.showItemInFolder(folder)
},
showOpenDialog (props: OpenDialogOptions) {
return electron.dialog.showOpenDialog(props)
},
showSaveDialog (window: BrowserWindow, props: SaveDialogOptions) {
return electron.dialog.showSaveDialog(window, props)
},
copyTextToClipboard (text: string) {
electron.clipboard.writeText(text)
},
isMainWindowFocused () {
return isMainWindowFocused()
},
focusMainWindow () {
return focusMainWindow()
},
createNotification (title, body) {
return new electron.Notification({ title, body })
},
},
localSettingsApi: {
async setPreferences (object: AllowedState) {
const state = await savedState.create()
return state.set(object)
},
async getPreferences () {
return (await savedState.create()).get()
},
async getAvailableEditors () {
const { availableEditors } = await getUserEditor(true)
return availableEditors
},
},
cohortsApi: {
async getCohorts () {
return cohorts.get()
},
async getCohort (name: string) {
return cohorts.getByName(name)
},
async insertCohort (cohort) {
cohorts.set(cohort)
},
},
})
GracefulExit.addStep(async () => {
await clearCtx()
}, 'clear data context')
return ctx
}