From 265a3e075a06800177e3473c69ea67d86d8c05cc Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Tue, 26 Nov 2024 23:23:23 -0300 Subject: [PATCH] electron: Offer automatic app update With this new implementation, the user is informed that a new version of the application is available and can choose between downloading it or not. --- electron/main.ts | 88 ++++++++++++-- electron/preload.ts | 16 +++ src/App.vue | 3 + src/components/InteractionDialog.vue | 2 +- src/components/UpdateNotification.vue | 167 ++++++++++++++++++++++++++ src/libs/cosmos.ts | 120 +++++++++++++++++- 6 files changed, 382 insertions(+), 14 deletions(-) create mode 100644 src/components/UpdateNotification.vue diff --git a/electron/main.ts b/electron/main.ts index b560ea0f6..9e5b813ce 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,6 @@ -import { app, BrowserWindow, protocol, screen } from 'electron' +import { app, BrowserWindow, ipcMain, protocol, screen } from 'electron' +// @ts-ignore: electron-updater is not a module +import electronUpdater, { type AppUpdater } from 'electron-updater' import { join } from 'path' export const ROOT_PATH = { @@ -7,10 +9,25 @@ export const ROOT_PATH = { let mainWindow: BrowserWindow | null +/** + * Get auto updater instance + * @returns {AppUpdater} + * @see https://www.electron.build/auto-update + */ +function getAutoUpdater(): AppUpdater { + // Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'. + // It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976. + const { autoUpdater } = electronUpdater + autoUpdater.logger = require('electron-log') + // @ts-ignore + autoUpdater.logger.transports.file.level = 'info' + return autoUpdater +} + /** * Create electron window */ -function createWindow(): void { +async function createWindow(): Promise { const { width, height } = screen.getPrimaryDisplay().workAreaSize mainWindow = new BrowserWindow({ icon: join(ROOT_PATH.dist, 'pwa-512x512.png'), @@ -23,15 +40,10 @@ function createWindow(): void { height, }) - // Test active push message to Renderer-process. - mainWindow.webContents.on('did-finish-load', () => { - mainWindow?.webContents.send('main-process-message', new Date().toLocaleString()) - }) - if (process.env.VITE_DEV_SERVER_URL) { - mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL) + mainWindow!.loadURL(process.env.VITE_DEV_SERVER_URL) } else { - mainWindow.loadFile(join(ROOT_PATH.dist, 'index.html')) + mainWindow!.loadFile(join(ROOT_PATH.dist, 'index.html')) } } @@ -59,7 +71,63 @@ protocol.registerSchemesAsPrivileged([ }, ]) -app.whenReady().then(createWindow) +app.whenReady().then(async () => { + console.log('Electron app is ready.') + console.log(`Cockpit version: ${app.getVersion()}`) + + console.log('Creating window...') + await createWindow() + + console.log('Setting up auto updater...') + setTimeout(() => { + setupAutoUpdater() + }, 5000) +}) + +const setupAutoUpdater = (): void => { + const autoUpdater = getAutoUpdater() + autoUpdater.autoDownload = false // Prevent automatic downloads + + autoUpdater + .checkForUpdates() + .then((e) => console.log(e)) + .catch((e) => console.log(e)) + + autoUpdater.on('checking-for-update', () => { + mainWindow!.webContents.send('checking-for-update') + }) + + autoUpdater.on('update-available', (info) => { + mainWindow!.webContents.send('update-available', info) + }) + + autoUpdater.on('update-not-available', (info) => { + mainWindow!.webContents.send('update-not-available', info) + }) + + autoUpdater.on('download-progress', (progressInfo) => { + mainWindow!.webContents.send('download-progress', progressInfo) + }) + + autoUpdater.on('update-downloaded', (info) => { + mainWindow!.webContents.send('update-downloaded', info) + }) + + // Add handlers for update control + ipcMain.on('download-update', () => { + autoUpdater.downloadUpdate() + }) + + ipcMain.on('install-update', () => { + autoUpdater.quitAndInstall() + }) + + ipcMain.on('cancel-update', () => { + // Cancel any ongoing download + autoUpdater.removeAllListeners('update-downloaded') + autoUpdater.removeAllListeners('download-progress') + }) +} app.on('before-quit', () => { // @ts-ignore: import.meta.env does not exist in the types diff --git a/electron/preload.ts b/electron/preload.ts index e69de29bb..d39d16665 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -0,0 +1,16 @@ +import { contextBridge, ipcRenderer } from 'electron' + +contextBridge.exposeInMainWorld('electronAPI', { + onUpdateAvailable: (callback: (info: any) => void) => + ipcRenderer.on('update-available', (_event, info) => callback(info)), + onUpdateDownloaded: (callback: (info: any) => void) => + ipcRenderer.on('update-downloaded', (_event, info) => callback(info)), + onCheckingForUpdate: (callback: () => void) => ipcRenderer.on('checking-for-update', () => callback()), + onUpdateNotAvailable: (callback: (info: any) => void) => + ipcRenderer.on('update-not-available', (_event, info) => callback(info)), + onDownloadProgress: (callback: (info: any) => void) => + ipcRenderer.on('download-progress', (_event, info) => callback(info)), + downloadUpdate: () => ipcRenderer.send('download-update'), + installUpdate: () => ipcRenderer.send('install-update'), + cancelUpdate: () => ipcRenderer.send('cancel-update'), +}) diff --git a/src/App.vue b/src/App.vue index fc19580ca..7636b40ac 100644 --- a/src/App.vue +++ b/src/App.vue @@ -315,6 +315,7 @@ + diff --git a/src/libs/cosmos.ts b/src/libs/cosmos.ts index dc865c944..2b5820b3d 100644 --- a/src/libs/cosmos.ts +++ b/src/libs/cosmos.ts @@ -80,30 +80,144 @@ declare global { sum(): number } - /* eslint-disable jsdoc/require-jsdoc */ + /** + * Extended Window interface with some dedicated APIs, including data-lake methods, cockpit actions management and + * Electron messaging. + */ interface Window { + /** + * Exposed data-lake and cockpit action methods + */ cockpit: { - // Data lake: + /** + * The object that holds the data-lake variables data + */ cockpitActionVariableData: typeof cockpitActionVariableData + /** + * Get data from an specific data lake variable + * @param id - The id of the data to retrieve + * @returns The data or undefined if not available + */ getCockpitActionVariableData: typeof getCockpitActionVariableData + /** + * Listen to data changes on a specific data lake variable + * @param id - The id of the data to listen to + * @param listener - The listener callback + */ listenCockpitActionVariable: typeof listenCockpitActionVariable + /** + * Stop listening to data changes on a specific data lake variable + * @param id - The id of the data to stop listening to + */ unlistenCockpitActionVariable: typeof unlistenCockpitActionVariable + /** + * Get info about all variables in the data lake + * @returns Data lake data + */ getAllCockpitActionVariablesInfo: typeof getAllCockpitActionVariablesInfo + /** + * Get info about a specific variable in the data lake + * @param id - The id of the data to retrieve + * @returns The data info or undefined if not available + */ getCockpitActionVariableInfo: typeof getCockpitActionVariableInfo + /** + * Set the value of an specific data lake variable + * @param id - The id of the data to set + * @param value - The value to set + */ setCockpitActionVariableData: typeof setCockpitActionVariableData + /** + * Create a new variable in the data lake + * @param variable - The variable to create + * @param initialValue - The initial value for the variable + */ createCockpitActionVariable: typeof createCockpitActionVariable + /** + * Update information about an specific data lake variable + * @param variable - The variable to update + */ updateCockpitActionVariableInfo: typeof updateCockpitActionVariableInfo + /** + * Delete a variable from the data lake + * @param id - The id of the variable to delete + */ deleteCockpitActionVariable: typeof deleteCockpitActionVariable + + /** + * Cockpit actions related methods + */ // Cockpit actions: + /** + * Get all available cockpit actions + * @returns Available cockpit actions + */ availableCockpitActions: typeof availableCockpitActions + /** + * Register a new cockpit action + * @param action - The action to register + */ registerNewAction: typeof registerNewAction + /** + * Delete a cockpit action + * @param id - The id of the action to delete + */ deleteAction: typeof deleteAction + /** + * Register a callback for a cockpit action + * @param action - The action to register the callback for + * @param callback - The callback to register + */ registerActionCallback: typeof registerActionCallback + /** + * Unregister a callback for a cockpit action + * @param id - The id of the action to unregister the callback for + */ unregisterActionCallback: typeof unregisterActionCallback + /** + * Execute the callback for a cockpit action + * @param id - The id of the action to execute the callback for + */ executeActionCallback: typeof executeActionCallback } + /** + * Electron API for update management + */ + electronAPI: { + /** + * Register callback for update available event + */ + onUpdateAvailable: (callback: (info: any) => void) => void + /** + * Register callback for update downloaded event + */ + onUpdateDownloaded: (callback: (info: any) => void) => void + /** + * Trigger update download + */ + downloadUpdate: () => void + /** + * Trigger update installation + */ + installUpdate: () => void + /** + * Cancel ongoing update + */ + cancelUpdate: () => void + /** + * Register callback for checking for update event + */ + onCheckingForUpdate: (callback: () => void) => void + /** + * Register callback for update not available event + */ + onUpdateNotAvailable: (callback: (info: any) => void) => void + /** + * Register callback for download progress event + */ + onDownloadProgress: (callback: (info: any) => void) => void + } } - /* eslint-enable jsdoc/require-jsdoc */ } // Use global as window when running for browsers