From 691c6c86288ed1b7252e386215a4cf001cae4401 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Sun, 9 Nov 2025 14:33:55 +1300 Subject: [PATCH] Add Electron upgrade feature with UI and backend fixes Introduces an Electron upgrade section in settings, allowing users to check for and upgrade to the latest stable or nightly Electron versions. Implements backend logic to read the installed Electron version directly from package.json, properly handles switching between stable and nightly builds, and improves error handling and UI feedback during upgrade operations. Includes documentation of the upgrade process and bug fixes related to version display and upgrade reliability. --- ELECTRON_UPGRADE_FIXES.md | 94 +++++++++++++++++++ main.js | 144 +++++++++++++++++++++++++++++ renderer/settings.html | 25 ++++++ renderer/settings.js | 184 +++++++++++++++++++++++++++++++++++++- 4 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 ELECTRON_UPGRADE_FIXES.md diff --git a/ELECTRON_UPGRADE_FIXES.md b/ELECTRON_UPGRADE_FIXES.md new file mode 100644 index 0000000..8831828 --- /dev/null +++ b/ELECTRON_UPGRADE_FIXES.md @@ -0,0 +1,94 @@ +# Electron Upgrade Feature - Bug Fixes + +## Problem Identified +The upgrade feature was downloading and installing new Electron versions successfully, but the app always showed the old version (1.0.0) after restart because: + +1. **Version Source Issue**: The app was reading `app.getVersion()` which gets the version from `package.json` at startup time +2. **Package.json Not Re-read**: Even after npm installed a new Electron version, the app didn't re-read the updated `package.json` +3. **Runtime Display**: The About tab showed the bundled Electron version (37.x) which is baked into the binary at build time + +## Solutions Implemented + +### 1. **New Helper Function: `getInstalledElectronVersion()`** +- Reads `package.json` directly every time it's called (not cached) +- Extracts the actual installed Electron version from `devDependencies` +- Handles both stable (`electron`) and nightly (`electron-nightly`) packages +- Strips version specifiers (^, ~, etc.) to get the clean version number +- Falls back to `app.getVersion()` if reading fails + +```javascript +function getInstalledElectronVersion() { + try { + const packageJsonPath = path.join(__dirname, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + const electronDep = packageJson.devDependencies?.electron; + const electronNightlyDep = packageJson.devDependencies?.['electron-nightly']; + + if (electronDep) { + return electronDep.replace(/^\D+/, ''); + } + if (electronNightlyDep) { + return electronNightlyDep.replace(/^\D+/, ''); + } + return app.getVersion(); + } catch (err) { + return app.getVersion(); + } +} +``` + +### 2. **Updated `get-electron-versions` Handler** +- Now uses `getInstalledElectronVersion()` instead of `app.getVersion()` +- Returns the actual installed version that was modified by npm +- Performs fresh version checks each time (no caching) + +### 3. **Improved `upgrade-electron` Handler** +- Increased `maxBuffer` to handle large npm output +- Added cleanup logic to remove the old Electron variant when switching types + - Removes `electron` when upgrading to `nightly` + - Removes `electron-nightly` when upgrading to `stable` +- Better error logging to debug npm failures +- Returns clearer messages about installation status + +### 4. **Enhanced UI/UX in settings.js** +- Added more descriptive status text ("Downloading and installing..." instead of just "Upgrading...") +- Disables all controls during upgrade to prevent multiple clicks +- Reduced restart delay from 2000ms to 1500ms for faster feedback +- Better error handling with proper cleanup of disabled states + +## How It Works Now + +1. **User clicks "Check for Updates"** + - Queries npm registry for latest version + - Uses `getInstalledElectronVersion()` to read current version from `package.json` + - Compares versions and shows if update is available + +2. **User clicks "Upgrade Electron"** + - Confirms action + - Runs `npm install --save-dev electron@latest` (or `electron-nightly@latest`) + - npm downloads and installs new version + - Handler removes the other Electron variant from `package.json` if needed + - Shows success message + +3. **App Restarts** + - Uses `app.relaunch()` and `app.quit()` + - When app relaunches, it: + - Loads new Electron binary from `node_modules` + - Runs new Electron version + - Settings page shows correct new version on next check + +## Testing Recommendations + +1. Test upgrading from stable to nightly version +2. Test upgrading from nightly back to stable +3. Verify version display updates after restart +4. Check that old variant is removed from `package.json` +5. Verify app runs stably with new Electron version + +## Notes for Future Development + +- The About tab displays `process.versions.electron` which is the bundled Chromium version, not the Electron framework version +- The Electron version we display in the upgrade section comes from `package.json` which is the actual framework version +- When building with electron-builder, the bundled version becomes fixed until next rebuild +- For development/testing, the upgrade feature reads live from `package.json` diff --git a/main.js b/main.js index cd0f3e2..50beb6b 100644 --- a/main.js +++ b/main.js @@ -666,6 +666,150 @@ ipcMain.handle('open-devtools', (event) => { return contents.isDevToolsOpened(); }); +// Helper function to read package.json version +function getInstalledElectronVersion() { + try { + const packageJsonPath = path.join(__dirname, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Get the version from devDependencies + const electronDep = packageJson.devDependencies?.electron; + const electronNightlyDep = packageJson.devDependencies?.['electron-nightly']; + + if (electronDep) { + return electronDep.replace(/^\D+/, ''); // Remove ^ or ~ or other version specifiers + } + if (electronNightlyDep) { + return electronNightlyDep.replace(/^\D+/, ''); + } + + return app.getVersion(); + } catch (err) { + console.error('Error reading installed electron version:', err); + return app.getVersion(); + } +} + +// Electron version management handlers +ipcMain.handle('get-electron-versions', async (event, buildType = 'stable') => { + const https = require('https'); + + return new Promise((resolve) => { + let url; + + if (buildType === 'nightly') { + // Get latest nightly version from npm + url = 'https://registry.npmjs.org/electron-nightly/latest'; + } else { + // Get latest stable version from npm + url = 'https://registry.npmjs.org/electron/latest'; + } + + const request = https.get(url, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const packageInfo = JSON.parse(data); + // Get the actual installed version from package.json, not app.getVersion() + const installedVersion = getInstalledElectronVersion(); + resolve({ + available: packageInfo.version, + current: installedVersion, + buildType: buildType + }); + } catch (err) { + console.error('Failed to parse version info:', err); + resolve({ + available: null, + current: getInstalledElectronVersion(), + error: 'Failed to fetch version info' + }); + } + }); + }); + + request.on('error', (err) => { + console.error('Failed to fetch versions:', err); + resolve({ + available: null, + current: getInstalledElectronVersion(), + error: err.message + }); + }); + + request.setTimeout(5000, () => { + request.destroy(); + resolve({ + available: null, + current: getInstalledElectronVersion(), + error: 'Version check timed out' + }); + }); + }); +}); + +ipcMain.handle('upgrade-electron', async (event, buildType = 'stable') => { + const { execFile } = require('child_process'); + const packageName = buildType === 'nightly' ? 'electron-nightly' : 'electron'; + + return new Promise((resolve) => { + // First, remove the other electron package if switching types + const otherPackage = buildType === 'nightly' ? 'electron' : 'electron-nightly'; + + // Run npm install to upgrade the package + const args = ['install', '--save-dev', packageName + '@latest']; + + execFile('npm', args, + { cwd: __dirname, shell: true, maxBuffer: 10 * 1024 * 1024 }, + (error, stdout, stderr) => { + if (error) { + console.error('Upgrade failed:', error); + console.error('stderr:', stderr); + resolve({ + success: false, + error: error.message, + message: 'Failed to upgrade Electron' + }); + } else { + console.log('Upgrade output:', stdout); + console.log('Upgrade stderr:', stderr); + + // Update package.json to remove the other electron variant if needed + try { + const packageJsonPath = path.join(__dirname, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Remove electron if we're upgrading to nightly + if (buildType === 'nightly' && packageJson.devDependencies?.electron) { + delete packageJson.devDependencies.electron; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); + } + // Remove electron-nightly if we're upgrading to stable + else if (buildType === 'stable' && packageJson.devDependencies?.['electron-nightly']) { + delete packageJson.devDependencies['electron-nightly']; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); + } + } catch (err) { + console.warn('Could not clean up alternate electron package:', err); + } + + resolve({ + success: true, + message: 'Electron upgrade completed. Restarting application...' + }); + } + } + ); + }); +}); + +ipcMain.handle('restart-app', async (event) => { + // Quit and relaunch the app + app.relaunch(); + app.quit(); +}); + // Open local file dialog -> returns file:// URL (or null if cancelled) ipcMain.handle('show-open-file-dialog', async () => { try { diff --git a/renderer/settings.html b/renderer/settings.html index 4b2249f..8b59f73 100644 --- a/renderer/settings.html +++ b/renderer/settings.html @@ -245,6 +245,31 @@ +
+

Electron Upgrade

+
+
+ + +
+
+ Loading available versions... + +
+
+ + + +
+

Upgrading Electron will require the application to restart.

+
+
+
diff --git a/renderer/settings.js b/renderer/settings.js index d14e02d..07e9444 100644 --- a/renderer/settings.js +++ b/renderer/settings.js @@ -262,7 +262,8 @@ async function populateAbout() { byId('about-mem').textContent = `${info.totalMemGB} GB`; const copyBtn = document.getElementById('copy-about-btn'); - if (copyBtn) { + if (copyBtn && !copyBtn.dataset.listenerAttached) { + copyBtn.dataset.listenerAttached = 'true'; copyBtn.addEventListener('click', async () => { const payload = [ `Nebula ${info.appVersion} (${info.isPackaged ? 'packaged' : 'dev'})`, @@ -289,8 +290,189 @@ async function populateAbout() { // Populate about info after DOM is ready window.addEventListener('DOMContentLoaded', () => { populateAbout(); + setupElectronUpgrade(); + + // Refresh about info when About tab is clicked + const aboutTabBtn = document.getElementById('tab-about'); + if (aboutTabBtn) { + aboutTabBtn.addEventListener('click', () => { + // Refresh after a short delay to allow tab transition + setTimeout(() => { + populateAbout(); + }, 100); + }); + } }); +// Electron upgrade feature setup +async function setupElectronUpgrade() { + const versionSelect = document.getElementById('electron-version-select'); + const checkBtn = document.getElementById('check-electron-versions'); + const upgradeBtn = document.getElementById('upgrade-electron-btn'); + const loadingSpan = document.getElementById('electron-loading'); + const versionContainer = document.getElementById('electron-versions-container'); + const versionInfo = document.getElementById('electron-version-info'); + const versionText = document.getElementById('electron-version-text'); + const statusText = document.getElementById('electron-status-text'); + + if (!checkBtn || !versionSelect) return; + + let availableVersion = null; + let currentVersion = null; + + const checkVersions = async () => { + try { + if (!ipc) { + showStatus('IPC not available'); + return; + } + + checkBtn.disabled = true; + loadingSpan.style.display = 'block'; + versionInfo.style.display = 'none'; + statusText.style.display = 'none'; + statusText.textContent = ''; + + const buildType = versionSelect.value; + const result = await ipc.invoke('get-electron-versions', buildType); + + if (result.error) { + statusText.textContent = `Error: ${result.error}`; + statusText.style.display = 'block'; + showStatus(`Failed to check versions: ${result.error}`); + } else { + availableVersion = result.available; + currentVersion = result.current; + + if (availableVersion) { + versionText.textContent = `Available: ${availableVersion} | Current: ${currentVersion}`; + versionInfo.style.display = 'block'; + + // Enable upgrade button only if there's a newer version + const isNewer = compareVersions(availableVersion, currentVersion) > 0; + upgradeBtn.disabled = !isNewer; + upgradeBtn.style.display = 'block'; + + if (isNewer) { + statusText.textContent = 'Update available!'; + statusText.style.color = '#4CAF50'; + } else { + statusText.textContent = 'You are running the latest version.'; + statusText.style.color = '#888'; + } + statusText.style.display = 'block'; + showStatus(`Current: ${currentVersion} | Latest ${buildType}: ${availableVersion}`); + } + } + + checkBtn.disabled = false; + loadingSpan.style.display = 'none'; + } catch (error) { + console.error('Error checking versions:', error); + statusText.textContent = `Error: ${error.message}`; + statusText.style.display = 'block'; + showStatus('Failed to check versions'); + checkBtn.disabled = false; + loadingSpan.style.display = 'none'; + } + }; + + const handleUpgrade = async () => { + const buildType = versionSelect.value; + if (!availableVersion) { + showStatus('No version information available'); + return; + } + + const confirmed = confirm( + `Upgrade Electron from ${currentVersion} to ${availableVersion} (${buildType})?\n\nThe application will restart automatically.` + ); + + if (!confirmed) return; + + try { + upgradeBtn.disabled = true; + checkBtn.disabled = true; + versionSelect.disabled = true; + statusText.textContent = 'Downloading and installing...'; + statusText.style.color = '#FFC107'; + statusText.style.display = 'block'; + showStatus('Starting Electron upgrade...'); + + const result = await ipc.invoke('upgrade-electron', buildType); + + if (result.success) { + statusText.textContent = result.message; + statusText.style.color = '#4CAF50'; + showStatus(result.message); + + // Restart the app after a short delay + setTimeout(() => { + if (ipc) { + ipc.invoke('restart-app').catch(err => console.error('Restart failed:', err)); + } + }, 1500); + } else { + statusText.textContent = `Failed: ${result.error}`; + statusText.style.color = '#F44336'; + showStatus(`Upgrade failed: ${result.error}`); + upgradeBtn.disabled = false; + checkBtn.disabled = false; + versionSelect.disabled = false; + } + } catch (error) { + console.error('Upgrade error:', error); + statusText.textContent = `Error: ${error.message}`; + statusText.style.color = '#F44336'; + statusText.style.display = 'block'; + showStatus(`Upgrade error: ${error.message}`); + upgradeBtn.disabled = false; + checkBtn.disabled = false; + versionSelect.disabled = false; + } + }; + + checkBtn.addEventListener('click', checkVersions); + upgradeBtn.addEventListener('click', handleUpgrade); + + versionSelect.addEventListener('change', () => { + // Reset UI when build type changes + versionInfo.style.display = 'none'; + upgradeBtn.style.display = 'none'; + upgradeBtn.disabled = true; + statusText.style.display = 'none'; + loadingSpan.style.display = 'block'; + availableVersion = null; + }); + + // Auto-refresh about tab and electron versions when this section comes into view + const aboutTabBtn = document.getElementById('tab-about'); + if (aboutTabBtn) { + aboutTabBtn.addEventListener('click', () => { + setTimeout(() => { + // Refresh about info when About tab is clicked + populateAbout(); + // Also refresh electron versions display + checkVersions(); + }, 100); + }); + } +} + +// Helper function to compare semantic versions +function compareVersions(v1, v2) { + const parts1 = v1.split('-')[0].split('.').map(x => parseInt(x, 10)); + const parts2 = v2.split('-')[0].split('.').map(x => parseInt(x, 10)); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const p1 = parts1[i] || 0; + const p2 = parts2[i] || 0; + if (p1 > p2) return 1; + if (p1 < p2) return -1; + } + return 0; +} + // Keep settings open when clicking GitHub by asking host to open externally/new tab window.addEventListener('DOMContentLoaded', () => { const gh = document.getElementById('github-link');