Skip to content

Commit

Permalink
Auto Update Modes (#2866)
Browse files Browse the repository at this point in the history
Manual
Startup
Startup and daily
  • Loading branch information
jameskerr authored Nov 16, 2023
1 parent abe0594 commit f8a727d
Show file tree
Hide file tree
Showing 68 changed files with 701 additions and 5,917 deletions.
4 changes: 4 additions & 0 deletions apps/zui/dev-app-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
owner: brimdata
repo: zui
provider: github
updaterCacheDirName: zui-dev-updater
19 changes: 19 additions & 0 deletions apps/zui/pages/update.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, {useEffect, useState} from "react"
import {AppProvider} from "src/app/core/context"
import initialize from "src/js/initializers/initialize"
import {UpdateWindow} from "src/views/update-window"

export default function Update() {
const [app, setApp] = useState(null)

useEffect(() => {
initialize().then((app) => setApp(app))
}, [])

if (!app) return null
return (
<AppProvider store={app.store} api={app.api}>
<UpdateWindow />
</AppProvider>
)
}
54 changes: 54 additions & 0 deletions apps/zui/public/zui-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/zui/src/app/core/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {app as electronApp} from "electron"
import pkg from "src/electron/pkg"

const isPackaged = () =>
electronApp.isPackaged || process.env.NODE_ENV === "production"
Expand Down Expand Up @@ -31,4 +32,7 @@ export default {
get isLinux() {
return process.platform === "linux"
},
get isInsiders() {
return pkg.name === "zui-insiders"
},
}
2 changes: 1 addition & 1 deletion apps/zui/src/app/core/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const fetchStatusCode = async (link: string): Promise<[string, number]> => {
const controller = new AbortController()
const timeout = setTimeout(() => {
controller.abort()
}, 1000)
}, 10_000)

try {
const resp = await fetch(link, {signal: controller.signal})
Expand Down
2 changes: 2 additions & 0 deletions apps/zui/src/components/forms.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
color: white;
font-weight: 500;
min-width: 80px;
user-select: none;
}

.form .submit:hover:not(:disabled) {
Expand All @@ -110,6 +111,7 @@
box-shadow: 0 3px 4px -4px var(--foreground-color-light);
font-weight: 500;
min-width: 80px;
user-select: none;
}

.form .button:hover:not(:disabled) {
Expand Down
5 changes: 5 additions & 0 deletions apps/zui/src/core/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import electronLog from "electron-log"

export const info = electronLog.info
export const debug = electronLog.debug
export const error = electronLog.error
2 changes: 0 additions & 2 deletions apps/zui/src/core/main/main-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ export class MainObject {
return this.store.dispatch as AppDispatch
}

select

getPath(name: PathName) {
return getPath(name)
}
Expand Down
22 changes: 22 additions & 0 deletions apps/zui/src/core/on-state-change.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Selector} from "@reduxjs/toolkit"
import {Store} from "src/js/state/types"

export function onStateChange(
store: Store,
selector: Selector,
onChange: (value: any) => void
) {
let current = undefined

function listener() {
const next = selector(store.getState())
if (next !== current) {
current = next
onChange(current)
}
}

const unsubscribe = store.subscribe(listener)
listener()
return unsubscribe
}
4 changes: 4 additions & 0 deletions apps/zui/src/core/operations.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {ipcMain, IpcMainInvokeEvent} from "electron"
import {OperationName} from "src/domain/messages"
import {MainObject} from "./main/main-object"
import {Dispatch} from "src/js/state/types"
import {select} from "./main/select"

type OperationContext = {
dispatch: Dispatch
main: MainObject
event?: IpcMainInvokeEvent | null
select: typeof select
}

let context: OperationContext | null = null
Expand Down
3 changes: 3 additions & 0 deletions apps/zui/src/domain/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {WindowHandlers, WindowOperations} from "./window/messages"
import {LegacyOperations} from "./legacy-ops/messages"
import {E2EOperations} from "./e2e/messages"
import {EnvOperations} from "./env/messages"
import {UpdatesOperations} from "./updates/messages"
import {LoadsHandlers, LoadsOperations} from "./loads/messages"

export type Handlers = ResultsHandlers &
Expand All @@ -22,6 +23,8 @@ export type Operations = PoolsOperations &
E2EOperations &
ResultsOperations &
EnvOperations &
WindowOperations &
UpdatesOperations &
LoadsOperations &
WindowOperations

Expand Down
Empty file.
45 changes: 45 additions & 0 deletions apps/zui/src/domain/updates/linux-updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {app, shell} from "electron"
import fetch from "node-fetch"
import semver from "semver"
import env from "src/app/core/env"
import links from "src/app/core/links"
import pkg from "src/electron/pkg"
import {Updater} from "./types"
import {getMainObject} from "src/core/main"

export class LinuxUpdater implements Updater {
async check() {
const latest = await this.latest()
const current = app.getVersion()
if (semver.lt(current, latest)) {
return latest
} else {
return null
}
}

async install() {
shell.openExternal(this.downloadUrl())
}

private async latest() {
const resp = await fetch(this.latestUrl())
if (resp.status === 204) return app.getVersion()
const data = await resp.json()
return data.name
}

private latestUrl() {
const repo = getMainObject().appMeta.repo
const platform = "darwin-x64" // If the mac version exists, the linux does too
return `https://update.electronjs.org/${repo}/${platform}/${app.getVersion()}`
}

private downloadUrl() {
if (env.isInsiders) {
return pkg.repository + "/releases/latest"
} else {
return links.ZUI_DOWNLOAD
}
}
}
43 changes: 43 additions & 0 deletions apps/zui/src/domain/updates/mac-win-updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {autoUpdater} from "electron-updater"
import {Updater} from "./types"
import semver from "semver"
import {app} from "electron"
import {getMainObject} from "src/core/main"

autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false

export class MacWinUpdater implements Updater {
async check() {
const {updateInfo} = await autoUpdater.checkForUpdates()
const latest = updateInfo.version
const current = app.getVersion()
if (semver.lt(current, latest)) {
return latest
} else {
return null
}
}

async install(onProgress) {
const progress = (r) => {
onProgress(r.percent / 100)
}
autoUpdater.on("error", (e) => {
throw e
})
autoUpdater.on("download-progress", progress)

return new Promise((resolve, reject) => {
autoUpdater.on("update-downloaded", resolve)
autoUpdater.on("error", reject)
autoUpdater.downloadUpdate()
}).then(() => {
// `autoUpdater.quitAndInstall()` will close all application windows first and only emit `before-quit` event on `app` after that.
// We have some logic when closing windows that checks to see if we are quitting or not.
// So we call onBeforeQuit manually here to tell the main object we are quitting
getMainObject().onBeforeQuit()
autoUpdater.quitAndInstall()
})
}
}
7 changes: 7 additions & 0 deletions apps/zui/src/domain/updates/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as operations from "./operations"

export type UpdatesOperations = {
"updates.open": typeof operations.open
"updates.check": typeof operations.check
"updates.install": typeof operations.install
}
50 changes: 50 additions & 0 deletions apps/zui/src/domain/updates/operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {createOperation} from "src/core/operations"
import {updater} from "./updater"
import Updates from "src/js/state/Updates"
import {errorToString} from "src/util/error-to-string"
import {info} from "src/core/log"

export const open = createOperation("updates.open", ({main}) => {
check()
main.windows.activate("update")
})

export const check = createOperation(
"updates.check",
async ({main, dispatch}) => {
try {
info("Checking for Updates...")
dispatch(Updates.setIsChecking(true))
const newVersion = await updater.check()
if (newVersion) {
info("New Version Found: " + newVersion)
dispatch(Updates.setNextVersion(newVersion))
main.windows.activate("update")
}
} catch (e) {
info("Error Checking for Update: " + errorToString(e))
dispatch(Updates.setError(errorToString(e)))
} finally {
dispatch(Updates.setIsChecking(false))
}
}
)

export const install = createOperation(
"updates.install",
async ({dispatch, main}) => {
info("Installing Update")
const onProgress = (n: number) => dispatch(Updates.setDownloadProgress(n))
try {
dispatch(Updates.setIsDownloading(true))
dispatch(Updates.setDownloadProgress(0))
await updater.install(onProgress)
main.windows.byName("update").forEach((w) => w.close())
} catch (e) {
info("Error Installing")
dispatch(Updates.setError(errorToString(e)))
} finally {
dispatch(Updates.setIsDownloading(false))
}
}
)
34 changes: 34 additions & 0 deletions apps/zui/src/domain/updates/scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {UpdateMode} from "./types"

export class Scheduler {
static interval = 1000 * 60 * 60 * 24 // 1 day

start(mode: UpdateMode, check: () => any, args: {delay?: number} = {}) {
switch (mode) {
case "default":
this.delay(check, args.delay)
this.schedule(check)
break
case "startup":
this.delay(check, args.delay)
}
}

private delayedId: any
private delay(check, ms = 0) {
this.delayedId = setTimeout(check, ms)
}

private scheduleId: any
private schedule(check: () => any) {
this.scheduleId = setTimeout(() => {
check()
this.schedule(check)
}, Scheduler.interval)
}

stop() {
clearTimeout(this.delayedId)
clearTimeout(this.scheduleId)
}
}
6 changes: 6 additions & 0 deletions apps/zui/src/domain/updates/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type UpdateMode = "manual" | "startup" | "default"

export interface Updater {
check(): Promise<string | null>
install(onProgress: (percent: number) => void): Promise<void>
}
5 changes: 5 additions & 0 deletions apps/zui/src/domain/updates/updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import env from "src/app/core/env"
import {LinuxUpdater} from "./linux-updater"
import {MacWinUpdater} from "./mac-win-updater"

export const updater = env.isLinux ? new LinuxUpdater() : new MacWinUpdater()
Loading

0 comments on commit f8a727d

Please sign in to comment.