Skip to content

Commit 1b68869

Browse files
astone123tbiethman
andauthored
fix: Ensure that file watchers are closed before Electron exits (#22606)
* Adding prototype before-quit handler to handle async teardown. * Getting binary builds * Let's try this then * Working the async changes backwards, hope I got them all. Unit tests will be a disaster currently. * Actually getting build artifacts for testing * Moving logic to server interactive/run * fix: Fix some tests * fix: Revert changes to circle config * fix: Fix some tests * fix: Fix more tests * fix: Remove dead code comment * fix: Fix ProjectDataSource tests * fix: Add comment prefix * fix: Remove comment and unnecessary async * fix: Build Mac binary * Reverting run changes * Addressing PR comments. Cleaning up a few naming quibbles I had. * Addressing TODO regarding onLoadError watcher cleanup. * Correcting catch * Fighting some unref errors on these catches * Reverting making this private en lieu of binding * Should have left these as instances, whoops * Removing unnecessary test that was previously skipped * Adding a couple cheap unit tests for the new interactive mode behavior Co-authored-by: Tyler Biethman <[email protected]> Co-authored-by: Tyler Biethman <[email protected]>
1 parent 8e07318 commit 1b68869

30 files changed

+257
-178
lines changed

circle.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ macWorkflowFilters: &darwin-workflow-filters
3737
or:
3838
- equal: [ develop, << pipeline.git.branch >> ]
3939
- equal: [ linux-arm64, << pipeline.git.branch >> ]
40+
- equal: [ 'tbiethman/UNIFY-1816-prototype', << pipeline.git.branch >> ]
4041
- matches:
4142
pattern: "-release$"
4243
value: << pipeline.git.branch >>
@@ -128,7 +129,7 @@ commands:
128129
- run:
129130
name: Check current branch to persist artifacts
130131
command: |
131-
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "issue-22147-nohoist" ]]; then
132+
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "tbiethman/UNIFY-1816-prototype" ]]; then
132133
echo "Not uploading artifacts or posting install comment for this branch."
133134
circleci-agent step halt
134135
fi

packages/data-context/src/DataContext.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,10 +429,14 @@ export class DataContext {
429429
}
430430

431431
async destroy () {
432-
const destroy = util.promisify(this.coreData.servers.gqlServer?.destroy || (() => {}))
432+
let destroyGqlServer = () => Promise.resolve()
433+
434+
if (this.coreData.servers.gqlServer?.destroy) {
435+
destroyGqlServer = util.promisify(this.coreData.servers.gqlServer.destroy)
436+
}
433437

434438
return Promise.all([
435-
destroy(),
439+
destroyGqlServer(),
436440
this._reset(),
437441
])
438442
}

packages/data-context/src/actions/ProjectActions.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class ProjectActions {
9393
d.app.browserStatus = 'closed'
9494
})
9595

96-
this.ctx.lifecycleManager.clearCurrentProject()
96+
await this.ctx.lifecycleManager.clearCurrentProject()
9797
resetIssuedWarnings()
9898
await this.api.closeActiveProject()
9999
}
@@ -128,13 +128,13 @@ export class ProjectActions {
128128
await this.updateProjectList(() => this.api.insertProjectToCache(projectRoot))
129129

130130
await this.clearCurrentProject()
131-
this.ctx.lifecycleManager.setCurrentProject(projectRoot)
131+
await this.ctx.lifecycleManager.setCurrentProject(projectRoot)
132132
}
133133

134134
// Temporary: remove after other refactor lands
135-
setCurrentProjectAndTestingTypeForTestSetup (projectRoot: string) {
136-
this.ctx.lifecycleManager.clearCurrentProject()
137-
this.ctx.lifecycleManager.setCurrentProject(projectRoot)
135+
async setCurrentProjectAndTestingTypeForTestSetup (projectRoot: string) {
136+
await this.ctx.lifecycleManager.clearCurrentProject()
137+
await this.ctx.lifecycleManager.setCurrentProject(projectRoot)
138138
this.ctx.lifecycleManager.setCurrentTestingType('e2e')
139139
// @ts-expect-error - we are setting this as a convenience for our integration tests
140140
this.ctx._modeOptions = {}
@@ -416,7 +416,7 @@ export class ProjectActions {
416416

417417
this.ctx.actions.project.setSpecs(specs)
418418

419-
this.ctx.project.startSpecWatcher({
419+
await this.ctx.project.startSpecWatcher({
420420
projectRoot,
421421
testingType,
422422
specPattern,

packages/data-context/src/data/ProjectConfigManager.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type ProjectConfigManagerOptions = {
2727
handlers: IpcHandler[]
2828
hasCypressEnvFile: boolean
2929
eventRegistrar: EventRegistrar
30-
onError: (cypressError: CypressError, title?: string | undefined) => void
30+
onError: (cypressError: CypressError) => void
3131
onInitialConfigLoaded: (initialConfig: Cypress.ConfigOptions) => void
3232
onFinalConfigLoaded: (finalConfig: FullConfig, options: OnFinalConfigLoadedOptions) => Promise<void>
3333
refreshLifecycle: () => Promise<void>
@@ -106,7 +106,7 @@ export class ProjectConfigManager {
106106
this._state = 'loadingConfig'
107107

108108
// Clean things up for a new load
109-
this.closeWatchers()
109+
await this.closeWatchers()
110110
this._cachedLoadConfig = undefined
111111
this._cachedFullConfig = undefined
112112

@@ -137,7 +137,7 @@ export class ProjectConfigManager {
137137
}
138138

139139
this._state = 'errored'
140-
this.closeWatchers()
140+
await this.closeWatchers()
141141

142142
throw error
143143
} finally {
@@ -177,7 +177,7 @@ export class ProjectConfigManager {
177177
this._eventsIpc.cleanupIpc()
178178
}
179179

180-
this.closeWatchers()
180+
await this.closeWatchers()
181181

182182
throw error
183183
} finally {
@@ -310,9 +310,9 @@ export class ProjectConfigManager {
310310
)
311311
}
312312

313-
onLoadError = (error: any) => {
314-
this.closeWatchers()
315-
this.options.onError(error, 'Cypress configuration error')
313+
onLoadError = async (error: any) => {
314+
await this.closeWatchers()
315+
this.options.onError(error)
316316
}
317317

318318
private watchFiles (paths: string[]) {
@@ -495,17 +495,18 @@ export class ProjectConfigManager {
495495
return true
496496
}
497497

498-
private closeWatchers () {
499-
for (const watcher of this._watchers.values()) {
500-
// We don't care if there's an error while closing the watcher,
501-
// the watch listener on our end is already removed synchronously by chokidar
502-
watcher.close().catch((e) => {})
503-
}
498+
private async closeWatchers () {
499+
await Promise.all(Array.from(this._watchers).map((watcher) => {
500+
return watcher.close().catch((error) => {
501+
// Watcher close errors are ignored, we cannot meaningfully handle these
502+
})
503+
}))
504+
504505
this._watchers = new Set()
505506
this._pathToWatcherRecord = {}
506507
}
507508

508-
destroy () {
509+
async destroy () {
509510
if (this._eventsIpc) {
510511
this._eventsIpc.cleanupIpc()
511512
}
@@ -514,6 +515,6 @@ export class ProjectConfigManager {
514515
this._cachedLoadConfig = undefined
515516
this._cachedFullConfig = undefined
516517
this._registeredEventsTarget = undefined
517-
this.closeWatchers()
518+
await this.closeWatchers()
518519
}
519520
}

packages/data-context/src/data/ProjectLifecycleManager.ts

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,18 @@ export class ProjectLifecycleManager {
8989

9090
constructor (private ctx: DataContext) {
9191
this._eventRegistrar = new EventRegistrar()
92+
9293
if (ctx.coreData.currentProject) {
93-
this.setCurrentProject(ctx.coreData.currentProject)
94+
this._setCurrentProject(ctx.coreData.currentProject)
9495
}
9596

96-
process.on('exit', this.onProcessExit)
97-
9897
return autoBindDebug(this)
9998
}
10099

101100
get git () {
102101
return this.ctx.coreData.currentProjectGitInfo
103102
}
104103

105-
private onProcessExit = () => {
106-
this.resetInternalState()
107-
}
108-
109104
async getProjectId (): Promise<string | null> {
110105
try {
111106
const contents = await this.ctx.project.getConfig()
@@ -185,8 +180,8 @@ export class ProjectLifecycleManager {
185180
return this._configManager?.eventProcessPid
186181
}
187182

188-
clearCurrentProject () {
189-
this.resetInternalState()
183+
async clearCurrentProject () {
184+
await this.resetInternalState()
190185
this._initializedProject = undefined
191186
this._projectRoot = undefined
192187
}
@@ -215,13 +210,7 @@ export class ProjectLifecycleManager {
215210
handlers: getServerPluginHandlers(),
216211
hasCypressEnvFile: this._projectMetaState.hasCypressEnvFile,
217212
eventRegistrar: this._eventRegistrar,
218-
onError: (cypressError, title) => {
219-
if (this.ctx.isRunMode && this._pendingInitialize) {
220-
this._pendingInitialize.reject(cypressError)
221-
} else {
222-
this.ctx.onError(cypressError, title)
223-
}
224-
},
213+
onError: this.onLoadError,
225214
onInitialConfigLoaded: (initialConfig: Cypress.ConfigOptions) => {
226215
this._cachedInitialConfig = initialConfig
227216

@@ -391,23 +380,10 @@ export class ProjectLifecycleManager {
391380
return this._configManager.initializeConfig()
392381
}
393382

394-
/**
395-
* When we set the current project, we need to cleanup the
396-
* previous project that might have existed. We use this as the
397-
* single location we should use to set the `projectRoot`, because
398-
* we can call it from legacy code and it'll be a no-op if the `projectRoot`
399-
* is already the same, otherwise it'll do the necessary cleanup
400-
*/
401-
setCurrentProject (projectRoot: string) {
402-
if (this._projectRoot === projectRoot) {
403-
return
404-
}
405-
383+
private _setCurrentProject (projectRoot: string) {
406384
this._projectRoot = projectRoot
407385
this._initializedProject = undefined
408386

409-
this.resetInternalState()
410-
411387
this._configManager = this.createConfigManager()
412388

413389
// Preemptively load these so that they are available when we need them later
@@ -443,6 +419,23 @@ export class ProjectLifecycleManager {
443419
}
444420
}
445421

422+
/**
423+
* When we set the current project, we need to cleanup the
424+
* previous project that might have existed. We use this as the
425+
* single location we should use to set the `projectRoot`, because
426+
* we can call it from legacy code and it'll be a no-op if the `projectRoot`
427+
* is already the same, otherwise it'll do the necessary cleanup
428+
*/
429+
async setCurrentProject (projectRoot: string) {
430+
if (this._projectRoot === projectRoot) {
431+
return
432+
}
433+
434+
await this.resetInternalState()
435+
436+
this._setCurrentProject(projectRoot)
437+
}
438+
446439
/**
447440
* Handles pre-initialization checks. These will display warnings or throw with errors if catastrophic.
448441
* Returns false, if we're not ready to initialize due to needing to migrate
@@ -551,14 +544,14 @@ export class ProjectLifecycleManager {
551544
}
552545
}
553546

554-
private resetInternalState () {
547+
private async resetInternalState () {
555548
if (this._configManager) {
556-
this._configManager.destroy()
549+
await this._configManager.destroy()
557550
this._configManager = undefined
558551
}
559552

560-
this.ctx.coreData.currentProjectGitInfo?.destroy()
561-
this.ctx.project.destroy()
553+
await this.ctx.coreData.currentProjectGitInfo?.destroy()
554+
await this.ctx.project.destroy()
562555
this._currentTestingType = null
563556
this._cachedInitialConfig = undefined
564557
this._cachedFullConfig = undefined
@@ -580,9 +573,9 @@ export class ProjectLifecycleManager {
580573
return this._configManager.getConfigFileContents()
581574
}
582575

583-
reinitializeCypress () {
576+
async reinitializeCypress () {
584577
resetPluginHandlers()
585-
this.resetInternalState()
578+
await this.resetInternalState()
586579
}
587580

588581
registerEvent (event: string, callback: Function) {
@@ -717,10 +710,8 @@ export class ProjectLifecycleManager {
717710
}
718711
}
719712

720-
destroy () {
721-
this.resetInternalState()
722-
// @ts-ignore
723-
process.removeListener('exit', this.onProcessExit)
713+
async destroy () {
714+
await this.resetInternalState()
724715
}
725716

726717
isTestingTypeConfigured (testingType: TestingType): boolean {
@@ -798,8 +789,8 @@ export class ProjectLifecycleManager {
798789
* for run mode
799790
*/
800791
onLoadError = (err: CypressError) => {
801-
if (this.ctx.isRunMode && this._configManager) {
802-
this._configManager.onLoadError(err)
792+
if (this.ctx.isRunMode && this._pendingInitialize) {
793+
this._pendingInitialize.reject(err)
803794
} else {
804795
this.ctx.onError(err, 'Cypress configuration error')
805796
}

packages/data-context/src/index.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ export { globalPubSub }
2323

2424
let ctx: DataContext | null = null
2525

26-
// globalPubSub.on('cleanup', clearCtx)
27-
28-
/**
29-
* Shouldn't ever be called from runtime code, primarily for test situations where we need to
30-
*/
31-
export function clearCtx () {
32-
ctx = null
26+
export async function clearCtx () {
27+
if (ctx) {
28+
await ctx.destroy()
29+
ctx = null
30+
}
3331
}
3432

3533
export function hasCtx () {

packages/data-context/src/sources/GitDataSource.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,18 +156,18 @@ export class GitDataSource {
156156
return this.#currentBranch
157157
}
158158

159-
destroy () {
159+
async destroy () {
160160
this.#destroyed = true
161161
if (this.#intervalTimer) {
162162
clearTimeout(this.#intervalTimer)
163163
}
164164

165-
this.#destroyWatcher(this.#gitBaseDirWatcher)
165+
await this.#destroyWatcher(this.#gitBaseDirWatcher)
166166
}
167167

168-
#destroyWatcher (watcher?: chokidar.FSWatcher) {
168+
async #destroyWatcher (watcher?: chokidar.FSWatcher) {
169169
// Can't do anything actionable with these errors
170-
watcher?.close().catch((e) => {})
170+
await watcher?.close().catch((e) => {})
171171
}
172172

173173
async #loadAndWatchCurrentBranch () {

packages/data-context/src/sources/ProjectDataSource.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,15 @@ export class ProjectDataSource {
289289
return matched
290290
}
291291

292-
startSpecWatcher ({
292+
async startSpecWatcher ({
293293
projectRoot,
294294
testingType,
295295
specPattern,
296296
configSpecPattern,
297297
excludeSpecPattern,
298298
additionalIgnorePattern,
299299
}: FindSpecs<string[]>) {
300-
this.stopSpecWatcher()
300+
await this.stopSpecWatcher()
301301

302302
// Early return the spec watcher if we're in run mode, we do not want to
303303
// init a lot of files watchers that are unneeded
@@ -455,16 +455,16 @@ export class ProjectDataSource {
455455
return false
456456
}
457457

458-
destroy () {
459-
this.stopSpecWatcher()
458+
async destroy () {
459+
await this.stopSpecWatcher()
460460
}
461461

462-
stopSpecWatcher () {
462+
async stopSpecWatcher () {
463463
if (!this._specWatcher) {
464464
return
465465
}
466466

467-
this._specWatcher.close().catch(() => {})
467+
await this._specWatcher.close().catch(() => {})
468468
this._specWatcher = null
469469
}
470470

0 commit comments

Comments
 (0)