Add auto updater configuration (#122)

Adds a new set of configurations for managing whether the app will
automatically check for updates. Ports over the auto update code from
the old app. In this version, the main difference is that updates can be
manually checked for using a menu bar item, even if auto updates are
disabled.
This commit is contained in:
Evan Simkowitz 2024-07-18 16:55:04 -07:00 committed by GitHub
parent 776ccd7da0
commit c47e17903d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 277 additions and 14 deletions

View File

@ -54,6 +54,8 @@ tasks:
generates: generates:
- frontend/types/gotypes.d.ts - frontend/types/gotypes.d.ts
- pkg/wshrpc/wshclient/wshclient.go - pkg/wshrpc/wshclient/wshclient.go
- frontend/app/store/services.ts
- frontend/app/store/wshserver.ts
build:server: build:server:
desc: Build the wavesrv component. desc: Build the wavesrv component.

View File

@ -73,7 +73,7 @@ const config = {
}, },
publish: { publish: {
provider: "generic", provider: "generic",
url: "https://dl.waveterm.dev/releases", url: "https://dl.waveterm.dev/releases-w2",
}, },
}; };

View File

@ -3,6 +3,7 @@
import { WaveDevVarName, WaveDevViteVarName } from "@/util/isdev"; import { WaveDevVarName, WaveDevViteVarName } from "@/util/isdev";
import * as electron from "electron"; import * as electron from "electron";
import { autoUpdater } from "electron-updater";
import fs from "fs"; import fs from "fs";
import * as child_process from "node:child_process"; import * as child_process from "node:child_process";
import os from "os"; import os from "os";
@ -536,21 +537,60 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
} }
function makeAppMenu() { function makeAppMenu() {
const fileMenu: Electron.MenuItemConstructorOptions[] = []; const fileMenu: Electron.MenuItemConstructorOptions[] = [
fileMenu.push({ {
label: "New Window", label: "New Window",
accelerator: "CommandOrControl+N", accelerator: "CommandOrControl+N",
click: () => fireAndForget(createNewWaveWindow), click: () => fireAndForget(createNewWaveWindow),
});
fileMenu.push({
role: "close",
click: () => {
electron.BrowserWindow.getFocusedWindow()?.close();
}, },
}); {
role: "close",
click: () => {
electron.BrowserWindow.getFocusedWindow()?.close();
},
},
];
const appMenu: Electron.MenuItemConstructorOptions[] = [
{
role: "about",
},
{
label: "Check for Updates",
click: () => {
const checkingNotification = new electron.Notification({
title: "Wave Terminal",
body: "Checking for updates.",
});
checkingNotification.show();
fireAndForget(() => checkForUpdates());
},
},
{
type: "separator",
},
{
role: "services",
},
{
type: "separator",
},
{
role: "hide",
},
{
role: "hideOthers",
},
{
type: "separator",
},
{
role: "quit",
},
];
const menuTemplate: Electron.MenuItemConstructorOptions[] = [ const menuTemplate: Electron.MenuItemConstructorOptions[] = [
{ {
role: "appMenu", role: "appMenu",
submenu: appMenu,
}, },
{ {
role: "fileMenu", role: "fileMenu",
@ -596,6 +636,170 @@ process.on("uncaughtException", (error) => {
electron.app.quit(); electron.app.quit();
}); });
// ====== AUTO-UPDATER ====== //
let autoUpdateLock = false;
let autoUpdateInterval: NodeJS.Timeout | null = null;
let availableUpdateReleaseName: string | null = null;
let availableUpdateReleaseNotes: string | null = null;
let appUpdateStatus = "unavailable";
let lastUpdateCheck: Date = null;
/**
* Sets the app update status and sends it to the main window
* @param status The AppUpdateStatus to set, either "ready" or "unavailable"
*/
function setAppUpdateStatus(status: string) {
appUpdateStatus = status;
electron.BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send("app-update-status", appUpdateStatus);
});
}
/**
* Checks if an hour has passed since the last update check, and if so, checks for updates using the `autoUpdater` object
*/
async function checkForUpdates() {
const autoUpdateOpts = (await services.FileService.GetSettingsConfig()).autoupdate;
if (!autoUpdateOpts.enabled) {
console.log("Auto update is disabled in settings. Removing the auto update interval.");
clearInterval(autoUpdateInterval);
autoUpdateInterval = null;
return;
}
const now = new Date();
if (!lastUpdateCheck || Math.abs(now.getTime() - lastUpdateCheck.getTime()) > autoUpdateOpts.intervalms) {
fireAndForget(() => autoUpdater.checkForUpdates());
lastUpdateCheck = now;
}
}
/**
* Initializes the updater and sets up event listeners
*/
function initUpdater() {
if (isDev) {
console.log("skipping auto-updater in dev mode");
return null;
}
setAppUpdateStatus("unavailable");
autoUpdater.removeAllListeners();
autoUpdater.on("error", (err) => {
console.log("updater error");
console.log(err);
});
autoUpdater.on("checking-for-update", () => {
console.log("checking-for-update");
});
autoUpdater.on("update-available", () => {
console.log("update-available; downloading...");
});
autoUpdater.on("update-not-available", () => {
console.log("update-not-available");
});
autoUpdater.on("update-downloaded", (event) => {
console.log("update-downloaded", [event]);
availableUpdateReleaseName = event.releaseName;
availableUpdateReleaseNotes = event.releaseNotes as string | null;
// Display the update banner and create a system notification
setAppUpdateStatus("ready");
const updateNotification = new electron.Notification({
title: "Wave Terminal",
body: "A new version of Wave Terminal is ready to install.",
});
updateNotification.on("click", () => {
fireAndForget(() => installAppUpdate());
});
updateNotification.show();
});
}
/**
* Starts the auto update check interval.
* @returns The timeout object for the auto update checker.
*/
function startAutoUpdateInterval(): NodeJS.Timeout {
// check for updates right away and keep checking later
checkForUpdates();
return setInterval(() => {
checkForUpdates();
}, 600000); // intervals are unreliable when an app is suspended so we will check every 10 mins if an hour has passed.
}
/**
* Prompts the user to install the downloaded application update and restarts the application
*/
async function installAppUpdate() {
const dialogOpts: Electron.MessageBoxOptions = {
type: "info",
buttons: ["Restart", "Later"],
title: "Application Update",
message: process.platform === "win32" ? availableUpdateReleaseNotes : availableUpdateReleaseName,
detail: "A new version has been downloaded. Restart the application to apply the updates.",
};
const allWindows = electron.BrowserWindow.getAllWindows();
if (allWindows.length > 0) {
await electron.dialog
.showMessageBox(electron.BrowserWindow.getFocusedWindow() ?? allWindows[0], dialogOpts)
.then(({ response }) => {
if (response === 0) autoUpdater.quitAndInstall();
});
}
}
electron.ipcMain.on("install-app-update", () => fireAndForget(() => installAppUpdate()));
electron.ipcMain.on("get-app-update-status", (event) => {
event.returnValue = appUpdateStatus;
});
/**
* Configures the auto-updater based on the user's preference
* @param enabled Whether the auto-updater should be enabled
*/
async function configureAutoUpdater() {
// simple lock to prevent multiple auto-update configuration attempts, this should be very rare
if (autoUpdateLock) {
console.log("auto-update configuration already in progress, skipping");
return;
}
autoUpdateLock = true;
const autoUpdateEnabled = (await services.FileService.GetSettingsConfig()).autoupdate.enabled;
try {
console.log("Configuring updater");
initUpdater();
} catch (e) {
console.warn("error configuring updater", e.toString());
}
if (autoUpdateEnabled && autoUpdateInterval == null) {
lastUpdateCheck = null;
try {
console.log("configuring auto update interval");
autoUpdateInterval = startAutoUpdateInterval();
} catch (e) {
console.log("error configuring auto update interval", e.toString());
}
} else if (!autoUpdateEnabled && autoUpdateInterval != null) {
console.log("disabling auto updater");
clearInterval(autoUpdateInterval);
autoUpdateInterval = null;
}
autoUpdateLock = false;
}
// ====== AUTO-UPDATER ====== //
async function appMain() { async function appMain() {
const startTs = Date.now(); const startTs = Date.now();
const instanceLock = electronApp.requestSingleInstanceLock(); const instanceLock = electronApp.requestSingleInstanceLock();
@ -637,6 +841,7 @@ async function appMain() {
console.log("show", win.waveWindowId); console.log("show", win.waveWindowId);
win.show(); win.show();
} }
configureAutoUpdater();
globalIsStarting = false; globalIsStarting = false;
electronApp.on("activate", () => { electronApp.on("activate", () => {

View File

@ -49,7 +49,7 @@ class FileServiceType {
DeleteFile(arg1: string): Promise<void> { DeleteFile(arg1: string): Promise<void> {
return WOS.callBackendService("file", "DeleteFile", Array.from(arguments)) return WOS.callBackendService("file", "DeleteFile", Array.from(arguments))
} }
GetSettingsConfig(): Promise<any> { GetSettingsConfig(): Promise<SettingsConfigType> {
return WOS.callBackendService("file", "GetSettingsConfig", Array.from(arguments)) return WOS.callBackendService("file", "GetSettingsConfig", Array.from(arguments))
} }
GetWaveFile(arg1: string, arg2: string): Promise<any> { GetWaveFile(arg1: string, arg2: string): Promise<any> {

View File

@ -5,6 +5,12 @@
declare global { declare global {
// wconfig.AutoUpdateOpts
type AutoUpdateOpts = {
enabled: boolean;
intervalms: number;
};
// wstore.Block // wstore.Block
type Block = WaveObj & { type Block = WaveObj & {
blockdef: BlockDef; blockdef: BlockDef;
@ -214,6 +220,7 @@ declare global {
term: TerminalConfigType; term: TerminalConfigType;
widgets: WidgetsConfigType[]; widgets: WidgetsConfigType[];
blockheader: BlockHeaderOpts; blockheader: BlockHeaderOpts;
autoupdate: AutoUpdateOpts;
}; };
// wstore.StickerClickOptsType // wstore.StickerClickOptsType

View File

@ -82,6 +82,7 @@
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"electron-updater": "6.2.1",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"immer": "^10.1.1", "immer": "^10.1.1",
"jotai": "^2.8.0", "jotai": "^2.8.0",

View File

@ -161,7 +161,7 @@ func (fs *FileService) DeleteFile(path string) error {
return os.Remove(cleanedPath) return os.Remove(cleanedPath)
} }
func (fs *FileService) GetSettingsConfig() interface{} { func (fs *FileService) GetSettingsConfig() wconfig.SettingsConfigType {
watcher := wconfig.GetWatcher() watcher := wconfig.GetWatcher()
return watcher.GetSettingsConfig() return watcher.GetSettingsConfig()
} }

View File

@ -36,11 +36,17 @@ type BlockHeaderOpts struct {
ShowBlockIds bool `json:"showblockids"` ShowBlockIds bool `json:"showblockids"`
} }
type AutoUpdateOpts struct {
Enabled bool `json:"enabled"`
IntervalMs uint32 `json:"intervalms"`
}
type SettingsConfigType struct { type SettingsConfigType struct {
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
Term TerminalConfigType `json:"term"` Term TerminalConfigType `json:"term"`
Widgets []WidgetsConfigType `json:"widgets"` Widgets []WidgetsConfigType `json:"widgets"`
BlockHeader BlockHeaderOpts `json:"blockheader"` BlockHeader BlockHeaderOpts `json:"blockheader"`
AutoUpdate AutoUpdateOpts `json:"autoupdate"`
} }
func getSettingsConfigDefaults() SettingsConfigType { func getSettingsConfigDefaults() SettingsConfigType {
@ -97,5 +103,9 @@ func getSettingsConfigDefaults() SettingsConfigType {
}, },
}, },
}, },
AutoUpdate: AutoUpdateOpts{
Enabled: true,
IntervalMs: 3600000,
},
} }
} }

View File

@ -7154,6 +7154,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"electron-updater@npm:6.2.1":
version: 6.2.1
resolution: "electron-updater@npm:6.2.1"
dependencies:
builder-util-runtime: "npm:9.2.4"
fs-extra: "npm:^10.1.0"
js-yaml: "npm:^4.1.0"
lazy-val: "npm:^1.0.5"
lodash.escaperegexp: "npm:^4.1.2"
lodash.isequal: "npm:^4.5.0"
semver: "npm:^7.3.8"
tiny-typed-emitter: "npm:^2.1.0"
checksum: 10c0/b376e13bf2b4675ca853c4164a4caf9454d0d41e797c0f8fd011d66693d4eb5ece020953f3d06c0a63d9d5077a9ae9e8447f26c602da317edb1521c4bd99e2f8
languageName: node
linkType: hard
"electron-vite@npm:^2.2.0": "electron-vite@npm:^2.2.0":
version: 2.2.0 version: 2.2.0
resolution: "electron-vite@npm:2.2.0" resolution: "electron-vite@npm:2.2.0"
@ -9970,6 +9986,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lodash.escaperegexp@npm:^4.1.2":
version: 4.1.2
resolution: "lodash.escaperegexp@npm:4.1.2"
checksum: 10c0/484ad4067fa9119bb0f7c19a36ab143d0173a081314993fe977bd00cf2a3c6a487ce417a10f6bac598d968364f992153315f0dbe25c9e38e3eb7581dd333e087
languageName: node
linkType: hard
"lodash.isequal@npm:^4.5.0":
version: 4.5.0
resolution: "lodash.isequal@npm:4.5.0"
checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f
languageName: node
linkType: hard
"lodash.merge@npm:^4.6.2": "lodash.merge@npm:^4.6.2":
version: 4.6.2 version: 4.6.2
resolution: "lodash.merge@npm:4.6.2" resolution: "lodash.merge@npm:4.6.2"
@ -13504,6 +13534,7 @@ __metadata:
dayjs: "npm:^1.11.11" dayjs: "npm:^1.11.11"
electron: "npm:^31.1.0" electron: "npm:^31.1.0"
electron-builder: "npm:^24.13.3" electron-builder: "npm:^24.13.3"
electron-updater: "npm:6.2.1"
electron-vite: "npm:^2.2.0" electron-vite: "npm:^2.2.0"
eslint: "npm:^9.2.0" eslint: "npm:^9.2.0"
eslint-config-prettier: "npm:^9.1.0" eslint-config-prettier: "npm:^9.1.0"
@ -13573,6 +13604,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tiny-typed-emitter@npm:^2.1.0":
version: 2.1.0
resolution: "tiny-typed-emitter@npm:2.1.0"
checksum: 10c0/522bed4c579ee7ee16548540cb693a3d098b137496110f5a74bff970b54187e6b7343a359b703e33f77c5b4b90ec6cebc0d0ec3dbdf1bd418723c5c3ce36d8a2
languageName: node
linkType: hard
"tinybench@npm:^2.5.1": "tinybench@npm:^2.5.1":
version: 2.8.0 version: 2.8.0
resolution: "tinybench@npm:2.8.0" resolution: "tinybench@npm:2.8.0"