feat(desktop): make updates persistent and responsive (#31191)
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import { app, dialog } from "electron"
|
||||
import pkg from "electron-updater"
|
||||
import { UPDATER_ENABLED } from "./constants"
|
||||
import { createUpdaterController, type UpdaterReadyRecord } from "./updater-controller"
|
||||
import { getLogger } from "./logging"
|
||||
import { getStore } from "./store"
|
||||
|
||||
const { autoUpdater } = pkg
|
||||
type UpdateCheckResult = { updateAvailable: boolean; version?: string; failed?: boolean }
|
||||
let downloadedVersion: string | undefined
|
||||
let pendingCheck: Promise<UpdateCheckResult> | undefined
|
||||
const key = "ready"
|
||||
|
||||
export function setupAutoUpdater() {
|
||||
if (!UPDATER_ENABLED) return
|
||||
export function setupAutoUpdater(stop: () => Promise<void>) {
|
||||
const logger = getLogger()
|
||||
autoUpdater.logger = logger
|
||||
autoUpdater.channel = "latest"
|
||||
@@ -23,110 +22,50 @@ export function setupAutoUpdater() {
|
||||
allowDowngrade: autoUpdater.allowDowngrade,
|
||||
currentVersion: app.getVersion(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkUpdate(): Promise<UpdateCheckResult> {
|
||||
if (!UPDATER_ENABLED) return { updateAvailable: false }
|
||||
if (downloadedVersion) return { updateAvailable: true, version: downloadedVersion }
|
||||
if (pendingCheck) return pendingCheck
|
||||
|
||||
pendingCheck = checkAndDownloadUpdate().finally(() => {
|
||||
pendingCheck = undefined
|
||||
})
|
||||
return pendingCheck
|
||||
}
|
||||
|
||||
async function checkAndDownloadUpdate(): Promise<UpdateCheckResult> {
|
||||
const logger = getLogger()
|
||||
logger.log("checking for updates", {
|
||||
const store = getStore("opencode.updater")
|
||||
return createUpdaterController({
|
||||
enabled: UPDATER_ENABLED,
|
||||
currentVersion: app.getVersion(),
|
||||
channel: autoUpdater.channel,
|
||||
allowPrerelease: autoUpdater.allowPrerelease,
|
||||
allowDowngrade: autoUpdater.allowDowngrade,
|
||||
backend: autoUpdater,
|
||||
persistence: {
|
||||
get() {
|
||||
const value = store.get(key)
|
||||
if (!value || typeof value !== "object" || !("version" in value) || typeof value.version !== "string") return
|
||||
return { version: value.version } satisfies UpdaterReadyRecord
|
||||
},
|
||||
set: (value) => store.set(key, value),
|
||||
clear: () => store.delete(key),
|
||||
},
|
||||
stop,
|
||||
log: (message, data) => logger.log(message, data),
|
||||
})
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
const updateInfo = result?.updateInfo
|
||||
logger.log("update metadata fetched", {
|
||||
releaseVersion: updateInfo?.version ?? null,
|
||||
releaseDate: updateInfo?.releaseDate ?? null,
|
||||
releaseName: updateInfo?.releaseName ?? null,
|
||||
files: updateInfo?.files?.map((file) => file.url) ?? [],
|
||||
})
|
||||
const version = result?.updateInfo?.version
|
||||
if (result?.isUpdateAvailable === false || !version) {
|
||||
logger.log("no update available", {
|
||||
reason: "provider returned no newer version",
|
||||
})
|
||||
return { updateAvailable: false }
|
||||
}
|
||||
logger.log("update available", { version })
|
||||
await autoUpdater.downloadUpdate()
|
||||
downloadedVersion = version
|
||||
logger.log("update download completed", { version })
|
||||
return { updateAvailable: true, version }
|
||||
} catch (error) {
|
||||
logger.error("update check failed", error)
|
||||
return { updateAvailable: false, failed: true }
|
||||
}
|
||||
}
|
||||
|
||||
export async function installUpdate(killSidecar: () => Promise<void>) {
|
||||
const result = downloadedVersion ? { updateAvailable: true, version: downloadedVersion } : await checkUpdate()
|
||||
const logger = getLogger()
|
||||
if (!result.updateAvailable || !downloadedVersion) {
|
||||
logger.log("install update skipped", {
|
||||
reason: result.failed ? "update check failed" : "no update available",
|
||||
})
|
||||
return
|
||||
}
|
||||
logger.log("installing downloaded update", {
|
||||
version: result.version ?? null,
|
||||
})
|
||||
await killSidecar()
|
||||
autoUpdater.quitAndInstall()
|
||||
}
|
||||
|
||||
export async function checkForUpdates(alertOnFail: boolean, killSidecar: () => Promise<void>) {
|
||||
if (!UPDATER_ENABLED) return
|
||||
const logger = getLogger()
|
||||
logger.log("checkForUpdates invoked", { alertOnFail })
|
||||
const result = await checkUpdate()
|
||||
if (!result.updateAvailable) {
|
||||
if (result.failed) {
|
||||
logger.log("no update decision", { reason: "update check failed" })
|
||||
if (!alertOnFail) return
|
||||
await dialog.showMessageBox({
|
||||
type: "error",
|
||||
message: "Update check failed.",
|
||||
title: "Update Error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.log("no update decision", { reason: "already up to date" })
|
||||
export async function showUpdaterDialog(
|
||||
controller: ReturnType<typeof setupAutoUpdater>,
|
||||
alertOnFail: boolean,
|
||||
) {
|
||||
const state = await controller.check()
|
||||
if (state.status === "error") {
|
||||
if (!alertOnFail) return
|
||||
await dialog.showMessageBox({
|
||||
type: "info",
|
||||
message: "You're up to date.",
|
||||
title: "No Updates",
|
||||
})
|
||||
await dialog.showMessageBox({ type: "error", message: "Update check failed.", title: "Update Error" })
|
||||
return
|
||||
}
|
||||
if (state.status === "up-to-date") {
|
||||
if (!alertOnFail) return
|
||||
await dialog.showMessageBox({ type: "info", message: "You're up to date.", title: "No Updates" })
|
||||
return
|
||||
}
|
||||
if (state.status !== "ready") return
|
||||
|
||||
const response = await dialog.showMessageBox({
|
||||
type: "info",
|
||||
message: `Update ${result.version ?? ""} downloaded. Restart now?`,
|
||||
message: `Update ${state.version} downloaded. Restart now?`,
|
||||
title: "Update Ready",
|
||||
buttons: ["Restart", "Later"],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
})
|
||||
logger.log("update prompt response", {
|
||||
version: result.version ?? null,
|
||||
restartNow: response.response === 0,
|
||||
})
|
||||
if (response.response === 0) {
|
||||
await installUpdate(killSidecar)
|
||||
}
|
||||
if (response.response === 0) await controller.install()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user