From 988826ac8ea43acf94ef10c546591c2d66016646 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:38:19 +0700 Subject: [PATCH] feat: implement single instance lock to ensure data integrity and resource efficiency --- apps/desktop/README.md | 7 ++ apps/desktop/src/main/index.ts | 118 +++++++++++++++++++-------------- 2 files changed, 77 insertions(+), 48 deletions(-) diff --git a/apps/desktop/README.md b/apps/desktop/README.md index e8653b7..220e0ee 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -146,6 +146,13 @@ A fully managed update lifecycle powered by `electron-updater`. Background downl A transparent proxy mechanism that handles cross-origin requests by sanitizing non-standard `app://` and `file://` Origin headers on outgoing requests and injecting permissive CORS response headers on incoming responses — allowing seamless integration with cloud APIs without server-side configuration changes. +### 🔒 Single Instance Lock & Data Integrity + +The application enforces a **single running instance** via `app.requestSingleInstanceLock()`. If a user attempts to launch a second instance, the duplicate process is terminated immediately and the existing window is restored and focused. This mechanism serves two critical purposes: + +- **Data Integrity**: Prevents race conditions and write conflicts in local databases (IndexedDB/PouchDB) that could arise from concurrent access by multiple Electron processes. +- **Resource Efficiency**: Avoids duplicate memory allocation, IPC handler registration, and protocol handler conflicts. + --- ## Hardened Security Perimeter diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 23c51c8..c083294 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -358,60 +358,82 @@ function sendToRenderer(channel: string, ...args: unknown[]): void { } } -// ─── 7. App Lifecycle ─────────────────────────────────────────── +// ─── 7. Single Instance Lock & App Lifecycle ──────────────────── +// Prevent multiple instances to protect local database integrity +// (IndexedDB/PouchDB) and avoid resource contention. -app.whenReady().then(() => { - // Register the custom protocol before creating the window - registerAppProtocol(); +const gotTheLock = app.requestSingleInstanceLock(); - // Setup CORS bypass for API calls - setupCorsBypass(); - - // Setup IPC handlers - setupPrinterIPC(); - setupAutoUpdaterIPC(); - setupAutoUpdaterEvents(); - - // Create the main window - createWindow(); - - // Check for updates on startup (production only) - if (!IS_DEV) { - setTimeout(async () => { - try { - await autoUpdater.checkForUpdatesAndNotify(); - } catch (err) { - // Gracefully handle offline or network errors - console.error('[AutoUpdater] Startup check failed (possibly offline):', err); +if (!gotTheLock) { + // Another instance is already running — terminate immediately. + app.quit(); +} else { + // ── Handle second-instance launch attempts ────────────────── + // If a user tries to open a second instance, restore and + // focus the existing window instead. + app.on('second-instance', () => { + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); } - }, 3000); - } - - // macOS: re-create window when dock icon is clicked - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); + mainWindow.focus(); } }); -}); -// Quit when all windows are closed (except macOS) -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } -}); + // ── Primary initialization ────────────────────────────────── + app.whenReady().then(() => { + // Register the custom protocol before creating the window + registerAppProtocol(); -// Security: prevent navigation to unexpected URLs -app.on('web-contents-created', (_event, contents) => { - contents.on('will-navigate', (event, url) => { - // Allow navigation within the app protocol and dev server - if ( - url.startsWith('app://') || - (IS_DEV && url.startsWith(DEV_SERVER_URL)) - ) { - return; + // Setup CORS bypass for API calls + setupCorsBypass(); + + // Setup IPC handlers + setupPrinterIPC(); + setupAutoUpdaterIPC(); + setupAutoUpdaterEvents(); + + // Create the main window + createWindow(); + + // Check for updates on startup (production only) + if (!IS_DEV) { + setTimeout(async () => { + try { + await autoUpdater.checkForUpdatesAndNotify(); + } catch (err) { + // Gracefully handle offline or network errors + console.error('[AutoUpdater] Startup check failed (possibly offline):', err); + } + }, 3000); } - event.preventDefault(); + + // macOS: re-create window when dock icon is clicked + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); }); -}); + + // Quit when all windows are closed (except macOS) + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } + }); + + // Security: prevent navigation to unexpected URLs + app.on('web-contents-created', (_event, contents) => { + contents.on('will-navigate', (event, url) => { + // Allow navigation within the app protocol and dev server + if ( + url.startsWith('app://') || + (IS_DEV && url.startsWith(DEV_SERVER_URL)) + ) { + return; + } + event.preventDefault(); + }); + }); +}