1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Add auto-update and pre-release update user settings

- Add settings in user preferences for auto-updating (default false) and
  for allowing pre-release versions (default false)

- Use in-Lens notifications instead of OS notifications as those were
  found to be flaky

- Add rudimentary main->renderer notification system.

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-12-18 15:28:08 -05:00
parent 1a4b6cddb5
commit cd154a8343
14 changed files with 335 additions and 30 deletions

View File

@ -162,6 +162,7 @@
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.12.0",
"@types/crypto-js": "^3.1.47",
"@types/dateformat": "^3.0.1",
"@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
@ -171,6 +172,7 @@
"@types/marked": "^0.7.4",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.12.45",
"@types/node-notifier": "^8.0.0",
"@types/proper-lockfile": "^4.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.3",
@ -178,6 +180,7 @@
"chalk": "^4.1.0",
"conf": "^7.0.1",
"crypto-js": "^4.0.0",
"dateformat": "^4.3.1",
"electron-updater": "^4.3.1",
"electron-window-state": "^5.0.3",
"file-type": "^14.7.1",
@ -196,6 +199,7 @@
"mobx-observable-history": "^1.0.3",
"mock-fs": "^4.12.0",
"node-machine-id": "^1.1.12",
"node-notifier": "^9.0.0",
"node-pty": "^0.9.0",
"openid-client": "^3.15.2",
"path-to-regexp": "^6.1.0",

View File

@ -5,6 +5,9 @@
import { ipcMain, ipcRenderer, WebContents, webContents } from "electron"
import logger from "../main/logger";
export const NotificationChannelPrefix: IpcChannel = "notications:";
export const NotificationChannelAdd: IpcChannel = `${NotificationChannelPrefix}add`;
export type IpcChannel = string;
export interface IpcChannelOptions {
@ -50,7 +53,7 @@ export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, c
return ipcChannel;
}
export interface IpcBroadcastParams<A extends any[] = any> {
export interface IpcBroadcastParams<A extends any[] = any[]> {
channel: IpcChannel
webContentId?: number; // send to single webContents view
frameId?: number; // send to inner frame of webContents

View File

@ -2,7 +2,7 @@ import type { ThemeId } from "../renderer/theme.store";
import { app, remote } from 'electron';
import semver from "semver"
import { readFile } from "fs-extra"
import { action, observable, reaction, toJS } from "mobx";
import { action, IReactionDisposer, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store"
import { getAppVersion } from "./utils/app-version";
@ -27,6 +27,8 @@ export interface UserPreferences {
downloadKubectlBinaries?: boolean;
downloadBinariesPath?: string;
kubectlBinariesPath?: string;
allowAutoUpdates?: boolean;
allowPrereleaseVersions?: boolean;
}
export class UserStore extends BaseStore<UserStoreModel> {
@ -59,6 +61,8 @@ export class UserStore extends BaseStore<UserStoreModel> {
colorTheme: UserStore.defaultTheme,
downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
allowAutoUpdates: false,
allowPrereleaseVersions: false,
};
get isNewVersion() {

View File

@ -1,19 +1,188 @@
import { autoUpdater } from "electron-updater"
import logger from "./logger"
import { autoUpdater, UpdateInfo } from "electron-updater";
import { autorun } from "mobx";
import { userStore } from "../common/user-store";
import logger from "./logger";
import dateFormat from "dateformat";
import { broadcastIpc, IpcChannel, NotificationChannelAdd, NotificationChannelPrefix } from "../common/ipc";
import { ipcMain } from "electron";
import { isDevelopment } from "../common/vars";
import { SemVer } from "semver";
export default class AppUpdater {
function delay(duration: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, duration));
}
protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day
class NotificationBackchannel {
private static _id = 0;
constructor() {
autoUpdater.logger = logger
}
public start() {
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify()
}, this.updateInterval)
return autoUpdater.checkForUpdatesAndNotify()
static nextId(): IpcChannel {
return `${NotificationChannelPrefix}${NotificationBackchannel._id++}`
}
}
const title = "Lens Updater";
async function autoUpdateNow(): Promise<void> {
const body = "Downloading and installing update.";
broadcastIpc({
channel: NotificationChannelAdd,
args: [{
title,
body,
status: "info",
timeout: 5000,
}]
})
logger.info("[UPDATE CHECKER]: update downloaded started");
await autoUpdater.downloadUpdate();
logger.info("[UPDATE CHECKER]: update downloadeded");
autoUpdater.quitAndInstall();
}
async function autoUpdateCheck(args: UpdateInfo): Promise<void> {
return new Promise(async resolve => {
const body = "Install and restart Lens?";
const yesNowChannel = NotificationBackchannel.nextId();
const yesLaterChannel = NotificationBackchannel.nextId();
const noChannel = NotificationBackchannel.nextId();
function cleanupChannels() {
ipcMain.removeAllListeners(yesNowChannel);
ipcMain.removeAllListeners(yesLaterChannel);
ipcMain.removeAllListeners(noChannel);
}
ipcMain
.on(yesNowChannel, async () => {
cleanupChannels();
await autoUpdater.downloadUpdate();
autoUpdater.quitAndInstall();
resolve();
})
.on(yesLaterChannel, async () => {
cleanupChannels();
await autoUpdater.downloadUpdate();
autoUpdater.autoInstallOnAppQuit = true;
resolve();
})
.on(noChannel, () => {
cleanupChannels();
resolve();
});
broadcastIpc({
channel: NotificationChannelAdd,
args: [{
title,
body,
status: "info",
buttons: [
{
label: "Yes, now",
backchannel: yesNowChannel,
style: {
background: "green",
marginRight: "10px"
}
},
{
label: "Yes, later",
backchannel: yesLaterChannel,
style: {
background: "green",
marginRight: "10px"
}
},
{
label: "No",
backchannel: noChannel,
accent: true
}
],
closeChannel: noChannel,
}]
});
});
}
/**
* starts the automatic update checking
* @param interval milliseconds between interval to check on, defaulkts to 24h
*/
export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
if (isDevelopment) {
return;
}
autoUpdater.logger = logger;
autoUpdater.autoInstallOnAppQuit = false;
/**
* GC saftey: This function's lifetime is the lifetime of the application.
* So no need to call the disposer.
*/
autorun(() => {
autoUpdater.autoDownload = userStore.preferences.allowAutoUpdates;
autoUpdater.allowPrerelease = userStore.preferences.allowPrereleaseVersions;
});
autoUpdater
.on("update-available", async (args: UpdateInfo) => {
try {
const releaseDate = new Date(args.releaseDate);
const body = `Version ${args.version} was release on ${dateFormat(releaseDate, "dddd, mmmm dS, yyyy")}.`;
broadcastIpc({
channel: NotificationChannelAdd,
args: [{
title,
body,
status: "info",
timeout: 5000,
}]
});
const version = new SemVer(args.version);
if (userStore.preferences.allowAutoUpdates && version.prerelease !== null) {
// don't auto update to pre-release versions.
await autoUpdateNow();
} else {
await autoUpdateCheck(args);
}
} catch (error) {
logger.error("[UPDATE CHECKER]: notification failed", { error: String(error) })
}
})
.on("update-not-available", () => {
try {
const stream = userStore.preferences.allowPrereleaseVersions ? "prerelease" : "stable";
const body = `Lens is running the latest ${stream} version.`;
broadcastIpc({
channel: NotificationChannelAdd,
args: [{
title,
body,
status: "info",
timeout: 5000,
}]
})
} catch (error) {
logger.error("[UPDATE CHECKER]: notification failed", { error: String(error) })
}
})
async function helper() {
while (true) {
await autoUpdater.checkForUpdates();
await delay(interval);
}
}
helper()
.catch(error => logger.error("[UPDATE CHECKER]: failed with an error", { error: String(error) }));
}

View File

@ -8,7 +8,7 @@ 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 { startUpdateChecking } from "./app-updater"
import { shellSync } from "./shell-sync"
import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env"
@ -39,8 +39,6 @@ async function main() {
logger.info(`🚀 Starting Lens from "${workingDir}"`)
tracker.event("app", "start");
const updater = new AppUpdater()
updater.start();
registerFileProtocol("static", __static);
@ -75,6 +73,13 @@ async function main() {
// create window manager and open app
windowManager = new WindowManager(proxyPort);
/**
* This depends on:
* 1. userStore: it reads the user's auto update settings
* 2. windowManager: it will send IPC to the main window for notifications
*/
startUpdateChecking();
}
app.on("ready", main);

View File

@ -94,7 +94,7 @@ export class KubeAuthProxy {
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
const channel = `kube-auth:${this.cluster.id}`
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastIpc({ channel: channel, args: [res] });
broadcastIpc({ channel, args: [res] });
}
public exit() {

View File

@ -73,6 +73,10 @@ export class WindowManager {
}
}
send(channel: string, ...data: any[]) {
this.mainView.webContents.send(channel, ...data);
}
async showMain() {
try {
await this.showSplash();

View File

@ -9,6 +9,7 @@ import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store";
import { App } from "./components/app";
import { LensApp } from "./lens-app";
import { notificationsStore } from "./components/notifications/notifications.store";
type AppComponent = React.ComponentType & {
init?(): Promise<void>;
@ -30,6 +31,10 @@ export async function bootstrap(App: AppComponent) {
// Register additional store listeners
clusterStore.registerIpcListener();
if (process.isMainFrame) {
notificationsStore.registerIpcListener();
}
// init app's dependencies if any
if (App.init) {
await App.init();

View File

@ -194,6 +194,24 @@ export class Preferences extends React.Component {
<small className="hint">
<Trans>Telemetry & usage data is collected to continuously improve the Lens experience.</Trans>
</small>
<h2><Trans>Updates</Trans></h2>
<Checkbox
label={<Trans>Allow auto updates</Trans>}
value={preferences.allowAutoUpdates}
onChange={v => preferences.allowAutoUpdates = v}
/>
<small className="hint">
<Trans>Allow Lens to auto update itself to the latest version. Lens checks on startup and then once a day.</Trans>
</small>
<Checkbox
label={<Trans>Allow pre-release versions of Lens</Trans>}
value={preferences.allowPrereleaseVersions}
onChange={v => preferences.allowPrereleaseVersions = v}
/>
<small className="hint">
<Trans>Allow upgrading Lens to pre-release versions. This means that the update checker will ask about pre release versions but won't auto upgrade to them.</Trans>
</small>
</WizardLayout>
</div>
);

View File

@ -4,6 +4,10 @@ import { autobind } from "../../utils";
import isObject from "lodash/isObject"
import uniqueId from "lodash/uniqueId";
import { JsonApiErrorParsed } from "../../api/json-api";
import logger from "../../../main/logger";
import { ipcRenderer } from "electron";
import { IpcChannel, NotificationChannelAdd } from "../../../common/ipc";
import { Button, ButtonProps } from "../button";
export type IMessageId = string | number;
export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
@ -19,6 +23,39 @@ export interface INotification {
message: IMessage;
status?: NotificationStatus;
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
onClose?(): void; // additonal logic on when the notification times out or is closed by the "x"
}
export interface MainNotification {
title: string;
body: string;
buttons?: ({
backchannel: IpcChannel;
} & ButtonProps)[];
status: NotificationStatus;
timeout?: number;
closeChannel?: IpcChannel;
}
function renderButtons(id: IpcChannel, buttons?: MainNotification["buttons"]): React.ReactNode {
if (!buttons) {
return null;
}
return (
<>
<br />
<div className="flex row align-right box grow">
{buttons.map(({ backchannel, ...props}) => (
<Button {...props} onClick={() => {
console.log(`sending to ${backchannel}`)
ipcRenderer.send(backchannel);
notificationsStore.remove(id);
}} />
))}
</div>
</>
)
}
@autobind()
@ -27,6 +64,29 @@ export class NotificationsStore {
protected autoHideTimers = new Map<IMessageId, number>();
registerIpcListener(): void {
logger.info(`[NOTIFICATION-STORE] start to listen for notifications requests from main`);
ipcRenderer.on(NotificationChannelAdd, (event, model: MainNotification) => {
console.log(model);
const id = uniqueId("notification_");
this.add({
message: (
<>
<b>{model.title}</b>
<p>{model.body}</p>
{renderButtons(id, model.buttons)}
</>
),
id,
status: model.status,
timeout: model.timeout,
onClose: () => {
model.closeChannel && ipcRenderer.send(model.closeChannel);
}
});
})
}
addAutoHideTimer(notification: INotification) {
this.removeAutoHideTimer(notification);
const { id, timeout } = notification;

View File

@ -84,7 +84,10 @@ export class Notifications extends React.Component {
<div className="box center">
<Icon
material="close" className="close"
onClick={prevDefault(() => remove(notification))}
onClick={prevDefault(() => {
remove(notification);
notification.onClose?.();
})}
/>
</div>
</div>

View File

@ -29,12 +29,8 @@ export class ThemeStore {
{ id: "kontena-light", type: ThemeType.LIGHT },
];
@computed get activeThemeId() {
return userStore.preferences.colorTheme;
}
@computed get activeTheme(): Theme {
const activeTheme = this.themes.find(theme => theme.id === this.activeThemeId) || this.themes[0];
const activeTheme = this.themes.find(theme => theme.id === userStore.preferences.colorTheme) || this.themes[0];
return {
colors: {},
...activeTheme,
@ -43,9 +39,9 @@ export class ThemeStore {
constructor() {
// auto-apply active theme
reaction(() => this.activeThemeId, async themeId => {
reaction(() => this.activeTheme, async ({ id }) => {
try {
await this.loadTheme(themeId);
await this.loadTheme(id);
this.applyTheme();
} catch (err) {
userStore.resetTheme();

View File

@ -159,4 +159,4 @@ export default function (): webpack.Configuration {
}),
],
}
}
}

View File

@ -1723,6 +1723,11 @@
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-3.1.47.tgz#36e549dd3f1322742a3a738e7c113ebe48221860"
integrity sha512-eI6gvpcGHLk3dAuHYnRCAjX+41gMv1nz/VP55wAe5HtmAKDOoPSfr3f6vkMc08ov1S0NsjvUBxDtHHxqQY1LGA==
"@types/dateformat@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
"@types/debug@^4.1.5":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
@ -1966,6 +1971,13 @@
dependencies:
"@types/node" "*"
"@types/node-notifier@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@types/node-notifier/-/node-notifier-8.0.0.tgz#51100d67155ed1500a8aaa633987109f59a0637d"
integrity sha512-CseIDQOC/I+yvj/4ItpG4ATcwooQlGPDDJweII8nspjjZg4ZBuvkyHg9P81QkElgU9FpYlb5A27BRggD3idTCQ==
dependencies:
"@types/node" "*"
"@types/node@*":
version "14.0.11"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3"
@ -4299,6 +4311,11 @@ date-fns@^2.0.1, date-fns@^2.14.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.14.0.tgz#359a87a265bb34ef2e38f93ecf63ac453f9bc7ba"
integrity sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw==
dateformat@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.3.1.tgz#e010ca5915f0c7d47e5b4e4287dd5ecb41125a96"
integrity sha512-xhq1wI5BQ0TMJDvio0BLP8lNeYlhAvmh/7H52H9n6kfzqSmRpIhH5KEIjJ7onFEAh5CQVrAP2MAG8wZ6j0BKzQ==
debounce-fn@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/debounce-fn/-/debounce-fn-4.0.0.tgz#ed76d206d8a50e60de0dd66d494d82835ffe61c7"
@ -6760,7 +6777,7 @@ is-wsl@^1.1.0:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
is-wsl@^2.1.1:
is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
@ -8419,6 +8436,18 @@ node-notifier@^7.0.0:
uuid "^7.0.3"
which "^2.0.2"
node-notifier@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.0.tgz#46c5bbecbb796d4a803f646cea5bc91403f2ff38"
integrity sha512-SkwNwGnMMlSPrcoeH4CSo9XyWe72acAHEJGDdPdB+CyBVHsIYaTQ4U/1wk3URsyzC75xZLg2vzU2YaALlqDF1Q==
dependencies:
growly "^1.3.0"
is-wsl "^2.2.0"
semver "^7.3.2"
shellwords "^0.1.1"
uuid "^8.3.0"
which "^2.0.2"
node-pty@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.9.0.tgz#8f9bcc0d1c5b970a3184ffd533d862c7eb6590a6"
@ -11758,6 +11787,11 @@ uuid@^8.1.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
uuid@^8.3.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"