From 6b2b9078b03b5d5e4a5a3a6216db19b9fbbf65bd Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 1 Feb 2026 18:03:42 +0530 Subject: [PATCH 1/3] feat: linux electron auto update working --- src-node/package-lock.json | 4 +- src/document/DocumentCommandHandlers.js | 28 +- src/extensionsIntegrated/appUpdater/main.js | 2 +- .../appUpdater/update-electron.js | 355 ++++++++++++++++++ 4 files changed, 380 insertions(+), 9 deletions(-) create mode 100644 src/extensionsIntegrated/appUpdater/update-electron.js diff --git a/src-node/package-lock.json b/src-node/package-lock.json index ec84f2d7c..edb8b114d 100644 --- a/src-node/package-lock.json +++ b/src-node/package-lock.json @@ -1,12 +1,12 @@ { "name": "@phcode/node-core", - "version": "5.1.1-0", + "version": "5.1.2-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phcode/node-core", - "version": "5.1.1-0", + "version": "5.1.2-0", "license": "GNU-AGPL3.0", "dependencies": { "@expo/sudo-prompt": "^9.3.2", diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index e9e64ca51..7d468458e 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -1731,10 +1731,18 @@ define(function (require, exports, module) { .finally(()=>{ raceAgainstTime(_safeNodeTerminate()) .finally(()=>{ - // In Electron, use allowClose() to bypass the close handler - // (which would otherwise trigger another cleanup cycle). + // In Electron multi-window case, use allowClose() to bypass + // the close handler (which would otherwise trigger another + // cleanup cycle). But for last window, closeWindow() calls + // quitApp() (no loop) and runs quitTimeAppUpdateHandler. if(window.__ELECTRON__) { - window.electronAPI.allowClose(); + Phoenix.app.getPhoenixInstanceCount().then(count => { + if(count === 1) { + Phoenix.app.closeWindow(); + } else { + window.electronAPI.allowClose(); + } + }); } else { Phoenix.app.closeWindow(); } @@ -2276,11 +2284,19 @@ define(function (require, exports, module) { raceAgainstTime(_safeNodeTerminate()) .finally(()=>{ closeInProgress = false; - // In Electron, we must call allowClose() to complete the original - // close request (sets forceClose=true). Calling closeWindow() would + // In Electron multi-window case, we must call allowClose() to + // complete the original close request. Calling closeWindow() would // trigger a new close sequence and cause an infinite loop. + // But for last window, closeWindow() calls quitApp() (no loop), + // and we need it to run quitTimeAppUpdateHandler. if(window.__ELECTRON__) { - window.electronAPI.allowClose(); + Phoenix.app.getPhoenixInstanceCount().then(count => { + if(count === 1) { + Phoenix.app.closeWindow(); + } else { + window.electronAPI.allowClose(); + } + }); } else { Phoenix.app.closeWindow(); } diff --git a/src/extensionsIntegrated/appUpdater/main.js b/src/extensionsIntegrated/appUpdater/main.js index cb477df8a..8bcd636ac 100644 --- a/src/extensionsIntegrated/appUpdater/main.js +++ b/src/extensionsIntegrated/appUpdater/main.js @@ -24,6 +24,7 @@ // shell.js file. This is app updates are pretty core level even though we do it as an extension here. define(function (require, exports, module) { + require("./update-electron"); const AppInit = require("utils/AppInit"), Metrics = require("utils/Metrics"), FileSystem = require("filesystem/FileSystem"), @@ -521,7 +522,6 @@ define(function (require, exports, module) { // app updates are only for desktop builds return; } - // todo electron edge for app updater if (brackets.platform === "mac") { // in mac, the `update.app.tar.gz` is downloaded, and only extracted on app quit. // we do this only in mac as the `.app` file is extracted only at app quit and deleted diff --git a/src/extensionsIntegrated/appUpdater/update-electron.js b/src/extensionsIntegrated/appUpdater/update-electron.js new file mode 100644 index 000000000..0bd838e53 --- /dev/null +++ b/src/extensionsIntegrated/appUpdater/update-electron.js @@ -0,0 +1,355 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/*global logger*/ + +// Electron-specific app updater for Linux +// Windows/Mac are not supported in Electron edge builds + +define(function (require, exports, module) { + const AppInit = require("utils/AppInit"), + Metrics = require("utils/Metrics"), + Commands = require("command/Commands"), + CommandManager = require("command/CommandManager"), + Menus = require("command/Menus"), + Dialogs = require("widgets/Dialogs"), + DefaultDialogs = require("widgets/DefaultDialogs"), + Strings = require("strings"), + marked = require('thirdparty/marked.min'), + semver = require("thirdparty/semver.browser"), + NotificationUI = require("widgets/NotificationUI"), + TaskManager = require("features/TaskManager"), + NativeApp = require("utils/NativeApp"), + PreferencesManager = require("preferences/PreferencesManager"); + + let updateTask, updatePendingRestart, updateFailed; + + const KEY_LAST_UPDATE_CHECK_TIME = "PH_LAST_UPDATE_CHECK_TIME", + KEY_LAST_UPDATE_DESCRIPTION = "PH_LAST_UPDATE_DESCRIPTION", + KEY_UPDATE_AVAILABLE = "PH_UPDATE_AVAILABLE"; + + const PREFS_AUTO_UPDATE = "autoUpdate"; + let isAutoUpdateFlow = true; + let updateScheduled = false; + let cachedUpdateDetails = null; + + function showOrHideUpdateIcon() { + if(updateScheduled && !updateTask) { + updateTask = TaskManager.addNewTask(Strings.UPDATING_APP, Strings.UPDATING_APP_MESSAGE, + ``, { + noSpinnerNotification: isAutoUpdateFlow, + onSelect: function () { + if(updatePendingRestart){ + Dialogs.showInfoDialog(Strings.UPDATE_READY_RESTART_TITLE, + Strings.UPDATE_READY_RESTART_INSTALL_MESSAGE); + } else if(updateFailed){ + Dialogs.showInfoDialog(Strings.UPDATE_FAILED_TITLE, Strings.UPDATE_FAILED_MESSAGE); + } else { + Dialogs.showInfoDialog(Strings.UPDATING_APP, Strings.UPDATING_APP_DIALOG_MESSAGE); + } + } + }); + if(!isAutoUpdateFlow) { + updateTask.show(); + } else { + updateTask.flashSpinnerForAttention(); + } + } + let updateAvailable = PreferencesManager.getViewState(KEY_UPDATE_AVAILABLE); + if(updateAvailable){ + $("#update-notification").removeClass("forced-hidden"); + } else { + $("#update-notification").addClass("forced-hidden"); + } + } + + function fetchJSON(url) { + return fetch(url) + .then(response => { + if (!response.ok) { + return null; + } + return response.json(); + }); + } + + async function getUpdatePlatformKey() { + const platformArch = await Phoenix.app.getPlatformArch(); + let os = 'windows'; + if (brackets.platform === "mac") { + os = "darwin"; + } else if (brackets.platform === "linux") { + os = "linux"; + } + return `${os}-${platformArch}`; + } + + async function getUpdateDetails() { + const updatePlatformKey = await getUpdatePlatformKey(); + const updateDetails = { + shouldUpdate: false, + updatePendingRestart: false, + downloadURL: null, + currentVersion: Phoenix.metadata.apiVersion, + updateVersion: null, + releaseNotesMarkdown: null, + updatePlatform: updatePlatformKey + }; + try{ + const updateMetadata = await fetchJSON(brackets.config.app_update_url); + // In Electron, binary version and loaded app version are always the same + // since both are loaded at app start and only change after full restart + const currentVersion = await window.electronAPI.getAppVersion(); + if(semver.gt(updateMetadata.version, currentVersion)){ + console.log("Update available: ", updateMetadata, "Detected platform: ", updatePlatformKey); + PreferencesManager.setViewState(KEY_UPDATE_AVAILABLE, true); + updateDetails.shouldUpdate = true; + updateDetails.updateVersion = updateMetadata.version; + updateDetails.releaseNotesMarkdown = updateMetadata.notes; + if(updateMetadata.platforms && updateMetadata.platforms[updatePlatformKey]){ + updateDetails.downloadURL = updateMetadata.platforms[updatePlatformKey].url; + } + } else { + console.log("no updates available for platform: ", updateDetails.updatePlatform); + PreferencesManager.setViewState(KEY_UPDATE_AVAILABLE, false); + } + showOrHideUpdateIcon(); + } catch (e) { + console.error("Error getting update metadata", e); + logger.reportError(e, `Error getting app update metadata`); + updateFailed = true; + Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'fail', "Unknown"+Phoenix.platform); + } + return updateDetails; + } + + /** + * Check if we're at an upgradable location. + * For Electron on Linux, we require the AppImage to be in ~/.phoenix-code/ + */ + async function isUpgradableLocation() { + try { + return true; //todo remove + const isPackaged = await window.electronAPI.isPackaged(); + if (!isPackaged) { + return false; + } + const homeDir = await window.electronFSAPI.homeDir(); + const phoenixInstallDir = `${homeDir}.phoenix-code/`; + const execPath = await window.electronAPI.getExecutablePath(); + return execPath.startsWith(phoenixInstallDir); + } catch (e) { + console.error(e); + return false; + } + } + + function _getButtons(isUpgradableLoc) { + const updateLater = + {className: Dialogs.DIALOG_BTN_CLASS_NORMAL, id: Dialogs.DIALOG_BTN_CANCEL, text: Strings.UPDATE_LATER }; + const getItNow = + { className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK, text: Strings.GET_IT_NOW }; + const updateOnExit = + { className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK, text: Strings.UPDATE_ON_EXIT }; + if(!isUpgradableLoc) { + return [updateLater, getItNow]; + } + return [updateLater, updateOnExit]; + } + + async function scheduleUpdate(updateDetails) { + updateScheduled = true; + updatePendingRestart = true; + cachedUpdateDetails = updateDetails; + showOrHideUpdateIcon(); + Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'scheduled', Phoenix.platform); + updateTask.setSucceded(); + updateTask.setTitle(Strings.UPDATE_DONE); + updateTask.setMessage(Strings.UPDATE_RESTART_INSTALL); + NotificationUI.createToastFromTemplate(Strings.UPDATE_READY_RESTART_TITLE, + `
${Strings.UPDATE_READY_RESTART_INSTALL_MESSAGE}
`, { + toastStyle: NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.SUCCESS, + dismissOnClick: true + }); + Phoenix.app.registerQuitTimeAppUpdateHandler(quitTimeAppUpdateHandler); + } + + async function _updateWithConfirmDialog(isUpgradableLoc, updateDetails) { + const buttons = _getButtons(isUpgradableLoc); + let markdownHtml = marked.parse(updateDetails.releaseNotesMarkdown || ""); + Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'dialog', "shown"+Phoenix.platform); + Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_INFO, Strings.UPDATE_AVAILABLE_TITLE, markdownHtml, buttons) + .done(option=>{ + if(option === Dialogs.DIALOG_BTN_CANCEL){ + Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'dialog', "cancel"+Phoenix.platform); + return; + } + if(!isUpgradableLoc) { + const downloadPage = brackets.config.homepage_url || "https://phcode.io"; + NativeApp.openURLInDefaultBrowser(downloadPage); + Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'dialog', "nonUpgradable"+Phoenix.platform); + return; + } + if(option === Dialogs.DIALOG_BTN_OK && !updateScheduled){ + Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'dialog', "okUpdate"+Phoenix.platform); + scheduleUpdate(updateDetails); + } + }); + } + + async function checkForUpdates(isAutoUpdate) { + isAutoUpdateFlow = isAutoUpdate; + showOrHideUpdateIcon(); + if(!navigator.onLine) { + return; + } + if(updateTask){ + $("#status-tasks .btn-dropdown").click(); + return; + } + const updateDetails = await getUpdateDetails(); + if(updateFailed) { + if(!isAutoUpdate) { + Dialogs.showInfoDialog(Strings.UPDATE_FAILED_TITLE, Strings.UPDATE_FAILED_MESSAGE); + } + return; + } + if(updatePendingRestart || updateDetails.updatePendingRestart){ + if(!isAutoUpdate){ + Dialogs.showInfoDialog(Strings.UPDATE_READY_RESTART_TITLE, + Strings.UPDATE_READY_RESTART_INSTALL_MESSAGE); + } + return; + } + if(!updateDetails.shouldUpdate){ + (!isAutoUpdate) && Dialogs.showInfoDialog(Strings.UPDATE_NOT_AVAILABLE_TITLE, Strings.UPDATE_UP_TO_DATE); + return; + } + const autoUpdateEnabled = PreferencesManager.get(PREFS_AUTO_UPDATE); + if(isAutoUpdate && !autoUpdateEnabled){ + return; + } + const isUpgradableLoc = await isUpgradableLocation(); + if(!isUpgradableLoc || !isAutoUpdate) { + _updateWithConfirmDialog(isUpgradableLoc, updateDetails); + } else if(!updateScheduled) { + Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'auto', "silent"+Phoenix.platform); + PreferencesManager.setViewState(KEY_LAST_UPDATE_DESCRIPTION, { + releaseNotesMarkdown: updateDetails.releaseNotesMarkdown, + updateVersion: updateDetails.updateVersion + }); + scheduleUpdate(updateDetails); + } + } + + async function launchLinuxUpdater() { + const stageValue = Phoenix.config.environment; + console.log('Stage:', stageValue); + let execCommand = 'wget -qO- https://updates.phcode.io/linux/installer.sh | bash -s -- --upgrade'; + if(stageValue === 'dev' || stageValue === 'stage'){ + execCommand = "wget -qO- https://updates.phcode.io/linux/installer-latest-experimental-build.sh" + + " | bash -s -- --upgrade"; + } + const result = await window.electronAPI.runShellCommand(execCommand); + if(result.code !== 0){ + throw new Error("Update script exit with non-0 exit code: " + result.code); + } + } + + async function quitTimeAppUpdateHandler() { + if(!updateScheduled){ + return; + } + console.log("Installing update at quit time"); + return new Promise(resolve=>{ + let dialog; + function failUpdateDialogAndExit(err) { + console.error("error updating: ", err); + dialog && dialog.close(); + Dialogs.showInfoDialog(Strings.UPDATE_FAILED_TITLE, Strings.UPDATE_FAILED_VISIT_SITE_MESSAGE) + .done(()=>{ + NativeApp.openURLInDefaultBrowser(Phoenix.config.update_download_page) + .catch(console.error) + .finally(resolve); + }); + } + dialog = Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_INFO, + Strings.UPDATE_INSTALLING, + Strings.UPDATE_INSTALLING_MESSAGE, + [ + { + className: "forced-hidden", + id: Dialogs.DIALOG_BTN_OK, + text: Strings.OK + } + ], + false + ); + launchLinuxUpdater() + .then(resolve) + .catch(failUpdateDialogAndExit); + }); + } + + AppInit.appReady(function () { + if(!window.__ELECTRON__ || Phoenix.isTestWindow) { + return; + } + // Electron updates only supported on Linux currently + if (brackets.platform !== "linux") { + console.error("App updates not yet implemented on this platform in Electron builds!"); + return; + } + $("#update-notification").click(()=>{ + checkForUpdates(); + }); + CommandManager.register(Strings.CMD_CHECK_FOR_UPDATE, Commands.HELP_CHECK_UPDATES, ()=>{ + checkForUpdates(); + }); + CommandManager.register(Strings.CMD_AUTO_UPDATE, Commands.HELP_AUTO_UPDATE, ()=>{ + PreferencesManager.set(PREFS_AUTO_UPDATE, !PreferencesManager.get(PREFS_AUTO_UPDATE)); + }); + const helpMenu = Menus.getMenu(Menus.AppMenuBar.HELP_MENU); + helpMenu.addMenuItem(Commands.HELP_CHECK_UPDATES, "", Menus.AFTER, Commands.HELP_GET_INVOLVED); + PreferencesManager.definePreference(PREFS_AUTO_UPDATE, "boolean", true, { + description: Strings.DESCRIPTION_AUTO_UPDATE + }); + showOrHideUpdateIcon(); + const lastUpdateDetails = PreferencesManager.getViewState(KEY_LAST_UPDATE_DESCRIPTION); + if(lastUpdateDetails && (lastUpdateDetails.updateVersion === Phoenix.metadata.apiVersion)) { + let markdownHtml = marked.parse(lastUpdateDetails.releaseNotesMarkdown || ""); + Dialogs.showInfoDialog(Strings.UPDATE_WHATS_NEW, markdownHtml); + PreferencesManager.setViewState(KEY_LAST_UPDATE_DESCRIPTION, null); + PreferencesManager.setViewState(KEY_UPDATE_AVAILABLE, false); + $("#update-notification").addClass("forced-hidden"); + } + // check for updates at boot + let lastUpdateCheckTime = PreferencesManager.getViewState(KEY_LAST_UPDATE_CHECK_TIME); + const currentTime = Date.now(); + const oneDayInMilliseconds = 24 * 60 * 60 * 1000; + if(lastUpdateCheckTime && ((currentTime - lastUpdateCheckTime) < oneDayInMilliseconds)){ + console.log("Skipping update check: last update check was within one day"); + return; + } + PreferencesManager.setViewState(KEY_LAST_UPDATE_CHECK_TIME, currentTime); + checkForUpdates(true); + }); +}); From b7f702641dac1799597aee8eb7f07e63ad7e8110 Mon Sep 17 00:00:00 2001 From: abose Date: Sun, 1 Feb 2026 18:28:40 +0530 Subject: [PATCH 2/3] chore: linux electron app update working --- .../appUpdater/update-electron.js | 177 +++++++++++++++--- 1 file changed, 155 insertions(+), 22 deletions(-) diff --git a/src/extensionsIntegrated/appUpdater/update-electron.js b/src/extensionsIntegrated/appUpdater/update-electron.js index 0bd838e53..268523c8b 100644 --- a/src/extensionsIntegrated/appUpdater/update-electron.js +++ b/src/extensionsIntegrated/appUpdater/update-electron.js @@ -46,6 +46,7 @@ define(function (require, exports, module) { KEY_UPDATE_AVAILABLE = "PH_UPDATE_AVAILABLE"; const PREFS_AUTO_UPDATE = "autoUpdate"; + const MAX_LOG_LINES = 500; let isAutoUpdateFlow = true; let updateScheduled = false; let cachedUpdateDetails = null; @@ -146,7 +147,6 @@ define(function (require, exports, module) { */ async function isUpgradableLocation() { try { - return true; //todo remove const isPackaged = await window.electronAPI.isPackaged(); if (!isPackaged) { return false; @@ -259,18 +259,54 @@ define(function (require, exports, module) { } } - async function launchLinuxUpdater() { - const stageValue = Phoenix.config.environment; - console.log('Stage:', stageValue); - let execCommand = 'wget -qO- https://updates.phcode.io/linux/installer.sh | bash -s -- --upgrade'; - if(stageValue === 'dev' || stageValue === 'stage'){ - execCommand = "wget -qO- https://updates.phcode.io/linux/installer-latest-experimental-build.sh" + - " | bash -s -- --upgrade"; - } - const result = await window.electronAPI.runShellCommand(execCommand); - if(result.code !== 0){ - throw new Error("Update script exit with non-0 exit code: " + result.code); - } + /** + * Launches the Linux updater using spawnProcess with streaming output + * @param {function} onOutput - Callback for stdout/stderr lines + * @returns {Promise} Resolves when update completes, rejects on error + */ + function launchLinuxUpdater(onOutput) { + return new Promise((resolve, reject) => { + const stageValue = Phoenix.config.environment; + console.log('Stage:', stageValue); + let scriptUrl = 'https://updates.phcode.io/linux/installer.sh'; + if(stageValue === 'dev' || stageValue === 'stage'){ + scriptUrl = "https://updates.phcode.io/linux/installer-latest-experimental-build.sh"; + } + + // Use spawnProcess to run bash with the wget|bash command + const command = '/bin/bash'; + const args = ['-c', `wget -qO- ${scriptUrl} | bash -s -- --upgrade`]; + + window.electronAppAPI.spawnProcess(command, args) + .then(instanceId => { + // Set up output handlers + window.electronAppAPI.onProcessStdout((id, line) => { + if (id === instanceId && onOutput) { + onOutput('stdout', line); + } + }); + window.electronAppAPI.onProcessStderr((id, line) => { + if (id === instanceId && onOutput) { + onOutput('stderr', line); + } + }); + window.electronAppAPI.onProcessClose((id, data) => { + if (id === instanceId) { + if (data.code === 0) { + resolve(); + } else { + reject(new Error(`Update script exited with code: ${data.code}`)); + } + } + }); + window.electronAppAPI.onProcessError((id, err) => { + if (id === instanceId) { + reject(new Error(`Update process error: ${err}`)); + } + }); + }) + .catch(reject); + }); } async function quitTimeAppUpdateHandler() { @@ -278,22 +314,116 @@ define(function (require, exports, module) { return; } console.log("Installing update at quit time"); - return new Promise(resolve=>{ + return new Promise(resolve => { let dialog; + let logLines = []; + + function appendLogLine(text) { + // Split text into lines and add each + const lines = text.split('\n').filter(l => l.trim()); + for (const line of lines) { + logLines.push(line); + // Keep only last MAX_LOG_LINES + if (logLines.length > MAX_LOG_LINES) { + logLines.shift(); + } + } + // Update the log display + const logElement = document.getElementById('update-log-output'); + if (logElement) { + logElement.textContent = logLines.join('\n'); + logElement.scrollTop = logElement.scrollHeight; + } + } + function failUpdateDialogAndExit(err) { console.error("error updating: ", err); dialog && dialog.close(); - Dialogs.showInfoDialog(Strings.UPDATE_FAILED_TITLE, Strings.UPDATE_FAILED_VISIT_SITE_MESSAGE) - .done(()=>{ - NativeApp.openURLInDefaultBrowser(Phoenix.config.update_download_page) - .catch(console.error) - .finally(resolve); - }); + // Build full log text for copying + const fullLogText = logLines.join('\n') + '\n\nError: ' + (err.message || err); + // Show failure dialog with log output and hover copy icon + const failContent = ` +

${Strings.UPDATE_FAILED_VISIT_SITE_MESSAGE}

+
+
${fullLogText}
+ +
+ `; + const failDialog = Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.UPDATE_FAILED_TITLE, + failContent, + [{ className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK, text: Strings.OK }] + ); + // Set up hover and click handlers for copy icon + const $container = $('#update-fail-log-container'); + const $copyBtn = $('#update-log-copy-btn'); + $container.on('mouseenter', () => $copyBtn.css('opacity', '1')); + $container.on('mouseleave', () => $copyBtn.css('opacity', '0')); + $copyBtn.on('click', () => { + Phoenix.app.copyToClipboard(fullLogText); + $copyBtn.removeClass('fa-copy').addClass('fa-check'); + setTimeout(() => { + $copyBtn.removeClass('fa-check').addClass('fa-copy'); + }, 1500); + }); + $copyBtn.on('mouseenter', () => $copyBtn.css({ 'background': '#333', 'color': '#fff' })); + $copyBtn.on('mouseleave', () => $copyBtn.css({ 'background': 'transparent', 'color': '#888' })); + + failDialog.done(() => { + NativeApp.openURLInDefaultBrowser(Phoenix.config.update_download_page) + .catch(console.error) + .finally(resolve); + }); } + + // Create dialog with terminal-style log output + const dialogContent = ` +

${Strings.UPDATE_INSTALLING_MESSAGE}

+

+            `;
+
             dialog = Dialogs.showModalDialog(
                 DefaultDialogs.DIALOG_ID_INFO,
                 Strings.UPDATE_INSTALLING,
-                Strings.UPDATE_INSTALLING_MESSAGE,
+                dialogContent,
                 [
                     {
                         className: "forced-hidden",
@@ -303,7 +433,10 @@ define(function (require, exports, module) {
                 ],
                 false
             );
-            launchLinuxUpdater()
+
+            launchLinuxUpdater((type, text) => {
+                appendLogLine(text);
+            })
                 .then(resolve)
                 .catch(failUpdateDialogAndExit);
         });

From c588bda866e2e9ff70b003d62d82120755be63a9 Mon Sep 17 00:00:00 2001
From: abose 
Date: Sun, 1 Feb 2026 18:53:43 +0530
Subject: [PATCH 3/3] chore: auto update working in linux

---
 src/extensionsIntegrated/appUpdater/main.js   |  9 +++++-
 .../appUpdater/update-electron.js             | 29 ++++++++++++++++++-
 2 files changed, 36 insertions(+), 2 deletions(-)

diff --git a/src/extensionsIntegrated/appUpdater/main.js b/src/extensionsIntegrated/appUpdater/main.js
index 8bcd636ac..72bcfdb26 100644
--- a/src/extensionsIntegrated/appUpdater/main.js
+++ b/src/extensionsIntegrated/appUpdater/main.js
@@ -518,7 +518,14 @@ define(function (require, exports, module) {
 
     let updateInstalledDialogShown = false, updateFailedDialogShown = false;
     AppInit.appReady(function () {
-        if(!window.__TAURI__ || Phoenix.isTestWindow) {
+        if(Phoenix.isTestWindow) {
+            return;
+        }
+        if(window.__ELECTRON__) {
+            // Electron updates handled by update-electron.js
+            return;
+        }
+        if(!window.__TAURI__) {
             // app updates are only for desktop builds
             return;
         }
diff --git a/src/extensionsIntegrated/appUpdater/update-electron.js b/src/extensionsIntegrated/appUpdater/update-electron.js
index 268523c8b..db351b040 100644
--- a/src/extensionsIntegrated/appUpdater/update-electron.js
+++ b/src/extensionsIntegrated/appUpdater/update-electron.js
@@ -178,6 +178,8 @@ define(function (require, exports, module) {
         updateScheduled = true;
         updatePendingRestart = true;
         cachedUpdateDetails = updateDetails;
+        // Store in shared state so other windows know update is scheduled
+        await window.electronAPI.setUpdateScheduled(true);
         showOrHideUpdateIcon();
         Metrics.countEvent(Metrics.EVENT_TYPE.UPDATES, 'scheduled', Phoenix.platform);
         updateTask.setSucceded();
@@ -313,6 +315,8 @@ define(function (require, exports, module) {
         if(!updateScheduled){
             return;
         }
+        // Clear the scheduled flag in shared state
+        await window.electronAPI.setUpdateScheduled(false);
         console.log("Installing update at quit time");
         return new Promise(resolve => {
             let dialog;
@@ -442,7 +446,7 @@ define(function (require, exports, module) {
         });
     }
 
-    AppInit.appReady(function () {
+    AppInit.appReady(async function () {
         if(!window.__ELECTRON__ || Phoenix.isTestWindow) {
             return;
         }
@@ -451,6 +455,29 @@ define(function (require, exports, module) {
             console.error("App updates not yet implemented on this platform in Electron builds!");
             return;
         }
+        // Check if another window already scheduled an update (multi-window state persistence)
+        // This ensures the quit handler is registered in this window too
+        try {
+            const isUpdateScheduled = await window.electronAPI.getUpdateScheduled();
+            if (isUpdateScheduled) {
+                updateScheduled = true;
+                updatePendingRestart = true;
+                // Create task in success state (update ready, waiting for restart)
+                updateTask = TaskManager.addNewTask(Strings.UPDATE_DONE, Strings.UPDATE_RESTART_INSTALL,
+                    ``, {
+                        noSpinnerNotification: true,
+                        onSelect: function () {
+                            Dialogs.showInfoDialog(Strings.UPDATE_READY_RESTART_TITLE,
+                                Strings.UPDATE_READY_RESTART_INSTALL_MESSAGE);
+                        }
+                    });
+                updateTask.setSucceded();
+                Phoenix.app.registerQuitTimeAppUpdateHandler(quitTimeAppUpdateHandler);
+                console.log("Update was scheduled in another window, registering quit handler");
+            }
+        } catch (e) {
+            console.error("Error checking shared state for update state:", e);
+        }
         $("#update-notification").click(()=>{
             checkForUpdates();
         });