diff --git a/src/main/commandRunner.ts b/src/main/commandRunner.ts index 9bbe9e6..218fb0c 100644 --- a/src/main/commandRunner.ts +++ b/src/main/commandRunner.ts @@ -155,3 +155,23 @@ export function killAllRunningCommands(): void { } running.clear() } + +// Stops every running project's containers in one shot (equivalent to +// `ddev stop` on each of them, but faster) so nothing is left running in the +// background after the app quits. Resolves rather than rejects on any +// failure — a missing/unresponsive ddev or docker shouldn't block quitting — +// and the timeout guards against poweroff hanging if the docker daemon is +// stuck, which would otherwise stall app exit indefinitely. +export function powerOffAllProjects(): Promise { + return new Promise((resolve) => { + const child = spawn('ddev', ['poweroff'], { env: ENV_WITH_DDEV_PATH, stdio: 'ignore' }) + const timeout = setTimeout(() => child.kill(), 10_000) + + const finish = (): void => { + clearTimeout(timeout) + resolve() + } + child.on('error', finish) + child.on('close', finish) + }) +} diff --git a/src/main/index.ts b/src/main/index.ts index e184929..e06bb0b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,7 +9,7 @@ import { registerAddonsIpc } from './ipc/addons' import { registerLogsIpc } from './ipc/logs' import { registerCreateIpc } from './ipc/create' import { registerWindowIpc } from './ipc/window' -import { killAllRunningCommands } from './commandRunner' +import { killAllRunningCommands, powerOffAllProjects } from './commandRunner' import { warmupDdevImagesIfNeeded } from './imageWarmup' function createWindow(): void { @@ -89,10 +89,19 @@ app.on('window-all-closed', () => { } }) -// Kill any still-running ddev processes (e.g. an open `ddev logs -f` stream) -// so they don't linger as orphans after the app exits. -app.on('before-quit', () => { +// Kill any still-running ddev processes (e.g. an open `ddev logs -f` stream), +// then power off every running project so sites don't keep running in the +// background after the app exits. Quit is deferred until poweroff finishes +// (or times out), so this intercepts the first before-quit and re-fires +// app.quit() itself once cleanup is done — the isQuitting guard stops that +// from looping back into this handler. +let isQuitting = false +app.on('before-quit', (event) => { + if (isQuitting) return + event.preventDefault() + isQuitting = true killAllRunningCommands() + void powerOffAllProjects().finally(() => app.quit()) }) // In this file you can include the rest of your app's specific main process