From 90a337b8196863c55de16531c8df62b6179efa21 Mon Sep 17 00:00:00 2001 From: rxm Date: Sun, 14 Jun 2026 22:10:42 +0200 Subject: [PATCH] feat(main): prevent multiple app instances with single-instance lock Acquire app.requestSingleInstanceLock() before startup. A second launch (double-clicking the exe while DropPilot is in the tray, or auto-start firing alongside a manual launch) fails to acquire the lock and quits before creating a window or starting any Twitch/IPC services. Electron forwards that launch to the running process via the "second-instance" event, which restores the existing window from the tray and focuses it. A close-to-tray default means the running instance usually has a hidden but alive window, so the handler calls show()/restore() before focus(). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/index.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/index.ts b/src/main/index.ts index 1760c98..ae6345c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,6 +15,26 @@ import { loadStats, saveStats, bumpStats, resetStats } from "./core/stats"; const isDev = !app.isPackaged; +// Single-instance guard: only one DropPilot process may run at a time. A second +// launch (double-clicking the exe while it's already in the tray, or auto-start +// firing alongside a manual launch) fails to acquire the lock and exits before +// doing any startup work. Electron forwards that launch to the first process via +// the "second-instance" event, which we use to surface the running window. +const gotSingleInstanceLock = app.requestSingleInstanceLock(); +if (!gotSingleInstanceLock) { + app.quit(); +} else { + app.on("second-instance", () => { + const [win] = BrowserWindow.getAllWindows(); + if (!win) return; + if (win.isMinimized()) win.restore(); + // Default close-to-tray hides the window rather than destroying it, so a + // running instance often has a hidden-but-alive window to reveal. + if (!win.isVisible()) win.show(); + win.focus(); + }); +} + /** * Behavior flags read on every close/minimize event. Seeded from settings * at startup; refreshed whenever the IPC layer saves new settings (see @@ -458,6 +478,9 @@ function setupAutoUpdater() { } app.whenReady().then(async () => { + // Losing instance: the lock guard above already called app.quit(); bail before + // creating a window or starting any Twitch/IPC services. + if (!gotSingleInstanceLock) return; const startHidden = process.argv.includes("--start-in-tray"); // Read settings before creating the window so we can restore bounds + seed // behavior flags before any close/minimize event can fire.