| @@ -61,6 +61,14 @@ import { · 60 unmodified lines |
| 61 | UPDATE_STATUS_CHANNEL, | 61 | UPDATE_STATUS_CHANNEL, |
| 62 | UPDATE_STATUS_GET_CHANNEL, | 62 | UPDATE_STATUS_GET_CHANNEL, |
| 63 | } from "../shared/update"; | 63 | } from "../shared/update"; |
| | 64 | import { |
| | 65 | planCompletedUpdateCheck, |
| | 66 | planDownloadedUpdate, |
| | 67 | planFatalAutoInstallOnQuit, |
| | 68 | planUpdateCheck, |
| | 69 | statusAfterUpdateError, |
| | 70 | type UpdateCheckTrigger, |
| | 71 | } from "./updater-state"; |
| 64 | | 72 | |
| 65 | // Pin userData to a friendly app-name-scoped dir BEFORE app.ready so every | 73 | // Pin userData to a friendly app-name-scoped dir BEFORE app.ready so every |
| 66 | // Electron-side consumer (electron-store, electron-log, window-state) lands | 74 | // Electron-side consumer (electron-store, electron-log, window-state) lands |
| @@ -741,7 +749,9 @@ const registerIpcHandlers = () => { · 674 unmodified lines |
| 741 | // Crash-screen escape hatch: a recurring sidecar crash may already be | 749 | // Crash-screen escape hatch: a recurring sidecar crash may already be |
| 742 | // fixed upstream. Reuses the menu flow — staged updates prompt to install, | 750 | // fixed upstream. Reuses the menu flow — staged updates prompt to install, |
| 743 | // "no updates" / failures surface in their own dialogs. | 751 | // "no updates" / failures surface in their own dialogs. |
| 744 | ipcMain.handle("executor:updates:check", () => runUpdateCheck({ alertOnFail: true })); | 752 | ipcMain.handle("executor:updates:check", () => |
| | 753 | runUpdateCheck({ alertOnFail: true, trigger: "manual" }), |
| | 754 | ); |
| 745 | ipcMain.handle(UPDATE_STATUS_GET_CHANNEL, (): DesktopUpdateStatus => updateStatus); | 755 | ipcMain.handle(UPDATE_STATUS_GET_CHANNEL, (): DesktopUpdateStatus => updateStatus); |
| 746 | ipcMain.handle(UPDATE_INSTALL_CHANNEL, async (): Promise<void> => { | 756 | ipcMain.handle(UPDATE_INSTALL_CHANNEL, async (): Promise<void> => { |
| 747 | const version = "version" in updateStatus ? updateStatus.version : ""; | 757 | const version = "version" in updateStatus ? updateStatus.version : ""; |
| @@ -754,6 +764,7 @@ const registerIpcHandlers = () => { · 6 unmodified lines |
| 754 | } | 764 | } |
| 755 | // Stop the sidecar cleanly before Squirrel.Mac swaps the bundle, matching | 765 | // Stop the sidecar cleanly before Squirrel.Mac swaps the bundle, matching |
| 756 | // the native dialog's restart path. | 766 | // the native dialog's restart path. |
| | 767 | stopSupervisedMonitor(); |
| 757 | if (connection) { | 768 | if (connection) { |
| 758 | await stopConnection(connection); | 769 | await stopConnection(connection); |
| 759 | connection = null; | 770 | connection = null; |
| @@ -804,6 +815,7 @@ const registerIpcHandlers = () => { · 44 unmodified lines |
| 804 | // ────────────────────────────────────────────────────────────────────── | 815 | // ────────────────────────────────────────────────────────────────────── |
| 805 | | 816 | |
| 806 | let downloadedUpdateVersion: string | null = null; | 817 | let downloadedUpdateVersion: string | null = null; |
| | 818 | let declinedUpdateVersion: string | null = null; |
| 807 | let updateDialogOpen = false; | 819 | let updateDialogOpen = false; |
| 808 | | 820 | |
| 809 | // Renderer-facing auto-update status. The web shell renders a desktop-native | 821 | // Renderer-facing auto-update status. The web shell renders a desktop-native |
| @@ -845,11 +857,16 @@ const applyFakeUpdateFromEnv = () => { · 35 unmodified lines |
| 845 | state === "available" || | 857 | state === "available" || |
| 846 | state === "downloaded" || | 858 | state === "downloaded" || |
| 847 | state === "downloading" || | 859 | state === "downloading" || |
| | 860 | state === "error" || |
| 848 | state === "installing" | 861 | state === "installing" |
| 849 | ) { | 862 | ) { |
| 850 | if (state === "downloaded") downloadedUpdateVersion = version; | 863 | if (state === "downloaded") downloadedUpdateVersion = version; |
| 851 | setUpdateStatus( | 864 | setUpdateStatus( |
| 852 | state === "downloading" ? { state, version, percent: 100 } : { state, version }, | 865 | state === "downloading" |
| | 866 | ? { state, version, percent: 100 } |
| | 867 | : state === "error" |
| | 868 | ? { state, version, message: "Update failed" } |
| | 869 | : { state, version }, |
| 853 | ); | 870 | ); |
| 854 | } | 871 | } |
| 855 | } catch (error) { | 872 | } catch (error) { |
| @@ -874,12 +891,15 @@ const promptInstallUpdate = async (version: string) => { · 18 unmodified lines |
| 874 | if (response.response === 0) { | 891 | if (response.response === 0) { |
| 875 | // Stop the sidecar cleanly before Squirrel.Mac swaps the bundle. A | 892 | // Stop the sidecar cleanly before Squirrel.Mac swaps the bundle. A |
| 876 | // supervised daemon is left running — it's independent of this bundle. | 893 | // supervised daemon is left running — it's independent of this bundle. |
| | 894 | stopSupervisedMonitor(); |
| 877 | if (connection) { | 895 | if (connection) { |
| 878 | await stopConnection(connection); | 896 | await stopConnection(connection); |
| 879 | connection = null; | 897 | connection = null; |
| 880 | } | 898 | } |
| 881 | autoUpdater.quitAndInstall(false, true); | 899 | autoUpdater.quitAndInstall(false, true); |
| | 900 | return; |
| 882 | } | 901 | } |
| | 902 | declinedUpdateVersion = version; |
| 883 | } finally { | 903 | } finally { |
| 884 | updateDialogOpen = false; | 904 | updateDialogOpen = false; |
| 885 | } | 905 | } |
| @@ -910,25 +930,41 @@ const setupAutoUpdater = () => { · 24 unmodified lines |
| 910 | } | 930 | } |
| 911 | }); | 931 | }); |
| 912 | autoUpdater.on("update-downloaded", (info: UpdateInfo) => { | 932 | autoUpdater.on("update-downloaded", (info: UpdateInfo) => { |
| 913 | downloadedUpdateVersion = info.version; | 933 | const decision = planDownloadedUpdate({ |
| 914 | setUpdateStatus({ state: "downloaded", version: info.version }); | 934 | stagedVersion: downloadedUpdateVersion, |
| 915 | void promptInstallUpdate(info.version); | 935 | declinedVersion: declinedUpdateVersion, |
| | 936 | incomingVersion: info.version, |
| | 937 | trigger: "boot", |
| | 938 | }); |
| | 939 | downloadedUpdateVersion = decision.stagedVersion; |
| | 940 | declinedUpdateVersion = decision.declinedVersion; |
| | 941 | setUpdateStatus(decision.status); |
| | 942 | if (decision.promptVersion) void promptInstallUpdate(decision.promptVersion); |
| 916 | }); | 943 | }); |
| 917 | autoUpdater.on("error", (err: Error) => { | 944 | autoUpdater.on("error", (err: Error) => { |
| 918 | log.warn("[updater] error", err); | 945 | log.warn("[updater] error", err); |
| | 946 | setUpdateStatus( |
| | 947 | statusAfterUpdateError(updateStatus, "Update failed. Check again from the menu."), |
| | 948 | ); |
| | 949 | pendingUpdateVersion = null; |
| 919 | }); | 950 | }); |
| 920 | | 951 | |
| 921 | setInterval(() => { | 952 | setInterval(() => { |
| 922 | if (downloadedUpdateVersion) return; // already staged; waiting on the user | 953 | void runUpdateCheck({ alertOnFail: false, trigger: "interval" }); |
| 923 | void runUpdateCheck({ alertOnFail: false }); | | |
| 924 | }, UPDATE_POLL_INTERVAL_MS); | 954 | }, UPDATE_POLL_INTERVAL_MS); |
| 925 | }; | 955 | }; |
| 926 | | 956 | |
| 927 | interface UpdateCheckOptions { | 957 | interface UpdateCheckOptions { |
| 928 | readonly alertOnFail: boolean; | 958 | readonly alertOnFail: boolean; |
| | 959 | readonly trigger: UpdateCheckTrigger; |
| 929 | } | 960 | } |
| 930 | | 961 | |
| 931 | const runUpdateCheck = async ({ alertOnFail }: UpdateCheckOptions) => { | 962 | type UpdateCheckResult = { |
| | 963 | readonly isUpdateAvailable?: boolean; |
| | 964 | readonly updateInfo?: { readonly version?: string }; |
| | 965 | }; |
| | 966 | |
| | 967 | const runUpdateCheck = async ({ alertOnFail, trigger }: UpdateCheckOptions) => { |
| 932 | if (!app.isPackaged) { | 968 | if (!app.isPackaged) { |
| 933 | if (alertOnFail) { | 969 | if (alertOnFail) { |
| 934 | await dialog.showMessageBox({ | 970 | await dialog.showMessageBox({ |
| @@ -939,14 +975,23 @@ const runUpdateCheck = async ({ alertOnFail }: UpdateCheckOptions) => { · 4 unmodified lines |
| 939 | } | 975 | } |
| 940 | return; | 976 | return; |
| 941 | } | 977 | } |
| 942 | if (downloadedUpdateVersion) { | 978 | const checkPlan = planUpdateCheck({ stagedVersion: downloadedUpdateVersion, trigger }); |
| 943 | await promptInstallUpdate(downloadedUpdateVersion); | 979 | if (!checkPlan.check) return; |
| 944 | return; | | |
| 945 | } | | |
| 946 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: surface network/update failures only when the user asked | 980 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: surface network/update failures only when the user asked |
| 947 | try { | 981 | try { |
| 948 | const result = await autoUpdater.checkForUpdates(); | 982 | const result = (await autoUpdater.checkForUpdates()) as UpdateCheckResult | null; |
| 949 | const newer = result?.isUpdateAvailable === true; | 983 | const newer = result?.isUpdateAvailable === true; |
| | 984 | const promptAfterCheck = planCompletedUpdateCheck({ |
| | 985 | stagedVersion: checkPlan.promptVersionAfterCheck, |
| | 986 | trigger, |
| | 987 | updateAvailable: newer, |
| | 988 | availableVersion: |
| | 989 | typeof result?.updateInfo?.version === "string" ? result.updateInfo.version : null, |
| | 990 | }).promptVersion; |
| | 991 | if (promptAfterCheck) { |
| | 992 | await promptInstallUpdate(promptAfterCheck); |
| | 993 | return; |
| | 994 | } |
| 950 | if (!alertOnFail) return; | 995 | if (!alertOnFail) return; |
| 951 | if (newer) return; // 'update-downloaded' handler will fire the install dialog | 996 | if (newer) return; // 'update-downloaded' handler will fire the install dialog |
| 952 | await dialog.showMessageBox({ | 997 | await dialog.showMessageBox({ |
| @@ -956,6 +1001,9 @@ const runUpdateCheck = async ({ alertOnFail }: UpdateCheckOptions) => { · 3 unmodified lines |
| 956 | }); | 1001 | }); |
| 957 | } catch (error) { | 1002 | } catch (error) { |
| 958 | log.warn("[updater] check failed", error); | 1003 | log.warn("[updater] check failed", error); |
| | 1004 | setUpdateStatus( |
| | 1005 | statusAfterUpdateError(updateStatus, "Update failed. Check again from the menu."), |
| | 1006 | ); |
| 959 | if (!alertOnFail) return; | 1007 | if (!alertOnFail) return; |
| 960 | await dialog.showMessageBox({ | 1008 | await dialog.showMessageBox({ |
| 961 | type: "error", | 1009 | type: "error", |
| @@ -978,8 +1026,12 @@ const handleFatalSidecarFailure = async (error: unknown) => { · 16 unmodified lines |
| 978 | // Install whatever finishes downloading by the time the user quits the | 1026 | // Install whatever finishes downloading by the time the user quits the |
| 979 | // failure dialog; if it downloads while the dialog is open, the regular | 1027 | // failure dialog; if it downloads while the dialog is open, the regular |
| 980 | // 'update-downloaded' prompt offers an immediate restart instead. | 1028 | // 'update-downloaded' prompt offers an immediate restart instead. |
| 981 | autoUpdater.autoInstallOnAppQuit = true; | 1029 | const autoInstallPlan = planFatalAutoInstallOnQuit({ |
| 982 | void runUpdateCheck({ alertOnFail: false }); | 1030 | packaged: true, |
| | 1031 | retryAfterReset: false, |
| | 1032 | }); |
| | 1033 | if (autoInstallPlan.enableDuringFailure) autoUpdater.autoInstallOnAppQuit = true; |
| | 1034 | void runUpdateCheck({ alertOnFail: false, trigger: "boot" }); |
| 983 | } | 1035 | } |
| 984 | // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: sidecar startup failures arrive as plain Node errors and render in a native dialog | 1036 | // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: sidecar startup failures arrive as plain Node errors and render in a native dialog |
| 985 | const detail = error instanceof Error ? (error.stack ?? error.message) : String(error); | 1037 | const detail = error instanceof Error ? (error.stack ?? error.message) : String(error); |
| @@ -995,7 +1047,13 @@ const handleFatalSidecarFailure = async (error: unknown) => { · 9 unmodified lines |
| 995 | // Damaged executor state (failed migration, corrupt SQLite) makes startup | 1047 | // Damaged executor state (failed migration, corrupt SQLite) makes startup |
| 996 | // fail forever — updating can't fix it. Offer the move-aside reset and one | 1048 | // fail forever — updating can't fix it. Offer the move-aside reset and one |
| 997 | // immediate retry. Returns true when boot should be attempted again. | 1049 | // immediate retry. Returns true when boot should be attempted again. |
| 998 | if (response === 1 && (await confirmResetState())) { | 1050 | const retryAfterReset = response === 1 && (await confirmResetState()); |
| | 1051 | const autoInstallPlan = planFatalAutoInstallOnQuit({ |
| | 1052 | packaged: app.isPackaged, |
| | 1053 | retryAfterReset, |
| | 1054 | }); |
| | 1055 | if (retryAfterReset) { |
| | 1056 | if (autoInstallPlan.restoreAfterRecovery) autoUpdater.autoInstallOnAppQuit = false; |
| 999 | const { backupDir } = resetExecutorState(); | 1057 | const { backupDir } = resetExecutorState(); |
| 1000 | await announceBackup(backupDir); | 1058 | await announceBackup(backupDir); |
| 1001 | return true; | 1059 | return true; |
| @@ -1011,7 +1069,7 @@ const installApplicationMenu = () => { · 9 unmodified lines |
| 1011 | { role: "about" }, | 1069 | { role: "about" }, |
| 1012 | { | 1070 | { |
| 1013 | label: "Check for Updates…", | 1071 | label: "Check for Updates…", |
| 1014 | click: () => void runUpdateCheck({ alertOnFail: true }), | 1072 | click: () => void runUpdateCheck({ alertOnFail: true, trigger: "manual" }), |
| 1015 | }, | 1073 | }, |
| 1016 | { | 1074 | { |
| 1017 | label: "Export Diagnostics…", | 1075 | label: "Export Diagnostics…", |
| @@ -1105,7 +1163,7 @@ const boot = async () => { · 87 unmodified lines |
| 1105 | // A crashing sidecar may be a broken release — quietly stage any | 1163 | // A crashing sidecar may be a broken release — quietly stage any |
| 1106 | // available update so the install prompt appears on its own (same | 1164 | // available update so the install prompt appears on its own (same |
| 1107 | // self-heal as the fatal startup path). | 1165 | // self-heal as the fatal startup path). |
| 1108 | void runUpdateCheck({ alertOnFail: false }); | 1166 | void runUpdateCheck({ alertOnFail: false, trigger: "boot" }); |
| 1109 | }); | 1167 | }); |
| 1110 | // Prefer an OS-supervised daemon: attach to one that's running, kick one | 1168 | // Prefer an OS-supervised daemon: attach to one that's running, kick one |
| 1111 | // that's installed, or offer to install on first run. Quitting the app then | 1169 | // that's installed, or offer to install on first run. Quitting the app then |
| @@ -1121,7 +1179,7 @@ const boot = async () => { · 9 unmodified lines |
| 1121 | try { | 1179 | try { |
| 1122 | await createWindow(supervised); | 1180 | await createWindow(supervised); |
| 1123 | armSupervisedMonitor(); | 1181 | armSupervisedMonitor(); |
| 1124 | void runUpdateCheck({ alertOnFail: false }); | 1182 | void runUpdateCheck({ alertOnFail: false, trigger: "boot" }); |
| 1125 | return; | 1183 | return; |
| 1126 | } catch (error) { | 1184 | } catch (error) { |
| 1127 | log.warn("Failed to load supervised daemon; falling back to managed sidecar", error); | 1185 | log.warn("Failed to load supervised daemon; falling back to managed sidecar", error); |
| @@ -1196,7 +1254,7 @@ const boot = async () => { · 68 unmodified lines |
| 1196 | // Check at boot. If an update is available, autoDownload pulls it and | 1254 | // Check at boot. If an update is available, autoDownload pulls it and |
| 1197 | // the 'update-downloaded' handler fires the install dialog. Silent on | 1255 | // the 'update-downloaded' handler fires the install dialog. Silent on |
| 1198 | // no-update / failure so we don't bother users on every launch. | 1256 | // no-update / failure so we don't bother users on every launch. |
| 1199 | void runUpdateCheck({ alertOnFail: false }); | 1257 | void runUpdateCheck({ alertOnFail: false, trigger: "boot" }); |
| 1200 | }; | 1258 | }; |
| 1201 | | 1259 | |
| 1202 | if (ensureSingleInstance()) { | 1260 | if (ensureSingleInstance()) { |