Skip to content

Commit 80e443b

Browse files
committed
feat: add manual update fallback and error handling for code-signed installation failures
1 parent f1d362c commit 80e443b

4 files changed

Lines changed: 47 additions & 13 deletions

File tree

electron/main.cjs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ autoUpdater.on('update-downloaded', (info) => {
2828
}
2929
});
3030

31+
// Forward updater errors to renderer (e.g. code signature validation failures)
32+
autoUpdater.on('error', (err) => {
33+
console.error('[Electron] Auto-updater error:', err.message);
34+
if (mainWindow && !mainWindow.isDestroyed()) {
35+
mainWindow.webContents.send('updater:error', err.message);
36+
}
37+
});
38+
3139
const DIST = path.join(__dirname, '../dist');
3240
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'];
3341

@@ -676,17 +684,29 @@ ipcMain.handle('agent:restart', async () => {
676684
});
677685

678686
// Auto-updater: quit and install the downloaded update
679-
ipcMain.handle('updater:install', () => {
680-
console.log('[Electron] updater:install called — quitting and installing update');
687+
// Use ipcMain.on (fire-and-forget) instead of .handle (promise-based) —
688+
// quitAndInstall kills the process, which breaks the promise chain in .handle
689+
ipcMain.on('updater:install', () => {
690+
console.log('[Electron] updater:install — attempting quit and install');
681691
stopAgentServer();
682-
// autoUpdater.quitAndInstall is unreliable on macOS — do it manually
683-
autoUpdater.autoInstallOnAppQuit = true;
684-
app.relaunch();
685-
app.exit(0);
692+
autoUpdater.quitAndInstall(false, true);
693+
// If still alive after 3s, quitAndInstall failed silently (e.g. unsigned local build)
694+
setTimeout(() => {
695+
console.warn('[Electron] quitAndInstall did not exit — sending error to renderer');
696+
if (mainWindow && !mainWindow.isDestroyed()) {
697+
mainWindow.webContents.send('updater:error',
698+
'Update install failed — the app may not be code-signed');
699+
}
700+
}, 3000);
686701
});
687702

688703
// Let renderer check if an update was already downloaded (survives page refresh)
689704
ipcMain.handle('updater:check-pending', () => {
690705
return pendingUpdateInfo;
691706
});
692707

708+
// Fallback: open GitHub releases page for manual download
709+
ipcMain.handle('updater:open-releases', () => {
710+
shell.openExternal('https://github.com/theredstring/redstring/releases/latest');
711+
});
712+

electron/preload.cjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@ contextBridge.exposeInMainWorld('electron', {
5656
onUpdateReady: (callback) => {
5757
ipcRenderer.on('updater:update-ready', (_event, info) => callback(info));
5858
},
59+
onError: (callback) => {
60+
ipcRenderer.on('updater:error', (_event, msg) => callback(msg));
61+
},
5962
checkPending: () => ipcRenderer.invoke('updater:check-pending'),
60-
installUpdate: () => ipcRenderer.invoke('updater:install'),
63+
installUpdate: () => ipcRenderer.send('updater:install'),
64+
openReleases: () => ipcRenderer.invoke('updater:open-releases'),
6165
}
6266
});
6367

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "redstring",
33
"private": true,
4-
"version": "0.0.1",
4+
"version": "0.3.3",
55
"author": "Grant Eubanks <grant.w.eubanks@gmail.com>",
66
"description": "Redstring - A semantic knowledge graph application with visual node-based interface, RDF integration, and AI-powered knowledge discovery",
77
"license": "MIT",

src/components/UpdateToast.jsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect } from 'react';
2-
import { RotateCcw, X } from 'lucide-react';
2+
import { RotateCcw, ExternalLink, X } from 'lucide-react';
33
import PanelIconButton from './shared/PanelIconButton.jsx';
44
import './UpdateToast.css';
55

@@ -12,6 +12,7 @@ export default function UpdateToast() {
1212
const [dismissed, setDismissed] = useState(false);
1313
const [visible, setVisible] = useState(false);
1414
const [dismissing, setDismissing] = useState(false);
15+
const [installFailed, setInstallFailed] = useState(false);
1516

1617
useEffect(() => {
1718
if (!window.electron?.updater) return;
@@ -22,6 +23,11 @@ export default function UpdateToast() {
2223
requestAnimationFrame(() => setVisible(true));
2324
});
2425

26+
// Listen for updater errors (e.g. code signature validation failure on unsigned builds)
27+
window.electron.updater.onError?.(() => {
28+
setInstallFailed(true);
29+
});
30+
2531
// Check if an update was already downloaded before this mount (e.g. after page refresh)
2632
window.electron.updater.checkPending?.().then((info) => {
2733
if (info) {
@@ -34,7 +40,11 @@ export default function UpdateToast() {
3440
if (!updateInfo || dismissed) return null;
3541

3642
const handleRestart = () => {
37-
window.electron?.updater?.installUpdate();
43+
if (installFailed) {
44+
window.electron?.updater?.openReleases?.();
45+
} else {
46+
window.electron?.updater?.installUpdate();
47+
}
3848
};
3949

4050
const handleDismiss = () => {
@@ -56,15 +66,15 @@ export default function UpdateToast() {
5666
Version {updateInfo.version || 'update'} released
5767
</div>
5868
<div className="update-toast-subtitle">
59-
Restart to update
69+
{installFailed ? 'Download from GitHub' : 'Restart to update'}
6070
</div>
6171
</div>
6272
<div className="update-toast-actions">
6373
<PanelIconButton
64-
icon={RotateCcw}
74+
icon={installFailed ? ExternalLink : RotateCcw}
6575
size={15}
6676
onClick={handleRestart}
67-
title="Restart and install update"
77+
title={installFailed ? 'Download update' : 'Restart and install update'}
6878
variant="outline"
6979
/>
7080
<PanelIconButton

0 commit comments

Comments
 (0)