feat: implement single instance lock to ensure data integrity and resource efficiency

This commit is contained in:
Firman Ramdhani
2026-04-06 10:38:19 +07:00
parent 75deeece9f
commit 988826ac8e
2 changed files with 77 additions and 48 deletions
+7
View File
@@ -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. 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 ## Hardened Security Perimeter
+70 -48
View File
@@ -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(() => { const gotTheLock = app.requestSingleInstanceLock();
// Register the custom protocol before creating the window
registerAppProtocol();
// Setup CORS bypass for API calls if (!gotTheLock) {
setupCorsBypass(); // Another instance is already running — terminate immediately.
app.quit();
// Setup IPC handlers } else {
setupPrinterIPC(); // ── Handle second-instance launch attempts ──────────────────
setupAutoUpdaterIPC(); // If a user tries to open a second instance, restore and
setupAutoUpdaterEvents(); // focus the existing window instead.
app.on('second-instance', () => {
// Create the main window if (mainWindow) {
createWindow(); if (mainWindow.isMinimized()) {
mainWindow.restore();
// 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); mainWindow.focus();
}
// 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) // ── Primary initialization ──────────────────────────────────
app.on('window-all-closed', () => { app.whenReady().then(() => {
if (process.platform !== 'darwin') { // Register the custom protocol before creating the window
app.quit(); registerAppProtocol();
}
});
// Security: prevent navigation to unexpected URLs // Setup CORS bypass for API calls
app.on('web-contents-created', (_event, contents) => { setupCorsBypass();
contents.on('will-navigate', (event, url) => {
// Allow navigation within the app protocol and dev server // Setup IPC handlers
if ( setupPrinterIPC();
url.startsWith('app://') || setupAutoUpdaterIPC();
(IS_DEV && url.startsWith(DEV_SERVER_URL)) setupAutoUpdaterEvents();
) {
return; // 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();
});
});
}