Skip to content

Commit c08d8a4

Browse files
committed
- await async sqlite close on Electron quit to prevent node_sqlite3 abort
- JIRA: ZAPP-1714
1 parent 88797db commit c08d8a4

3 files changed

Lines changed: 38 additions & 11 deletions

File tree

src-electron/main-process/startup.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,21 +1198,31 @@ function clearDatabaseFile(dbPath) {
11981198
}
11991199

12001200
/**
1201-
* Shuts down any servers that might be running.
1201+
* Shuts down any servers that might be running, and closes the database.
1202+
*
1203+
* NOTE: node-sqlite3's Database.close() is always asynchronous, even when the
1204+
* wrapper is called "sync". If the close work completes after Electron has
1205+
* begun tearing down the Node environment, node-sqlite3 will try to invoke its
1206+
* JS completion callback into a dying isolate and trigger napi_fatal_error
1207+
* (SIGABRT). To avoid that, this function returns a promise that resolves
1208+
* only after the database close callback has actually fired. Callers in
1209+
* Electron's 'will-quit' must preventDefault(), await this, and then app.exit().
1210+
*
1211+
* @returns Promise that resolves when shutdown is complete.
12021212
*/
1203-
function shutdown() {
1213+
async function shutdown() {
12041214
env.logInfo('Shutting down HTTP and IPC servers...')
12051215
ipcServer.shutdownServerSync()
12061216
httpServer.shutdownHttpServerSync()
12071217

12081218
if (mainDatabase != null) {
1209-
// Use a sync call, because you can't have promises in the 'quit' event.
1219+
let dbToClose = mainDatabase
1220+
mainDatabase = null
12101221
try {
1211-
dbApi.closeDatabaseSync(mainDatabase)
1212-
mainDatabase = null
1222+
await dbApi.closeDatabase(dbToClose)
12131223
env.logInfo('Database closed, shutting down.')
12141224
} catch (err) {
1215-
env.logError('Failed to close database.')
1225+
env.logError(`Failed to close database: ${err}`)
12161226
}
12171227
}
12181228
}

src-electron/server/ipc-server.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,16 @@ function handlerConvert(context, data) {
111111
* @param {*} data
112112
*/
113113
function handlerStop(context, data) {
114-
console.log('Shutting down because of remote client request.')
114+
env.logInfo('Shutting down because of remote client request.')
115115
server.ipc.server.emit(
116116
context.socket,
117117
eventType.overAndOut,
118118
'Shutting down server.'
119119
)
120-
startup.shutdown()
121-
util.waitFor(1000).then(() => startup.quit())
120+
startup
121+
.shutdown()
122+
.catch((err) => env.logError(`Error during shutdown: ${err}`))
123+
.finally(() => util.waitFor(1000).then(() => startup.quit()))
122124
}
123125

124126
/**

src-electron/ui/main-ui.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,23 @@ function hookMainInstanceEvents(argv) {
7777
})
7878
}
7979

80-
app.on('will-quit', () => {
81-
startup.shutdown()
80+
let isCleanShutdownDone = false
81+
app.on('will-quit', (event) => {
82+
// node-sqlite3 closes asynchronously. If we let Electron tear down the
83+
// Node environment before the close callback fires, node-sqlite3 will
84+
// try to invoke a JS callback in a destroyed isolate and abort() (see
85+
// https://github.com/TryGhost/node-sqlite3 Database::Work_AfterClose).
86+
// Defer the actual quit until the async shutdown finishes, then use
87+
// app.exit() which does NOT re-fire 'will-quit'.
88+
if (isCleanShutdownDone) return
89+
event.preventDefault()
90+
startup
91+
.shutdown()
92+
.catch((err) => env.logError(`Error during shutdown: ${err}`))
93+
.finally(() => {
94+
isCleanShutdownDone = true
95+
app.exit(0)
96+
})
8297
})
8398

8499
app.on('second-instance', (event, commandLine, workingDirectory) => {

0 commit comments

Comments
 (0)