feat(update): implement auto-update functionality with electron-updater

- Add AppUpdater module with update lifecycle management
- Create UpdateSettings UI component with progress display
- Add Progress UI component based on Radix UI
- Create update Zustand store for state management
- Register update IPC handlers in main process
- Auto-check for updates on production startup
- Add commit documentation for commits 2-6
This commit is contained in:
Haze
2026-02-05 23:36:12 +08:00
Unverified
parent 98a2d9bc83
commit e02cf05baf
13 changed files with 1313 additions and 50 deletions

View File

@@ -9,6 +9,7 @@ import { registerIpcHandlers } from './ipc-handlers';
import { createTray } from './tray';
import { createMenu } from './menu';
import { PORTS } from '../utils/config';
import { appUpdater, registerUpdateHandlers } from './updater';
// Disable GPU acceleration for better compatibility
app.disableHardwareAcceleration();
@@ -76,6 +77,18 @@ async function initialize(): Promise<void> {
// Register IPC handlers
registerIpcHandlers(gatewayManager, mainWindow);
// Register update handlers
registerUpdateHandlers(appUpdater, mainWindow);
// Check for updates after a delay (only in production)
if (!process.env.VITE_DEV_SERVER_URL) {
setTimeout(() => {
appUpdater.checkForUpdates().catch((err) => {
console.error('Failed to check for updates:', err);
});
}, 10000); // Check after 10 seconds
}
// Handle window close
mainWindow.on('closed', () => {
mainWindow = null;

264
electron/main/updater.ts Normal file
View File

@@ -0,0 +1,264 @@
/**
* Auto-Updater Module
* Handles automatic application updates using electron-updater
*/
import { autoUpdater, UpdateInfo, ProgressInfo, UpdateDownloadedEvent } from 'electron-updater';
import { BrowserWindow, app, ipcMain } from 'electron';
import { EventEmitter } from 'events';
export interface UpdateStatus {
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
info?: UpdateInfo;
progress?: ProgressInfo;
error?: string;
}
export interface UpdaterEvents {
'status-changed': (status: UpdateStatus) => void;
'checking-for-update': () => void;
'update-available': (info: UpdateInfo) => void;
'update-not-available': (info: UpdateInfo) => void;
'download-progress': (progress: ProgressInfo) => void;
'update-downloaded': (event: UpdateDownloadedEvent) => void;
'error': (error: Error) => void;
}
export class AppUpdater extends EventEmitter {
private mainWindow: BrowserWindow | null = null;
private status: UpdateStatus = { status: 'idle' };
constructor() {
super();
// Configure auto-updater
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
// Use logger
autoUpdater.logger = {
info: (msg: string) => console.log('[Updater]', msg),
warn: (msg: string) => console.warn('[Updater]', msg),
error: (msg: string) => console.error('[Updater]', msg),
debug: (msg: string) => console.debug('[Updater]', msg),
};
this.setupListeners();
}
/**
* Set the main window for sending update events
*/
setMainWindow(window: BrowserWindow): void {
this.mainWindow = window;
}
/**
* Get current update status
*/
getStatus(): UpdateStatus {
return this.status;
}
/**
* Setup auto-updater event listeners
*/
private setupListeners(): void {
autoUpdater.on('checking-for-update', () => {
this.updateStatus({ status: 'checking' });
this.emit('checking-for-update');
});
autoUpdater.on('update-available', (info: UpdateInfo) => {
this.updateStatus({ status: 'available', info });
this.emit('update-available', info);
});
autoUpdater.on('update-not-available', (info: UpdateInfo) => {
this.updateStatus({ status: 'not-available', info });
this.emit('update-not-available', info);
});
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
this.updateStatus({ status: 'downloading', progress });
this.emit('download-progress', progress);
});
autoUpdater.on('update-downloaded', (event: UpdateDownloadedEvent) => {
this.updateStatus({ status: 'downloaded', info: event });
this.emit('update-downloaded', event);
});
autoUpdater.on('error', (error: Error) => {
this.updateStatus({ status: 'error', error: error.message });
this.emit('error', error);
});
}
/**
* Update status and notify renderer
*/
private updateStatus(newStatus: Partial<UpdateStatus>): void {
this.status = { ...this.status, ...newStatus };
this.sendToRenderer('update:status-changed', this.status);
}
/**
* Send event to renderer process
*/
private sendToRenderer(channel: string, data: unknown): void {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send(channel, data);
}
}
/**
* Check for updates
*/
async checkForUpdates(): Promise<UpdateInfo | null> {
try {
const result = await autoUpdater.checkForUpdates();
return result?.updateInfo || null;
} catch (error) {
console.error('[Updater] Check for updates failed:', error);
return null;
}
}
/**
* Download available update
*/
async downloadUpdate(): Promise<void> {
try {
await autoUpdater.downloadUpdate();
} catch (error) {
console.error('[Updater] Download update failed:', error);
throw error;
}
}
/**
* Install update and restart app
*/
quitAndInstall(): void {
autoUpdater.quitAndInstall();
}
/**
* Set update channel (stable, beta, dev)
*/
setChannel(channel: 'stable' | 'beta' | 'dev'): void {
autoUpdater.channel = channel;
}
/**
* Set auto-download preference
*/
setAutoDownload(enable: boolean): void {
autoUpdater.autoDownload = enable;
}
/**
* Get current version
*/
getCurrentVersion(): string {
return app.getVersion();
}
}
/**
* Register IPC handlers for update operations
*/
export function registerUpdateHandlers(
updater: AppUpdater,
mainWindow: BrowserWindow
): void {
updater.setMainWindow(mainWindow);
// Get current update status
ipcMain.handle('update:status', () => {
return updater.getStatus();
});
// Get current version
ipcMain.handle('update:version', () => {
return updater.getCurrentVersion();
});
// Check for updates
ipcMain.handle('update:check', async () => {
try {
const info = await updater.checkForUpdates();
return { success: true, info };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Download update
ipcMain.handle('update:download', async () => {
try {
await updater.downloadUpdate();
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
});
// Install update and restart
ipcMain.handle('update:install', () => {
updater.quitAndInstall();
return { success: true };
});
// Set update channel
ipcMain.handle('update:setChannel', (_, channel: 'stable' | 'beta' | 'dev') => {
updater.setChannel(channel);
return { success: true };
});
// Set auto-download preference
ipcMain.handle('update:setAutoDownload', (_, enable: boolean) => {
updater.setAutoDownload(enable);
return { success: true };
});
// Forward update events to renderer
updater.on('checking-for-update', () => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('update:checking');
}
});
updater.on('update-available', (info) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('update:available', info);
}
});
updater.on('update-not-available', (info) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('update:not-available', info);
}
});
updater.on('download-progress', (progress) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('update:progress', progress);
}
});
updater.on('update-downloaded', (event) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('update:downloaded', event);
}
});
updater.on('error', (error) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send('update:error', error.message);
}
});
}
// Export singleton instance
export const appUpdater = new AppUpdater();