From 3342d5b9319c05b2ecd1d53f29ac1461e4ecce0f Mon Sep 17 00:00:00 2001 From: benzntech Date: Fri, 6 Mar 2026 18:54:00 +0530 Subject: [PATCH 1/2] feat(electron): add auto-update functionality with electron-updater --- electron/main.js | 118 ++++++++++++++++++++++++++++++++++++++++++ electron/package.json | 9 +++- electron/preload.js | 20 ++++++- 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/electron/main.js b/electron/main.js index eb0584ebcb..0f6b1f6d0c 100644 --- a/electron/main.js +++ b/electron/main.js @@ -26,10 +26,13 @@ const { nativeImage, shell, session, + dialog, + Notification, } = require("electron"); const path = require("path"); const { spawn } = require("child_process"); const fs = require("fs"); +const { autoUpdater } = require("electron-updater"); // ── Single Instance Lock ─────────────────────────────────── const gotTheLock = app.requestSingleInstanceLock(); @@ -62,6 +65,11 @@ let serverPort = 20128; const getServerUrl = () => `http://localhost:${serverPort}`; +// ── Auto-Updater Configuration ────────────────────────────── +autoUpdater.autoDownload = false; +autoUpdater.autoInstallOnAppQuit = true; +autoUpdater.logger = console; + // ── Helper: Send IPC event to renderer (#5) ──────────────── function sendToRenderer(channel, data) { if (mainWindow && !mainWindow.isDestroyed()) { @@ -103,6 +111,85 @@ async function waitForServerExit(proc, timeoutMs = 5000) { ]); } +// ── Auto-Updater Event Handlers ───────────────────────────── +function setupAutoUpdater() { + autoUpdater.on("checking-for-update", () => { + sendToRenderer("update-status", { status: "checking" }); + console.log("[Electron] Checking for updates..."); + }); + + autoUpdater.on("update-available", (info) => { + sendToRenderer("update-status", { status: "available", version: info.version }); + console.log("[Electron] Update available:", info.version); + }); + + autoUpdater.on("update-not-available", (info) => { + sendToRenderer("update-status", { status: "not-available", version: info.version }); + console.log("[Electron] No update available"); + }); + + autoUpdater.on("download-progress", (progress) => { + sendToRenderer("update-status", { + status: "downloading", + percent: Math.round(progress.percent), + transferred: progress.transferred, + total: progress.total, + }); + }); + + autoUpdater.on("update-downloaded", (info) => { + sendToRenderer("update-status", { status: "downloaded", version: info.version }); + console.log("[Electron] Update downloaded:", info.version); + + if (Notification.isSupported()) { + const notification = new Notification({ + title: "OmniRoute Update Ready", + body: `Version ${info.version} is ready to install. Click to restart.`, + }); + notification.on("click", () => { + autoUpdater.quitAndInstall(); + }); + notification.show(); + } + }); + + autoUpdater.on("error", (error) => { + sendToRenderer("update-status", { status: "error", message: error.message }); + console.error("[Electron] Update error:", error); + }); +} + +async function checkForUpdates(silent = false) { + if (isDev) { + console.log("[Electron] Dev mode — skipping auto-update"); + if (!silent) { + sendToRenderer("update-status", { status: "error", message: "Updates disabled in dev mode" }); + } + return; + } + try { + await autoUpdater.checkForUpdates(); + } catch (error) { + console.error("[Electron] Check for updates failed:", error); + if (!silent) { + sendToRenderer("update-status", { status: "error", message: error.message }); + } + } +} + +async function downloadUpdate() { + try { + await autoUpdater.downloadUpdate(); + } catch (error) { + console.error("[Electron] Download update failed:", error); + sendToRenderer("update-status", { status: "error", message: error.message }); + } +} + +function installUpdate() { + autoUpdater.quitAndInstall(); +} + // ── Content Security Policy (#15) ────────────────────────── function setupContentSecurityPolicy() { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { @@ -236,6 +323,11 @@ function createTray() { ], }, { type: "separator" }, + { + label: "Check for Updates", + click: () => checkForUpdates(false), + }, + { type: "separator" }, { label: "Quit", click: () => { @@ -391,6 +483,24 @@ function setupIpcHandlers() { }); ipcMain.on("window-close", () => mainWindow?.close()); + + // Auto-update IPC handlers + ipcMain.handle("check-for-updates", async () => { + await checkForUpdates(false); + return { success: true }; + }); + + ipcMain.handle("download-update", async () => { + await downloadUpdate(); + return { success: true }; + }); + + ipcMain.handle("install-update", () => { + installUpdate(); + return { success: true }; + }); + + ipcMain.handle("get-app-version", () => app.getVersion()); } // ── App Lifecycle ────────────────────────────────────────── @@ -407,6 +517,14 @@ app.whenReady().then(async () => { createWindow(); createTray(); setupIpcHandlers(); + setupAutoUpdater(); + + // Check for updates after a short delay (don't block startup) + if (!isDev) { + setTimeout(() => { + checkForUpdates(true); + }, 3000); + } // macOS: recreate window when dock icon clicked app.on("activate", () => { diff --git a/electron/package.json b/electron/package.json index fa744c563e..b4f9bbb6ec 100644 --- a/electron/package.json +++ b/electron/package.json @@ -15,7 +15,9 @@ "build:linux": "electron-builder --linux", "pack": "electron-builder --dir" }, - "dependencies": {}, + "dependencies": { + "electron-updater": "^6.8.3" + }, "devDependencies": { "electron": "^40.6.1", "electron-builder": "^25.1.8" @@ -28,6 +30,11 @@ "output": "dist-electron", "buildResources": "assets" }, + "publish": { + "provider": "github", + "owner": "diegosouzapw", + "repo": "OmniRoute" + }, "files": [ "main.js", "preload.js", diff --git a/electron/preload.js b/electron/preload.js index 64f70cf7af..a9f0f3eeea 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -13,9 +13,18 @@ const { contextBridge, ipcRenderer } = require("electron"); // ── Channel Whitelist ────────────────────────────────────── const VALID_CHANNELS = { - invoke: ["get-app-info", "open-external", "get-data-dir", "restart-server"], + invoke: [ + "get-app-info", + "open-external", + "get-data-dir", + "restart-server", + "check-for-updates", + "download-update", + "install-update", + "get-app-version", + ], send: ["window-minimize", "window-maximize", "window-close"], - receive: ["server-status", "port-changed"], + receive: ["server-status", "port-changed", "update-status"], }; // ── Fix #16: Generic IPC wrappers ────────────────────────── @@ -48,6 +57,12 @@ contextBridge.exposeInMainWorld("electronAPI", { openExternal: (url) => safeInvoke("open-external", url), getDataDir: () => safeInvoke("get-data-dir"), restartServer: () => safeInvoke("restart-server"), + getAppVersion: () => safeInvoke("get-app-version"), + + // ── Auto-Update ────────────────────────────────────────── + checkForUpdates: () => safeInvoke("check-for-updates"), + downloadUpdate: () => safeInvoke("download-update"), + installUpdate: () => safeInvoke("install-update"), // ── Send (fire-and-forget) ─────────────────────────────── minimizeWindow: () => safeSend("window-minimize"), @@ -58,6 +73,7 @@ contextBridge.exposeInMainWorld("electronAPI", { // Fix #6: Returns a disposer function for precise cleanup onServerStatus: (callback) => safeOn("server-status", callback), onPortChanged: (callback) => safeOn("port-changed", callback), + onUpdateStatus: (callback) => safeOn("update-status", callback), // ── Static Properties ──────────────────────────────────── isElectron: true, From c11c275678c8c9cf1f44b2d07b5ab23433de6452 Mon Sep 17 00:00:00 2001 From: benzntech Date: Fri, 6 Mar 2026 19:22:41 +0530 Subject: [PATCH 2/2] fix(electron): address auto-updater review issues - Remove unused dialog import - Stop Next.js server before quitAndInstall() to prevent data loss - Propagate errors from checkForUpdates/downloadUpdate to IPC handlers so renderer can distinguish success from failure - Remove meaningless return value from install-update handler --- electron/main.js | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/electron/main.js b/electron/main.js index 0f6b1f6d0c..ad05633ec8 100644 --- a/electron/main.js +++ b/electron/main.js @@ -26,7 +26,6 @@ const { nativeImage, shell, session, - dialog, Notification, } = require("electron"); const path = require("path"); @@ -167,26 +166,18 @@ async function checkForUpdates(silent = false) { } return; } - try { - await autoUpdater.checkForUpdates(); - } catch (error) { - console.error("[Electron] Check for updates failed:", error); - if (!silent) { - sendToRenderer("update-status", { status: "error", message: error.message }); - } - } + await autoUpdater.checkForUpdates(); } async function downloadUpdate() { - try { - await autoUpdater.downloadUpdate(); - } catch (error) { - console.error("[Electron] Download update failed:", error); - sendToRenderer("update-status", { status: "error", message: error.message }); - } + await autoUpdater.downloadUpdate(); } function installUpdate() { + if (nextServer) { + nextServer.kill("SIGTERM"); + nextServer = null; + } autoUpdater.quitAndInstall(); } @@ -486,18 +477,30 @@ function setupIpcHandlers() { // Auto-update IPC handlers ipcMain.handle("check-for-updates", async () => { - await checkForUpdates(false); - return { success: true }; + try { + await checkForUpdates(false); + return { success: true }; + } catch (error) { + console.error("[Electron] Check for updates failed:", error); + sendToRenderer("update-status", { status: "error", message: error.message }); + return { success: false, error: error.message }; + } }); ipcMain.handle("download-update", async () => { - await downloadUpdate(); - return { success: true }; + try { + await downloadUpdate(); + return { success: true }; + } catch (error) { + console.error("[Electron] Download update failed:", error); + sendToRenderer("update-status", { status: "error", message: error.message }); + return { success: false, error: error.message }; + } }); ipcMain.handle("install-update", () => { installUpdate(); - return { success: true }; + // No return value — app will quit and restart }); ipcMain.handle("get-app-version", () => app.getVersion());