From ed1c1ff88ec8b16a2e7d97892c74cde74bca39d1 Mon Sep 17 00:00:00 2001 From: Alex Freska <alex.freska@gmail.com> Date: Thu, 25 Jan 2024 10:07:47 -0500 Subject: [PATCH] ability to upgrade to latest daemon while the app is running --- hostd/electron-src/download.ts | 1 + hostd/electron-src/ipc.ts | 7 +- hostd/electron-src/preload.ts | 1 + hostd/electron-src/startup.ts | 8 ++- hostd/electron-src/state.ts | 3 + hostd/electron-src/tray.ts | 3 +- hostd/electron-src/window.ts | 9 +-- hostd/renderer/components/UpdateBanner.tsx | 76 +++++++++++++++++--- hostd/renderer/renderer.d.ts | 1 + renterd/electron-src/download.ts | 1 + renterd/electron-src/ipc.ts | 7 +- renterd/electron-src/preload.ts | 1 + renterd/electron-src/startup.ts | 8 ++- renterd/electron-src/state.ts | 3 + renterd/electron-src/tray.ts | 3 +- renterd/electron-src/window.ts | 9 +-- renterd/renderer/components/UpdateBanner.tsx | 76 +++++++++++++++++--- renterd/renderer/renderer.d.ts | 1 + 18 files changed, 176 insertions(+), 42 deletions(-) diff --git a/hostd/electron-src/download.ts b/hostd/electron-src/download.ts index 49b5753..d9bb7ff 100644 --- a/hostd/electron-src/download.ts +++ b/hostd/electron-src/download.ts @@ -18,6 +18,7 @@ export async function downloadRelease(): Promise<void> { const release = releaseData.data const asset = release.assets.find((asset) => asset.name === releaseAsset()) + console.log(`Downloading ${releaseAsset()}`) if (asset) { console.log('Release name:', release.name) diff --git a/hostd/electron-src/ipc.ts b/hostd/electron-src/ipc.ts index f346296..ff3a90b 100644 --- a/hostd/electron-src/ipc.ts +++ b/hostd/electron-src/ipc.ts @@ -14,6 +14,7 @@ import { getIsConfigured, saveConfig, } from './config' +import { downloadRelease } from './download' export function initIpc() { ipcMain.handle('open-browser', (_, url: string) => { @@ -26,8 +27,10 @@ export function initIpc() { await stopDaemon() }) ipcMain.handle('daemon-is-running', (_) => { - const isDaemonRunning = getIsDaemonRunning() - return isDaemonRunning + return getIsDaemonRunning() + }) + ipcMain.handle('daemon-update', async (_) => { + await downloadRelease() }) ipcMain.handle('config-get', (_) => { const config = getConfig() diff --git a/hostd/electron-src/preload.ts b/hostd/electron-src/preload.ts index fa30894..76eb4f4 100644 --- a/hostd/electron-src/preload.ts +++ b/hostd/electron-src/preload.ts @@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('electron', { checkIsDaemonRunning: () => ipcRenderer.invoke('daemon-is-running'), daemonStart: () => ipcRenderer.invoke('daemon-start'), daemonStop: () => ipcRenderer.invoke('daemon-stop'), + daemonUpdate: () => ipcRenderer.invoke('daemon-update'), openBrowser: (url: string) => ipcRenderer.invoke('open-browser', url), getConfig: () => ipcRenderer.invoke('config-get'), saveConfig: (config: Config) => ipcRenderer.invoke('config-save', config), diff --git a/hostd/electron-src/startup.ts b/hostd/electron-src/startup.ts index e8c30b5..df6a892 100644 --- a/hostd/electron-src/startup.ts +++ b/hostd/electron-src/startup.ts @@ -1,6 +1,6 @@ import { startDaemon } from './daemon' import { getIsConfigured } from './config' -import { state } from './state' +import { state, system } from './state' export function startup() { // If the app is already configured, start the daemon and open browser @@ -9,4 +9,10 @@ export function startup() { startDaemon() state.mainWindow?.close() } + + if (system.isDev) { + state.mainWindow?.setMaximumSize(2000, 2000) + state.mainWindow?.setSize(1000, 800) + state.mainWindow?.webContents.openDevTools() + } } diff --git a/hostd/electron-src/state.ts b/hostd/electron-src/state.ts index 9f6088a..b27989c 100644 --- a/hostd/electron-src/state.ts +++ b/hostd/electron-src/state.ts @@ -1,5 +1,6 @@ import { ChildProcess } from 'child_process' import { BrowserWindow, Tray } from 'electron' +import isDev from 'electron-is-dev' export let state: { mainWindow: BrowserWindow | null @@ -14,10 +15,12 @@ export let state: { } export const system: { + isDev: boolean isDarwin: boolean isLinux: boolean isWindows: boolean } = { + isDev, isDarwin: process.platform === 'darwin', isLinux: process.platform === 'linux', isWindows: process.platform === 'win32', diff --git a/hostd/electron-src/tray.ts b/hostd/electron-src/tray.ts index fbce2de..7769a6f 100644 --- a/hostd/electron-src/tray.ts +++ b/hostd/electron-src/tray.ts @@ -1,11 +1,10 @@ import path from 'path' import { app, Tray, Menu } from 'electron' -import isDev from 'electron-is-dev' import { state, system } from './state' export function initTray() { const iconName = system.isDarwin ? 'tray.png' : 'tray-win.png' - const iconPath = isDev + const iconPath = system.isDev ? path.join(process.cwd(), 'assets', iconName) : path.join(__dirname, '../assets', iconName) diff --git a/hostd/electron-src/window.ts b/hostd/electron-src/window.ts index 5d222c5..f8caffc 100644 --- a/hostd/electron-src/window.ts +++ b/hostd/electron-src/window.ts @@ -1,6 +1,5 @@ import path, { join } from 'path' import { BrowserWindow, app } from 'electron' -import isDev from 'electron-is-dev' import { format } from 'url' import { state, system } from './state' @@ -45,7 +44,7 @@ export function initWindow() { return false }) - const url = isDev + const url = system.isDev ? 'http://localhost:8000/' : format({ pathname: path.join(__dirname, '../renderer/out/index.html'), @@ -53,10 +52,4 @@ export function initWindow() { slashes: true, }) state.mainWindow.loadURL(url) - - if (isDev) { - state.mainWindow.setMaximumSize(2000, 2000) - state.mainWindow.setSize(1000, 800) - state.mainWindow.webContents.openDevTools() - } } diff --git a/hostd/renderer/components/UpdateBanner.tsx b/hostd/renderer/components/UpdateBanner.tsx index 35a9547..4b363d1 100644 --- a/hostd/renderer/components/UpdateBanner.tsx +++ b/hostd/renderer/components/UpdateBanner.tsx @@ -1,21 +1,81 @@ 'use client' -import { Text } from '@siafoundation/design-system' +import { + LoadingDots, + Text, + triggerErrorToast, + triggerSuccessToast, +} from '@siafoundation/design-system' import { Upgrade16 } from '@siafoundation/react-icons' import { useLatestVersion } from './useLatestVersion' import { useInstalledVersion } from './useInstalledVersion' +import { useCallback, useState } from 'react' export function UpdateBanner() { const latestVersion = useLatestVersion() const installedVersion = useInstalledVersion() + const [isUpdating, setIsUpdating] = useState(false) + const [isRestarting, setIsRestarting] = useState(false) + + const update = useCallback(async () => { + try { + setIsUpdating(true) + await window.electron.daemonUpdate() + } catch (e) { + console.log(e) + triggerErrorToast('Error downloading update. Please try again.') + setIsUpdating(false) + setIsRestarting(false) + return + } + try { + setIsRestarting(true) + await window.electron.daemonStart() + } catch (e) { + console.log(e) + triggerErrorToast('Error restarting daemon. Please try again.') + setIsUpdating(false) + setIsRestarting(false) + return + } + triggerSuccessToast(`Updated to hostd version ${installedVersion.data}.`) + setIsUpdating(false) + setIsRestarting(false) + }, [setIsUpdating, installedVersion.data]) + + if (latestVersion.data === installedVersion.data) { + return null + } + return ( - <div className="flex w-full gap-2 items-center justify-center py-2 px-3 bg-amber-600 dark:bg-amber-500"> - <Text color="lo"> - <Upgrade16 /> - </Text> - <Text size="14" color="lo"> - An update to version {latestVersion.data} is available. - </Text> + <div + className="flex w-full gap-2 items-center justify-center py-2 px-3 bg-amber-600 dark:bg-amber-500 cursor-pointer" + onClick={update} + > + {isRestarting ? ( + <> + <LoadingDots /> + <Text size="14" color="lo"> + Restarting hostd daemon {latestVersion.data} + </Text> + </> + ) : isUpdating ? ( + <> + <LoadingDots /> + <Text size="14" color="lo"> + Updating to hostd {latestVersion.data} + </Text> + </> + ) : ( + <> + <Text color="lo"> + <Upgrade16 /> + </Text> + <Text size="14" color="lo"> + An update to hostd {latestVersion.data} is available. + </Text> + </> + )} </div> ) } diff --git a/hostd/renderer/renderer.d.ts b/hostd/renderer/renderer.d.ts index 074d20d..5653a01 100644 --- a/hostd/renderer/renderer.d.ts +++ b/hostd/renderer/renderer.d.ts @@ -5,6 +5,7 @@ export interface API { openBrowser: (url: string) => Promise<void> daemonStart: () => Promise<void> daemonStop: () => Promise<void> + daemonUpdate: () => Promise<void> getConfig: () => Promise<Config> openDataDirectory: () => Promise<void> getIsConfigured: () => Promise<boolean> diff --git a/renterd/electron-src/download.ts b/renterd/electron-src/download.ts index bdfd94f..1c28afa 100644 --- a/renterd/electron-src/download.ts +++ b/renterd/electron-src/download.ts @@ -18,6 +18,7 @@ export async function downloadRelease(): Promise<void> { const release = releaseData.data const asset = release.assets.find((asset) => asset.name === releaseAsset()) + console.log(`Downloading ${releaseAsset()}`) if (asset) { console.log('Release name:', release.name) diff --git a/renterd/electron-src/ipc.ts b/renterd/electron-src/ipc.ts index f346296..ff3a90b 100644 --- a/renterd/electron-src/ipc.ts +++ b/renterd/electron-src/ipc.ts @@ -14,6 +14,7 @@ import { getIsConfigured, saveConfig, } from './config' +import { downloadRelease } from './download' export function initIpc() { ipcMain.handle('open-browser', (_, url: string) => { @@ -26,8 +27,10 @@ export function initIpc() { await stopDaemon() }) ipcMain.handle('daemon-is-running', (_) => { - const isDaemonRunning = getIsDaemonRunning() - return isDaemonRunning + return getIsDaemonRunning() + }) + ipcMain.handle('daemon-update', async (_) => { + await downloadRelease() }) ipcMain.handle('config-get', (_) => { const config = getConfig() diff --git a/renterd/electron-src/preload.ts b/renterd/electron-src/preload.ts index fa30894..76eb4f4 100644 --- a/renterd/electron-src/preload.ts +++ b/renterd/electron-src/preload.ts @@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('electron', { checkIsDaemonRunning: () => ipcRenderer.invoke('daemon-is-running'), daemonStart: () => ipcRenderer.invoke('daemon-start'), daemonStop: () => ipcRenderer.invoke('daemon-stop'), + daemonUpdate: () => ipcRenderer.invoke('daemon-update'), openBrowser: (url: string) => ipcRenderer.invoke('open-browser', url), getConfig: () => ipcRenderer.invoke('config-get'), saveConfig: (config: Config) => ipcRenderer.invoke('config-save', config), diff --git a/renterd/electron-src/startup.ts b/renterd/electron-src/startup.ts index 9bd4544..83b160f 100644 --- a/renterd/electron-src/startup.ts +++ b/renterd/electron-src/startup.ts @@ -1,7 +1,7 @@ import { shell } from 'electron' import { startDaemon } from './daemon' import { getConfig, getIsConfigured } from './config' -import { state } from './state' +import { state, system } from './state' export function startup() { // If the app is already configured, start the daemon and open browser @@ -11,4 +11,10 @@ export function startup() { state.mainWindow?.close() shell.openExternal(`http://${getConfig().http.address}`) } + + if (system.isDev) { + state.mainWindow?.setMaximumSize(2000, 2000) + state.mainWindow?.setSize(1000, 800) + state.mainWindow?.webContents.openDevTools() + } } diff --git a/renterd/electron-src/state.ts b/renterd/electron-src/state.ts index 9f6088a..f8f7e8c 100644 --- a/renterd/electron-src/state.ts +++ b/renterd/electron-src/state.ts @@ -1,5 +1,6 @@ import { ChildProcess } from 'child_process' import { BrowserWindow, Tray } from 'electron' +import isDev from 'electron-is-dev' export let state: { mainWindow: BrowserWindow | null @@ -14,10 +15,12 @@ export let state: { } export const system: { + isDev: boolean isDarwin: boolean isLinux: boolean isWindows: boolean } = { + isDev: isDev, isDarwin: process.platform === 'darwin', isLinux: process.platform === 'linux', isWindows: process.platform === 'win32', diff --git a/renterd/electron-src/tray.ts b/renterd/electron-src/tray.ts index fbce2de..7769a6f 100644 --- a/renterd/electron-src/tray.ts +++ b/renterd/electron-src/tray.ts @@ -1,11 +1,10 @@ import path from 'path' import { app, Tray, Menu } from 'electron' -import isDev from 'electron-is-dev' import { state, system } from './state' export function initTray() { const iconName = system.isDarwin ? 'tray.png' : 'tray-win.png' - const iconPath = isDev + const iconPath = system.isDev ? path.join(process.cwd(), 'assets', iconName) : path.join(__dirname, '../assets', iconName) diff --git a/renterd/electron-src/window.ts b/renterd/electron-src/window.ts index 5d222c5..f8caffc 100644 --- a/renterd/electron-src/window.ts +++ b/renterd/electron-src/window.ts @@ -1,6 +1,5 @@ import path, { join } from 'path' import { BrowserWindow, app } from 'electron' -import isDev from 'electron-is-dev' import { format } from 'url' import { state, system } from './state' @@ -45,7 +44,7 @@ export function initWindow() { return false }) - const url = isDev + const url = system.isDev ? 'http://localhost:8000/' : format({ pathname: path.join(__dirname, '../renderer/out/index.html'), @@ -53,10 +52,4 @@ export function initWindow() { slashes: true, }) state.mainWindow.loadURL(url) - - if (isDev) { - state.mainWindow.setMaximumSize(2000, 2000) - state.mainWindow.setSize(1000, 800) - state.mainWindow.webContents.openDevTools() - } } diff --git a/renterd/renderer/components/UpdateBanner.tsx b/renterd/renderer/components/UpdateBanner.tsx index 35a9547..30ba31a 100644 --- a/renterd/renderer/components/UpdateBanner.tsx +++ b/renterd/renderer/components/UpdateBanner.tsx @@ -1,21 +1,81 @@ 'use client' -import { Text } from '@siafoundation/design-system' +import { + LoadingDots, + Text, + triggerErrorToast, + triggerSuccessToast, +} from '@siafoundation/design-system' import { Upgrade16 } from '@siafoundation/react-icons' import { useLatestVersion } from './useLatestVersion' import { useInstalledVersion } from './useInstalledVersion' +import { useCallback, useState } from 'react' export function UpdateBanner() { const latestVersion = useLatestVersion() const installedVersion = useInstalledVersion() + const [isUpdating, setIsUpdating] = useState(false) + const [isRestarting, setIsRestarting] = useState(false) + + const update = useCallback(async () => { + try { + setIsUpdating(true) + await window.electron.daemonUpdate() + } catch (e) { + console.log(e) + triggerErrorToast('Error downloading update. Please try again.') + setIsUpdating(false) + setIsRestarting(false) + return + } + try { + setIsRestarting(true) + await window.electron.daemonStart() + } catch (e) { + console.log(e) + triggerErrorToast('Error restarting daemon. Please try again.') + setIsUpdating(false) + setIsRestarting(false) + return + } + triggerSuccessToast(`Updated to renterd version ${installedVersion.data}.`) + setIsUpdating(false) + setIsRestarting(false) + }, [setIsUpdating, installedVersion.data]) + + if (latestVersion.data === installedVersion.data) { + return null + } + return ( - <div className="flex w-full gap-2 items-center justify-center py-2 px-3 bg-amber-600 dark:bg-amber-500"> - <Text color="lo"> - <Upgrade16 /> - </Text> - <Text size="14" color="lo"> - An update to version {latestVersion.data} is available. - </Text> + <div + className="flex w-full gap-2 items-center justify-center py-2 px-3 bg-amber-600 dark:bg-amber-500 cursor-pointer" + onClick={update} + > + {isRestarting ? ( + <> + <LoadingDots /> + <Text size="14" color="lo"> + Restarting renterd daemon {latestVersion.data} + </Text> + </> + ) : isUpdating ? ( + <> + <LoadingDots /> + <Text size="14" color="lo"> + Updating to renterd {latestVersion.data} + </Text> + </> + ) : ( + <> + <Text color="lo"> + <Upgrade16 /> + </Text> + <Text size="14" color="lo"> + An update to renterd {latestVersion.data} is available. + </Text> + </> + )} </div> ) } diff --git a/renterd/renderer/renderer.d.ts b/renterd/renderer/renderer.d.ts index 074d20d..5653a01 100644 --- a/renterd/renderer/renderer.d.ts +++ b/renterd/renderer/renderer.d.ts @@ -5,6 +5,7 @@ export interface API { openBrowser: (url: string) => Promise<void> daemonStart: () => Promise<void> daemonStop: () => Promise<void> + daemonUpdate: () => Promise<void> getConfig: () => Promise<Config> openDataDirectory: () => Promise<void> getIsConfigured: () => Promise<boolean>