mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
feat(updater): add update status summary, logging, and new IPC channels
- Add UpdateStatusSummary type and getUpdateStatusSummary() for exposing updater state to renderer processes - Add writeUpdaterLog() for persistent updater diagnostics - Add configureUpdateFeed() for feed URL configuration - Wire up get-update-status-summary and check-for-app-updates IPC handlers - Add primaryAction field to UpdateToastState for actionable toast buttons - Add checking-for-update event handler with status tracking - Surface automatic check errors via toast when appropriate
This commit is contained in:
Vendored
+10
@@ -50,6 +50,14 @@ interface UpdateToastState {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
}
|
||||
|
||||
interface UpdateStatusSummary {
|
||||
status: "idle" | "checking" | "up-to-date" | "available" | "downloading" | "ready" | "error";
|
||||
currentVersion: string;
|
||||
availableVersion: string | null;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
@@ -303,7 +311,9 @@ interface Window {
|
||||
dismissUpdateToast: () => Promise<{ success: boolean }>;
|
||||
skipUpdateVersion: () => Promise<{ success: boolean; message?: string }>;
|
||||
getCurrentUpdateToastPayload: () => Promise<UpdateToastState | null>;
|
||||
getUpdateStatusSummary: () => Promise<UpdateStatusSummary>;
|
||||
previewUpdateToast: () => Promise<{ success: boolean }>;
|
||||
checkForAppUpdates: () => Promise<{ success: boolean; logPath: string }>;
|
||||
onUpdateToastStateChanged: (callback: (payload: UpdateToastState | null) => void) => () => void;
|
||||
onUpdateReadyToast: (callback: (payload: {
|
||||
version: string;
|
||||
|
||||
@@ -265,9 +265,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getCurrentUpdateToastPayload: () => {
|
||||
return ipcRenderer.invoke("get-current-update-toast-payload");
|
||||
},
|
||||
getUpdateStatusSummary: () => {
|
||||
return ipcRenderer.invoke("get-update-status-summary");
|
||||
},
|
||||
previewUpdateToast: () => {
|
||||
return ipcRenderer.invoke("preview-update-toast");
|
||||
},
|
||||
checkForAppUpdates: () => {
|
||||
return ipcRenderer.invoke("check-for-app-updates");
|
||||
},
|
||||
onUpdateToastStateChanged: (
|
||||
callback: (payload: {
|
||||
version: string;
|
||||
@@ -276,6 +282,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
} | null) => void,
|
||||
) => {
|
||||
const listener = (
|
||||
@@ -288,6 +295,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
}
|
||||
| null,
|
||||
) => callback(payload);
|
||||
|
||||
+180
-1
@@ -1,17 +1,41 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { app, BrowserWindow, dialog } from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import type { MessageBoxOptions, MessageBoxReturnValue } from "electron";
|
||||
import { USER_DATA_PATH } from "./appPaths";
|
||||
|
||||
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||
export const UPDATE_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000;
|
||||
const DISMISSED_READY_REMINDER_DELAY_MS = 5 * 60 * 1000;
|
||||
const AUTO_UPDATES_DISABLED = process.env.RECORDLY_DISABLE_AUTO_UPDATES === "1";
|
||||
const AUTO_UPDATE_ERROR_TOASTS_DISABLED =
|
||||
process.env.RECORDLY_DISABLE_AUTO_UPDATE_ERROR_TOASTS === "1";
|
||||
const UPDATE_FEED_URL_OVERRIDE = process.env.RECORDLY_UPDATE_FEED_URL?.trim() ?? "";
|
||||
const UPDATER_LOG_PATH =
|
||||
process.env.RECORDLY_UPDATER_LOG_PATH?.trim() || path.join(USER_DATA_PATH, "updater.log");
|
||||
const DEV_UPDATE_PREVIEW_VERSION = "9.9.9";
|
||||
const DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS = 300;
|
||||
const DEV_UPDATE_PREVIEW_PROGRESS_INCREMENT = 20;
|
||||
|
||||
export type UpdateToastPhase = "available" | "downloading" | "ready" | "error";
|
||||
|
||||
export type UpdateStatusKind =
|
||||
| "idle"
|
||||
| "checking"
|
||||
| "up-to-date"
|
||||
| "available"
|
||||
| "downloading"
|
||||
| "ready"
|
||||
| "error";
|
||||
|
||||
export interface UpdateStatusSummary {
|
||||
status: UpdateStatusKind;
|
||||
currentVersion: string;
|
||||
availableVersion: string | null;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface UpdateToastPayload {
|
||||
version: string;
|
||||
detail: string;
|
||||
@@ -19,6 +43,7 @@ export interface UpdateToastPayload {
|
||||
delayMs: number;
|
||||
isPreview?: boolean;
|
||||
progressPercent?: number;
|
||||
primaryAction?: "download-update" | "install-update" | "retry-check";
|
||||
}
|
||||
|
||||
type UpdateToastSender = (
|
||||
@@ -38,6 +63,72 @@ let pendingDownloadedVersion: string | null = null;
|
||||
let downloadInProgress = false;
|
||||
let downloadToastDismissed = false;
|
||||
let skippedVersion: string | null = null;
|
||||
let updateCheckErrorHandled = false;
|
||||
let activeUpdateToastSender: UpdateToastSender | undefined;
|
||||
let updateStatusSummary: UpdateStatusSummary = {
|
||||
status: "idle",
|
||||
currentVersion: app.getVersion(),
|
||||
availableVersion: null,
|
||||
};
|
||||
|
||||
function setUpdateStatusSummary(summary: Partial<UpdateStatusSummary>) {
|
||||
updateStatusSummary = {
|
||||
...updateStatusSummary,
|
||||
currentVersion: app.getVersion(),
|
||||
...summary,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeError(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return error.stack || `${error.name}: ${error.message}`;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function writeUpdaterLog(message: string, detail?: unknown) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(UPDATER_LOG_PATH), { recursive: true });
|
||||
const suffix = detail === undefined ? "" : ` ${summarizeError(detail)}`;
|
||||
fs.appendFileSync(
|
||||
UPDATER_LOG_PATH,
|
||||
`${new Date().toISOString()} ${message}${suffix}\n`,
|
||||
"utf8",
|
||||
);
|
||||
} catch (logError) {
|
||||
console.error("Failed to write updater log:", logError);
|
||||
}
|
||||
}
|
||||
|
||||
function createAutoCheckErrorToastPayload(): UpdateToastPayload {
|
||||
return {
|
||||
version: app.getVersion(),
|
||||
phase: "error",
|
||||
detail:
|
||||
"Recordly could not check for updates automatically. Retry now, or inspect updater.log in your user data folder.",
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "retry-check",
|
||||
};
|
||||
}
|
||||
|
||||
function shouldSurfaceAutomaticCheckErrors() {
|
||||
return !AUTO_UPDATE_ERROR_TOASTS_DISABLED;
|
||||
}
|
||||
|
||||
function configureUpdateFeed() {
|
||||
if (!UPDATE_FEED_URL_OVERRIDE) {
|
||||
writeUpdaterLog("Using published GitHub update feed.");
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "generic",
|
||||
url: UPDATE_FEED_URL_OVERRIDE,
|
||||
channel: "latest",
|
||||
});
|
||||
writeUpdaterLog(`Using overridden update feed: ${UPDATE_FEED_URL_OVERRIDE}`);
|
||||
}
|
||||
|
||||
function canUseAutoUpdates() {
|
||||
return !AUTO_UPDATES_DISABLED && app.isPackaged && !process.mas;
|
||||
@@ -92,6 +183,7 @@ function createAvailableUpdateToastPayload(version: string): UpdateToastPayload
|
||||
phase: "available",
|
||||
detail: "A new version is available. Download it now, or wait and we will remind you again in 3 hours.",
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "download-update",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +210,7 @@ function createDownloadedUpdateToastPayload(version: string): UpdateToastPayload
|
||||
phase: "ready",
|
||||
detail: "Install now to restart into the new version, or wait and we will remind you again in 3 hours.",
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "install-update",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +220,7 @@ function createUpdateErrorToastPayload(version: string, error: unknown): UpdateT
|
||||
phase: "error",
|
||||
detail: `The update download failed. ${String(error)}`,
|
||||
delayMs: UPDATE_REMINDER_DELAY_MS,
|
||||
primaryAction: "download-update",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +244,14 @@ export function getCurrentUpdateToastPayload() {
|
||||
return currentToastPayload;
|
||||
}
|
||||
|
||||
export function getUpdaterLogPath() {
|
||||
return UPDATER_LOG_PATH;
|
||||
}
|
||||
|
||||
export function getUpdateStatusSummary() {
|
||||
return updateStatusSummary;
|
||||
}
|
||||
|
||||
async function showNoUpdatesDialog(getMainWindow: () => BrowserWindow | null) {
|
||||
await showMessageBox(getMainWindow, {
|
||||
type: "info",
|
||||
@@ -267,6 +369,8 @@ export function installDownloadedUpdateNow(sendToRenderer?: UpdateToastSender) {
|
||||
clearDeferredReminderTimer();
|
||||
downloadToastDismissed = false;
|
||||
clearVisibleUpdateToast(sendToRenderer);
|
||||
setUpdateStatusSummary({ status: "ready", availableVersion: pendingDownloadedVersion });
|
||||
writeUpdaterLog("Installing downloaded update.");
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
@@ -290,13 +394,26 @@ export async function downloadAvailableUpdate(sendToRenderer?: UpdateToastSender
|
||||
clearDeferredReminderTimer();
|
||||
downloadInProgress = true;
|
||||
downloadToastDismissed = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "downloading",
|
||||
availableVersion,
|
||||
detail: `Downloading Recordly ${availableVersion}`,
|
||||
});
|
||||
emitUpdateToastState(sendToRenderer, createDownloadingUpdateToastPayload(availableVersion, 0));
|
||||
writeUpdaterLog(`Starting update download for ${availableVersion}.`);
|
||||
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
writeUpdaterLog(`Update download requested for ${availableVersion}.`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
downloadInProgress = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "error",
|
||||
availableVersion,
|
||||
detail: String(error),
|
||||
});
|
||||
writeUpdaterLog(`Update download failed for ${availableVersion}.`, error);
|
||||
emitUpdateToastState(
|
||||
sendToRenderer,
|
||||
createUpdateErrorToastPayload(availableVersion, error),
|
||||
@@ -464,6 +581,9 @@ export async function checkForAppUpdates(
|
||||
options?: { manual?: boolean },
|
||||
) {
|
||||
if (!canUseAutoUpdates()) {
|
||||
writeUpdaterLog(
|
||||
`Skipped update check because auto-updates are unavailable. packaged=${app.isPackaged} mas=${process.mas ? "yes" : "no"} disabled=${AUTO_UPDATES_DISABLED ? "yes" : "no"}`,
|
||||
);
|
||||
if (options?.manual) {
|
||||
await showMessageBox(getMainWindow, {
|
||||
type: "info",
|
||||
@@ -478,6 +598,7 @@ export async function checkForAppUpdates(
|
||||
}
|
||||
|
||||
if (updateCheckInProgress) {
|
||||
writeUpdaterLog("Skipped update check because a previous check is still running.");
|
||||
if (options?.manual) {
|
||||
await showMessageBox(getMainWindow, {
|
||||
type: "info",
|
||||
@@ -490,16 +611,28 @@ export async function checkForAppUpdates(
|
||||
|
||||
manualCheckRequested = Boolean(options?.manual);
|
||||
updateCheckInProgress = true;
|
||||
updateCheckErrorHandled = false;
|
||||
setUpdateStatusSummary({ status: "checking", detail: "Checking for updates..." });
|
||||
writeUpdaterLog(`Starting ${manualCheckRequested ? "manual" : "automatic"} update check.`);
|
||||
|
||||
try {
|
||||
await autoUpdater.checkForUpdates();
|
||||
writeUpdaterLog("Update check request completed.");
|
||||
} catch (error) {
|
||||
updateCheckInProgress = false;
|
||||
const shouldReport = manualCheckRequested;
|
||||
manualCheckRequested = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "error",
|
||||
availableVersion,
|
||||
detail: String(error),
|
||||
});
|
||||
writeUpdaterLog("Update check failed.", error);
|
||||
console.error("Auto-update check failed:", error);
|
||||
if (shouldReport) {
|
||||
if (shouldReport && !updateCheckErrorHandled) {
|
||||
await showUpdateErrorDialog(getMainWindow, error);
|
||||
} else if (!updateCheckErrorHandled && shouldSurfaceAutomaticCheckErrors()) {
|
||||
emitUpdateToastState(activeUpdateToastSender, createAutoCheckErrorToastPayload());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,19 +646,34 @@ export function setupAutoUpdates(
|
||||
}
|
||||
|
||||
if (!canUseAutoUpdates()) {
|
||||
setUpdateStatusSummary({ status: "idle", availableVersion: null, detail: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
updaterInitialized = true;
|
||||
activeUpdateToastSender = sendToRenderer;
|
||||
configureUpdateFeed();
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
writeUpdaterLog(`Updater initialized. logPath=${UPDATER_LOG_PATH}`);
|
||||
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
setUpdateStatusSummary({ status: "checking", availableVersion: null, detail: "Checking for updates..." });
|
||||
writeUpdaterLog("electron-updater emitted checking-for-update.");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
writeUpdaterLog(`Update available: version=${info.version}`);
|
||||
updateCheckInProgress = false;
|
||||
availableVersion = info.version;
|
||||
pendingDownloadedVersion = null;
|
||||
downloadInProgress = false;
|
||||
downloadToastDismissed = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "available",
|
||||
availableVersion: info.version,
|
||||
detail: `Recordly ${info.version} is available.`,
|
||||
});
|
||||
if (skippedVersion === info.version) {
|
||||
manualCheckRequested = false;
|
||||
return;
|
||||
@@ -544,11 +692,17 @@ export function setupAutoUpdates(
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
writeUpdaterLog("No update available.");
|
||||
updateCheckInProgress = false;
|
||||
availableVersion = null;
|
||||
pendingDownloadedVersion = null;
|
||||
downloadInProgress = false;
|
||||
downloadToastDismissed = false;
|
||||
setUpdateStatusSummary({
|
||||
status: "up-to-date",
|
||||
availableVersion: null,
|
||||
detail: `Recordly ${app.getVersion()} is up to date.`,
|
||||
});
|
||||
clearVisibleUpdateToast(sendToRenderer);
|
||||
const shouldReport = manualCheckRequested;
|
||||
manualCheckRequested = false;
|
||||
@@ -563,6 +717,14 @@ export function setupAutoUpdates(
|
||||
}
|
||||
|
||||
downloadInProgress = true;
|
||||
setUpdateStatusSummary({
|
||||
status: "downloading",
|
||||
availableVersion,
|
||||
detail: `Downloading Recordly ${availableVersion}`,
|
||||
});
|
||||
writeUpdaterLog(
|
||||
`Download progress for ${availableVersion}: ${progress.percent.toFixed(1)}%`,
|
||||
);
|
||||
if (downloadToastDismissed) {
|
||||
return;
|
||||
}
|
||||
@@ -577,6 +739,15 @@ export function setupAutoUpdates(
|
||||
updateCheckInProgress = false;
|
||||
const shouldReport = manualCheckRequested;
|
||||
manualCheckRequested = false;
|
||||
if (!downloadInProgress) {
|
||||
updateCheckErrorHandled = true;
|
||||
}
|
||||
setUpdateStatusSummary({
|
||||
status: "error",
|
||||
availableVersion,
|
||||
detail: String(error),
|
||||
});
|
||||
writeUpdaterLog("electron-updater emitted error.", error);
|
||||
console.error("Auto-updater error:", error);
|
||||
if (downloadInProgress && availableVersion) {
|
||||
downloadInProgress = false;
|
||||
@@ -588,10 +759,13 @@ export function setupAutoUpdates(
|
||||
}
|
||||
if (shouldReport) {
|
||||
void showUpdateErrorDialog(getMainWindow, error);
|
||||
} else if (shouldSurfaceAutomaticCheckErrors()) {
|
||||
emitUpdateToastState(sendToRenderer, createAutoCheckErrorToastPayload());
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
writeUpdaterLog(`Update downloaded: version=${info.version}`);
|
||||
updateCheckInProgress = false;
|
||||
manualCheckRequested = false;
|
||||
downloadInProgress = false;
|
||||
@@ -601,6 +775,11 @@ export function setupAutoUpdates(
|
||||
}
|
||||
availableVersion = info.version;
|
||||
pendingDownloadedVersion = info.version;
|
||||
setUpdateStatusSummary({
|
||||
status: "ready",
|
||||
availableVersion: info.version,
|
||||
detail: `Recordly ${info.version} is ready to install.`,
|
||||
});
|
||||
clearDeferredReminderTimer();
|
||||
|
||||
if (emitUpdateToastState(sendToRenderer, createDownloadedUpdateToastPayload(info.version))) {
|
||||
|
||||
Reference in New Issue
Block a user