Add auto-update notifications and confirmation (#1941)

* add auto-update notifications and confirmation

* Show single update notification (#1985)

* Moving notification icons to top (#1987)

* Switch to EventEmitter (producer&consumer) model

* Add `onCorrect` and `onceCorrect` to ipc module for typechecking ipc  messages

* move type enforced ipc methods to seperate file, add unit tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-02-09 10:47:24 -05:00
1 parent 741973dd29
commit a61425124f
23 files changed
+434 -47

No files matched your search

+68 -10
View File
@@ -1,20 +1,78 @@
import { autoUpdater } from "electron-updater";
import { autoUpdater, UpdateInfo } from "electron-updater";
import logger from "./logger";
import { isDevelopment, isTestEnv } from "../common/vars";
import { delay } from "../common/utils";
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
import { ipcMain } from "electron";
export class AppUpdater {
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day
function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
if (arg.doUpdate) {
if (arg.now) {
logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
autoUpdater.downloadUpdate()
.then(() => autoUpdater.quitAndInstall())
.catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error }));
} else {
logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.downloadUpdate()
.catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download update`, { error }));
}
} else {
logger.info(`${AutoUpdateLogPrefix}: User chose not to update`);
}
}
static checkForUpdates() {
return autoUpdater.checkForUpdatesAndNotify();
/**
* starts the automatic update checking
* @param interval milliseconds between interval to check on, defaults to 24h
*/
export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
if (isDevelopment || isTestEnv) {
return;
}
autoUpdater.logger = logger;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater
.on("update-available", (args: UpdateInfo) => {
try {
const backchannel = `auto-update:${args.version}`;
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
autoUpdater.logger = logger;
ipcMain.removeAllListeners(backchannel); // only one handler should be present
// make sure that the handler is in place before broadcasting (prevent race-condition)
onceCorrect({
source: ipcMain,
channel: backchannel,
listener: handleAutoUpdateBackChannel,
verifier: areArgsUpdateAvailableToBackchannel,
});
logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: args.version });
broadcastMessage(UpdateAvailableChannel, backchannel, args);
} catch (error) {
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
}
});
async function helper() {
while (true) {
await checkForUpdates();
await delay(interval);
}
}
helper();
}
public start() {
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
export async function checkForUpdates(): Promise<void> {
try {
logger.info(`📡 Checking for app updates`);
return AppUpdater.checkForUpdates();
await autoUpdater.checkForUpdates();
} catch (error) {
logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) });
}
}
+2 -6
View File
@@ -10,7 +10,6 @@ import path from "path";
import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
import { AppUpdater } from "./app-updater";
import { shellSync } from "./shell-sync";
import { getFreePort } from "./port";
import { mangleProxyEnv } from "./proxy-env";
@@ -28,6 +27,7 @@ import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem";
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers } from "../common/ipc";
import { startUpdateChecking } from "./app-updater";
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
@@ -72,11 +72,6 @@ app.on("ready", async () => {
app.exit();
});
logger.info(`📡 Checking for app updates`);
const updater = new AppUpdater();
updater.start();
registerFileProtocol("static", __static);
await installDeveloperTools();
@@ -133,6 +128,7 @@ app.on("ready", async () => {
logger.info("🖥️ Starting WindowManager");
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
windowManager.whenLoaded.then(() => startUpdateChecking());
logger.info("🧩 Initializing extensions");
+4 -12
View File
@@ -1,9 +1,9 @@
import path from "path";
import packageInfo from "../../package.json";
import { dialog, Menu, NativeImage, Tray } from "electron";
import { Menu, NativeImage, Tray } from "electron";
import { autorun } from "mobx";
import { showAbout } from "./menu";
import { AppUpdater } from "./app-updater";
import { checkForUpdates } from "./app-updater";
import { WindowManager } from "./window-manager";
import { clusterStore } from "../common/cluster-store";
import { workspaceStore } from "../common/workspace-store";
@@ -102,16 +102,8 @@ function createTrayMenu(windowManager: WindowManager): Menu {
{
label: "Check for updates",
async click() {
const result = await AppUpdater.checkForUpdates();
if (!result) {
const browserWindow = await windowManager.ensureMainWindow();
dialog.showMessageBoxSync(browserWindow, {
message: "No updates available",
type: "info",
});
}
await checkForUpdates();
await windowManager.ensureMainWindow();
},
},
{
+5 -1
View File
@@ -1,5 +1,5 @@
import type { ClusterId } from "../common/cluster-store";
import { observable } from "mobx";
import { observable, when } from "mobx";
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus";
@@ -16,6 +16,9 @@ export class WindowManager extends Singleton {
protected windowState: windowStateKeeper.State;
protected disposers: Record<string, Function> = {};
@observable mainViewInitiallyLoaded = false;
whenLoaded = when(() => this.mainViewInitiallyLoaded);
@observable activeClusterId: ClusterId;
constructor(protected proxyPort: number) {
@@ -101,6 +104,7 @@ export class WindowManager extends Singleton {
setTimeout(() => {
appEventBus.emit({ name: "app", action: "start" });
}, 1000);
this.mainViewInitiallyLoaded = true;
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString());
}