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

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.
This commit is contained in:
Antonio Scandurra
2016-09-07 11:27:47 +02:00
parent db76b9fb39
commit 0f6eadcfce
6 changed files with 55 additions and 32 deletions

View File

@@ -244,6 +244,17 @@ class ApplicationDelegate
new Disposable ->
ipcRenderer.removeListener('context-command', outerCallback)
onSaveWindowState: (callback) ->
outerCallback = (event, message) ->
callback(event)
ipcRenderer.on('save-window-state', outerCallback)
new Disposable ->
ipcRenderer.removeListener('save-window-state', outerCallback)
didSaveWindowState: ->
ipcRenderer.send('did-save-window-state')
didCancelWindowUnload: ->
ipcRenderer.send('did-cancel-window-unload')

View File

@@ -674,6 +674,10 @@ class AtomEnvironment extends Model
@disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
@disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
@disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
@disposables.add @applicationDelegate.onSaveWindowState =>
callback = => @applicationDelegate.didSaveWindowState()
@saveState({isUnloading: true}).catch(callback).then(callback)
@listenForUpdates()
@registerDefaultTargetForKeymaps()
@@ -713,7 +717,6 @@ class AtomEnvironment extends Model
unloadEditorWindow: ->
return if not @project
@saveState({isUnloading: true})
@storeWindowBackground()
@packages.deactivatePackages()
@saveBlobStoreSync()
@@ -851,18 +854,17 @@ class AtomEnvironment extends Model
@blobStore.save()
saveState: (options) ->
return Promise.resolve() unless @enablePersistence
new Promise (resolve, reject) =>
return if not @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
if @enablePersistence and @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
else
resolve()
loadState: ->
if @enablePersistence

View File

@@ -96,11 +96,10 @@ class AtomApplication
@launch(options)
destroy: ->
@disposable.dispose()
windowsClosePromises = @windows.map (window) ->
window.close()
window.closedPromise
Promise.all(windowsClosePromises)
Promise.all(windowsClosePromises).then(=> @disposable.dispose())
launch: (options) ->
if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test
@@ -233,8 +232,11 @@ class AtomApplication
@openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
@disposable.add ipcHelpers.on app, 'before-quit', =>
@quitting = true
@disposable.add ipcHelpers.on app, 'before-quit', (event) =>
unless @quitting
event.preventDefault()
@quitting = true
Promise.all(@windows.map((window) -> window.saveState())).then(-> app.quit())
@disposable.add ipcHelpers.on app, 'will-quit', =>
@killAllProcesses()
@@ -322,6 +324,8 @@ class AtomApplication
@disposable.add ipcHelpers.on ipcMain, 'did-cancel-window-unload', =>
@quitting = false
for window in @windows
window.didCancelWindowUnload()
clipboard = require '../safe-clipboard'
@disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) ->

View File

@@ -1,4 +1,4 @@
{BrowserWindow, app, dialog} = require 'electron'
{BrowserWindow, app, dialog, ipcMain} = require 'electron'
path = require 'path'
fs = require 'fs'
url = require 'url'
@@ -128,8 +128,12 @@ class AtomWindow
false
handleEvents: ->
@browserWindow.on 'close', =>
@atomApplication.saveState(false)
@browserWindow.on 'close', (event) =>
unless @atomApplication.quitting or @unloading
event.preventDefault()
@unloading = true
@atomApplication.saveState(false)
@saveState().then(=> @close())
@browserWindow.on 'closed', =>
@fileRecoveryService.didCloseWindow(this)
@@ -170,6 +174,18 @@ class AtomWindow
@browserWindow.on 'blur', =>
@browserWindow.focusOnWebView()
didCancelWindowUnload: ->
@unloading = false
saveState: ->
new Promise (resolve) =>
callback = (event) =>
if BrowserWindow.fromWebContents(event.sender) is @browserWindow
ipcMain.removeListener('did-save-window-state', callback)
resolve()
ipcMain.on('did-save-window-state', callback)
@browserWindow.webContents.send('save-window-state')
openPath: (pathToOpen, initialLine, initialColumn) ->
@openLocations([{pathToOpen, initialLine, initialColumn}])