| author | |
| committer | |
| log | 32d89806d7b17dbcffa4f02b46b24b410bf54b5e |
| tree | 634b843f15891c1a0e304ce49617b0d9a07ce1b1 |
| parent | 2fd4e06b345e6edbe12f1de456bb3f057c2b14f0 |
- Add `updateState` helpers for auto-update eligibility reasons and progress throttling
- Show a user-facing dialog when manual update checks are unavailable
- Ensure manual update checks can open a window when none exist
- Add tests for disable-reason logic and 10% download progress bucketing3 files changed, 158 insertions(+), 17 deletions(-)
apps/desktop/src/main.ts+38-17| ... | ... | @@ -15,6 +15,7 @@ import { NetService } from "@t3tools/shared/Net"; |
| 15 | 15 | import { RotatingFileSink } from "@t3tools/shared/logging"; |
| 16 | 16 | import { showDesktopConfirmDialog } from "./confirmDialog"; |
| 17 | 17 | import { fixPath } from "./fixPath"; |
| 18 | import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; | |
| 18 | 19 | |
| 19 | 20 | fixPath(); |
| 20 | 21 | |
| ... | ... | @@ -425,11 +426,28 @@ function dispatchMenuAction(action: string): void { |
| 425 | 426 | } |
| 426 | 427 | |
| 427 | 428 | function handleCheckForUpdatesMenuClick(): void { |
| 428 | if (!shouldEnableAutoUpdates()) { | |
| 429 | const disabledReason = getAutoUpdateDisabledReason({ | |
| 430 | isDevelopment, | |
| 431 | isPackaged: app.isPackaged, | |
| 432 | platform: process.platform, | |
| 433 | appImage: process.env.APPIMAGE, | |
| 434 | disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", | |
| 435 | }); | |
| 436 | if (disabledReason) { | |
| 429 | 437 | console.info("[desktop-updater] Manual update check requested, but updates are disabled."); |
| 438 | void dialog.showMessageBox({ | |
| 439 | type: "info", | |
| 440 | title: "Updates unavailable", | |
| 441 | message: "Automatic updates are not available right now.", | |
| 442 | detail: disabledReason, | |
| 443 | buttons: ["OK"], | |
| 444 | }); | |
| 430 | 445 | return; |
| 431 | 446 | } |
| 432 | 447 | |
| 448 | if (!BrowserWindow.getAllWindows().length) { | |
| 449 | mainWindow = createWindow(); | |
| 450 | } | |
| 433 | 451 | void checkForUpdates("menu"); |
| 434 | 452 | } |
| 435 | 453 | |
| ... | ... | @@ -562,17 +580,15 @@ function setUpdateState(patch: Partial<DesktopUpdateState>): void { |
| 562 | 580 | } |
| 563 | 581 | |
| 564 | 582 | function shouldEnableAutoUpdates(): boolean { |
| 565 | if (isDevelopment || !app.isPackaged) { | |
| 566 | return false; | |
| 567 | } | |
| 568 | if (process.env.T3CODE_DISABLE_AUTO_UPDATE === "1") { | |
| 569 | return false; | |
| 570 | } | |
| 571 | if (process.platform === "linux" && !process.env.APPIMAGE) { | |
| 572 | // electron-updater only supports Linux auto-updates when running from AppImage. | |
| 573 | return false; | |
| 574 | } | |
| 575 | return true; | |
| 583 | return ( | |
| 584 | getAutoUpdateDisabledReason({ | |
| 585 | isDevelopment, | |
| 586 | isPackaged: app.isPackaged, | |
| 587 | platform: process.platform, | |
| 588 | appImage: process.env.APPIMAGE, | |
| 589 | disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", | |
| 590 | }) === null | |
| 591 | ); | |
| 576 | 592 | } |
| 577 | 593 | |
| 578 | 594 | async function checkForUpdates(reason: string): Promise<void> { |
| ... | ... | @@ -719,11 +735,16 @@ function configureAutoUpdater(): void { |
| 719 | 735 | }); |
| 720 | 736 | autoUpdater.on("download-progress", (progress) => { |
| 721 | 737 | const percent = Math.floor(progress.percent); |
| 722 | setUpdateState({ | |
| 723 | status: "downloading", | |
| 724 | downloadPercent: progress.percent, | |
| 725 | message: null, | |
| 726 | }); | |
| 738 | if ( | |
| 739 | shouldBroadcastDownloadProgress(updateState, progress.percent) || | |
| 740 | updateState.message !== null | |
| 741 | ) { | |
| 742 | setUpdateState({ | |
| 743 | status: "downloading", | |
| 744 | downloadPercent: progress.percent, | |
| 745 | message: null, | |
| 746 | }); | |
| 747 | } | |
| 727 | 748 | const milestone = percent - (percent % 10); |
| 728 | 749 | if (milestone > lastLoggedDownloadMilestone) { |
| 729 | 750 | lastLoggedDownloadMilestone = milestone; |
apps/desktop/src/updateState.test.ts created+82| ... | ... | @@ -0,0 +1,82 @@ |
| 1 | import { describe, expect, it } from "vitest"; | |
| 2 | import type { DesktopUpdateState } from "@t3tools/contracts"; | |
| 3 | ||
| 4 | import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; | |
| 5 | ||
| 6 | const baseState: DesktopUpdateState = { | |
| 7 | enabled: true, | |
| 8 | status: "idle", | |
| 9 | currentVersion: "1.0.0", | |
| 10 | availableVersion: null, | |
| 11 | downloadedVersion: null, | |
| 12 | downloadPercent: null, | |
| 13 | checkedAt: null, | |
| 14 | message: null, | |
| 15 | }; | |
| 16 | ||
| 17 | describe("shouldBroadcastDownloadProgress", () => { | |
| 18 | it("broadcasts the first downloading progress update", () => { | |
| 19 | expect( | |
| 20 | shouldBroadcastDownloadProgress( | |
| 21 | { ...baseState, status: "downloading", downloadPercent: null }, | |
| 22 | 1, | |
| 23 | ), | |
| 24 | ).toBe(true); | |
| 25 | }); | |
| 26 | ||
| 27 | it("skips progress updates within the same 10% bucket", () => { | |
| 28 | expect( | |
| 29 | shouldBroadcastDownloadProgress( | |
| 30 | { ...baseState, status: "downloading", downloadPercent: 11.2 }, | |
| 31 | 18.7, | |
| 32 | ), | |
| 33 | ).toBe(false); | |
| 34 | }); | |
| 35 | ||
| 36 | it("broadcasts progress updates when a new 10% bucket is reached", () => { | |
| 37 | expect( | |
| 38 | shouldBroadcastDownloadProgress( | |
| 39 | { ...baseState, status: "downloading", downloadPercent: 19.9 }, | |
| 40 | 20.1, | |
| 41 | ), | |
| 42 | ).toBe(true); | |
| 43 | }); | |
| 44 | }); | |
| 45 | ||
| 46 | describe("getAutoUpdateDisabledReason", () => { | |
| 47 | it("reports development builds as disabled", () => { | |
| 48 | expect( | |
| 49 | getAutoUpdateDisabledReason({ | |
| 50 | isDevelopment: true, | |
| 51 | isPackaged: false, | |
| 52 | platform: "darwin", | |
| 53 | appImage: undefined, | |
| 54 | disabledByEnv: false, | |
| 55 | }), | |
| 56 | ).toContain("packaged production builds"); | |
| 57 | }); | |
| 58 | ||
| 59 | it("reports env-disabled auto updates", () => { | |
| 60 | expect( | |
| 61 | getAutoUpdateDisabledReason({ | |
| 62 | isDevelopment: false, | |
| 63 | isPackaged: true, | |
| 64 | platform: "darwin", | |
| 65 | appImage: undefined, | |
| 66 | disabledByEnv: true, | |
| 67 | }), | |
| 68 | ).toContain("T3CODE_DISABLE_AUTO_UPDATE"); | |
| 69 | }); | |
| 70 | ||
| 71 | it("reports linux non-AppImage builds as disabled", () => { | |
| 72 | expect( | |
| 73 | getAutoUpdateDisabledReason({ | |
| 74 | isDevelopment: false, | |
| 75 | isPackaged: true, | |
| 76 | platform: "linux", | |
| 77 | appImage: undefined, | |
| 78 | disabledByEnv: false, | |
| 79 | }), | |
| 80 | ).toContain("AppImage"); | |
| 81 | }); | |
| 82 | }); |
apps/desktop/src/updateState.ts created+38| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | import type { DesktopUpdateState } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | export function shouldBroadcastDownloadProgress( | |
| 4 | currentState: DesktopUpdateState, | |
| 5 | nextPercent: number, | |
| 6 | ): boolean { | |
| 7 | if (currentState.status !== "downloading") { | |
| 8 | return true; | |
| 9 | } | |
| 10 | ||
| 11 | const currentPercent = currentState.downloadPercent; | |
| 12 | if (currentPercent === null) { | |
| 13 | return true; | |
| 14 | } | |
| 15 | ||
| 16 | const previousStep = Math.floor(currentPercent / 10); | |
| 17 | const nextStep = Math.floor(nextPercent / 10); | |
| 18 | return nextStep > previousStep || nextPercent === 100; | |
| 19 | } | |
| 20 | ||
| 21 | export function getAutoUpdateDisabledReason(args: { | |
| 22 | isDevelopment: boolean; | |
| 23 | isPackaged: boolean; | |
| 24 | platform: NodeJS.Platform; | |
| 25 | appImage?: string | undefined; | |
| 26 | disabledByEnv: boolean; | |
| 27 | }): string | null { | |
| 28 | if (args.isDevelopment || !args.isPackaged) { | |
| 29 | return "Automatic updates are only available in packaged production builds."; | |
| 30 | } | |
| 31 | if (args.disabledByEnv) { | |
| 32 | return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting."; | |
| 33 | } | |
| 34 | if (args.platform === "linux" && !args.appImage) { | |
| 35 | return "Automatic updates on Linux require running the AppImage build."; | |
| 36 | } | |
| 37 | return null; | |
| 38 | } |