Skip to content
This repository has been archived by the owner on Mar 3, 2023. It is now read-only.

Wait for windows' state to be saved before closing the app or any window #12619

Merged
merged 3 commits into from
Sep 7, 2016

Conversation

as-cii
Copy link
Contributor

@as-cii as-cii commented Sep 7, 2016

This fixes a test failure on #12300 where the new Electron version prevents state from being serialized.

Previously, we used to save the window's state in the renderer process beforeunload event handler: because of the synchronous nature of event handlers and the asynchronous design of IndexedDB, this could potentially not save anything if windows close fast enough to prevent IndexedDB from committing the pending transaction containing the state. (Ref.: https://mzl.la/2bXCXDn)

With this commit, we will intercept the before-quit events on electron.app and the close event on BrowserWindow (which will fire respectively before quitting the application and before closing a
window), and prevent them from performing the default action. We will then ask each renderer process to save its state and, finally, close the window and/or the app when the IndexedDB transaction completes.

/cc: @atom/core @thomasjo

Previously, we used to save the window's state in the renderer process
`beforeunload` event handler: because of the synchronous nature of event
handlers and the asynchronous design of IndexedDB, this could
potentially not save anything if windows close fast enough to prevent
IndexedDB from committing the pending transaction containing the state.
(Ref.: https://mzl.la/2bXCXDn)

With this commit, we will intercept the `before-quit` events on
`electron.app` and the `close` event on `BrowserWindow` (which will fire
respectively before quitting the application and before closing a
window), and prevent them from performing the default action. We will
then ask each renderer process to save its state and, finally, close the
window and/or the app.
@@ -322,6 +324,8 @@ class AtomApplication

@disposable.add ipcHelpers.on ipcMain, 'did-cancel-window-unload', =>
@quitting = false
for window in @windows
window.didCancelWindowUnload()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might not be more readable, and certainly not important, but these two lines could be compacted into

window.didCancelWindowUnload() for window in @windows

Copy link
Contributor

@thomasjo thomasjo Sep 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and we should probably call it something else to avoid shadowing the window global? Better safe than sorry 🙈

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and we should probably call it something else to avoid shadowing the window global? Better safe than sorry

I think the main process doesn't have a window global, so we should be good. 👍

@thomasjo
Copy link
Contributor

thomasjo commented Sep 7, 2016

LGTM! 🚀

@@ -368,6 +370,23 @@ describe('AtomApplication', function () {
})
})

describe('before quitting', function () {
it('waits until all the windows have saved their state and then quits', async function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful test!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yo @as-cii, is this a new feature in Atom's spec runner? I previously used it and stuff from jasmine-fix to make it accept async callbacks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steelbrain These are integration tests, and are being executed outside of Atom's test runner harness. They are using a more modern release of Jasmine ✨

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomasjo wonderful 👍

Copy link
Contributor

@nathansobo nathansobo Sep 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are main process tests and use Mocha. Eventually we want to use Mocha for our core render process tests too but its not a priority right now. You can use Mocha today for packages via atom-mocha-test-runner.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are main process tests and use Mocha.

Oh wow, I totally forgot about that. But I remember it now because of the explicit function vs arrow function discussion.
Thanks for setting the record straight 🙇

event.preventDefault()
@unloading = true
@atomApplication.saveState(false)
@saveState().then(=> @close())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we wait to assign @unloading = true until after the close promise resolves? I could see a weird case where close is fired twice before the state finishes saving.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see a weird case where close is fired twice before the state finishes saving.

Yeah, depending on how big the state is (and, thus, how long it takes to serialize it), the user might be able to click the exit button more than once in rapid succession and cause the state to be serialized twice. I don't think it would be extremely, but it seems better to avoid it if we can.

@nathansobo
Copy link
Contributor

Quick and awesome work to clear this obstacle @as-cii. ⚡

@alexandernst
Copy link

Why are renderers responsible for writing to disk? A renderer, by definition, is a one-way method. Maybe the entire write-to-disk logic should be moved somewhere else?

@as-cii as-cii merged commit 549e65c into master Sep 7, 2016
@as-cii as-cii deleted the as-save-window-state-before-closing-window-or-app branch September 7, 2016 14:02
@as-cii as-cii mentioned this pull request Sep 7, 2016
29 tasks
@as-cii
Copy link
Contributor Author

as-cii commented Sep 7, 2016

Why are renderers responsible for writing to disk? A renderer, by definition, is a one-way method. Maybe the entire write-to-disk logic should be moved somewhere else?

In this case we're borrowing Electron's terminology to draw a distinction between the main process and the windows (render) processes. Each window is responsible for maintaining its own state and, since renderer processes have also access to the IndexedDB API, they just serialize and save state by themselves.

I guess at some point we might let the main process handle serialization/persistence of windows' state, but that would come with the cost of transmitting potentially big object graphs via IPC and I am not sure it would be worth it. Moreover, I don't think the IndexedDB API is exposed in the main process, which would probably require us switching to a different backing store.

as-cii pushed a commit that referenced this pull request Sep 9, 2016
This regression was caused by a nuance in the way we maintain state in
`AtomApplication` for open windows. Specifically, when closing the last
window on Windows and Linux, we were explicitly calling `app.quit`
*before* removing such window from the list of open ones. In turn, this
caused the new `before-quit` behavior introduced in #12619 to work
improperly because it made the application wait on saving the state of a
stale window before exiting.

With this commit we are fixing that by making sure the stale window is
removed before calling `app.quit` in `removeWindow`.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants