| ... | ... | @@ -425,7 +425,9 @@ function dispatchMenuAction(action: string): void { |
| 425 | 425 | |
| 426 | 426 | function handleCheckForUpdatesMenuClick(): void { |
| 427 | 427 | if (updateState.status === "downloaded") { |
| 428 | | installDownloadedUpdate(); |
| 428 | console.info( |
| 429 | "[desktop-updater] Check for updates clicked while an update is already downloaded; skipping automatic install.", |
| 430 | ); |
| 429 | 431 | return; |
| 430 | 432 | } |
| 431 | 433 | |
| ... | ... | @@ -576,7 +578,11 @@ function shouldEnableAutoUpdates(): boolean { |
| 576 | 578 | |
| 577 | 579 | async function checkForUpdates(reason: string): Promise<void> { |
| 578 | 580 | if (!updaterConfigured || updateCheckInFlight) return; |
| 579 | | if (updateState.status === "downloading" || updateState.status === "downloaded") { |
| 581 | if ( |
| 582 | updateState.status === "downloading" || |
| 583 | updateState.status === "downloaded" || |
| 584 | updateState.status === "available" |
| 585 | ) { |
| 580 | 586 | console.info( |
| 581 | 587 | `[desktop-updater] Skipping update check (${reason}) while status=${updateState.status}.`, |
| 582 | 588 | ); |
| ... | ... | @@ -639,8 +645,9 @@ function installDownloadedUpdate(): void { |
| 639 | 645 | |
| 640 | 646 | isQuitting = true; |
| 641 | 647 | clearUpdatePollTimer(); |
| 642 | | stopBackend(); |
| 643 | | autoUpdater.quitAndInstall(); |
| 648 | void stopBackendAndWaitForExit().finally(() => { |
| 649 | autoUpdater.quitAndInstall(); |
| 650 | }); |
| 644 | 651 | } |
| 645 | 652 | |
| 646 | 653 | function configureAutoUpdater(): void { |
| ... | ... | @@ -674,6 +681,7 @@ function configureAutoUpdater(): void { |
| 674 | 681 | autoUpdater.autoDownload = false; |
| 675 | 682 | autoUpdater.autoInstallOnAppQuit = false; |
| 676 | 683 | autoUpdater.allowPrerelease = app.getVersion().includes("-"); |
| 684 | let lastLoggedDownloadMilestone = -1; |
| 677 | 685 | |
| 678 | 686 | autoUpdater.on("checking-for-update", () => { |
| 679 | 687 | console.info("[desktop-updater] Looking for updates..."); |
| ... | ... | @@ -687,6 +695,7 @@ function configureAutoUpdater(): void { |
| 687 | 695 | checkedAt: new Date().toISOString(), |
| 688 | 696 | message: null, |
| 689 | 697 | }); |
| 698 | lastLoggedDownloadMilestone = -1; |
| 690 | 699 | console.info(`[desktop-updater] Update available: ${info.version}`); |
| 691 | 700 | }); |
| 692 | 701 | autoUpdater.on("update-not-available", () => { |
| ... | ... | @@ -698,14 +707,19 @@ function configureAutoUpdater(): void { |
| 698 | 707 | checkedAt: new Date().toISOString(), |
| 699 | 708 | message: null, |
| 700 | 709 | }); |
| 710 | lastLoggedDownloadMilestone = -1; |
| 701 | 711 | console.info("[desktop-updater] No updates available."); |
| 702 | 712 | }); |
| 703 | 713 | autoUpdater.on("error", (error) => { |
| 704 | | const message = error instanceof Error ? error.message : String(error); |
| 705 | | setUpdateState({ |
| 706 | | status: "error", |
| 707 | | message, |
| 708 | | }); |
| 714 | const message = formatErrorMessage(error); |
| 715 | if (!updateCheckInFlight && !updateDownloadInFlight) { |
| 716 | setUpdateState({ |
| 717 | status: "error", |
| 718 | message, |
| 719 | checkedAt: new Date().toISOString(), |
| 720 | downloadPercent: null, |
| 721 | }); |
| 722 | } |
| 709 | 723 | console.error(`[desktop-updater] Updater error: ${message}`); |
| 710 | 724 | }); |
| 711 | 725 | autoUpdater.on("download-progress", (progress) => { |
| ... | ... | @@ -715,7 +729,9 @@ function configureAutoUpdater(): void { |
| 715 | 729 | downloadPercent: progress.percent, |
| 716 | 730 | message: null, |
| 717 | 731 | }); |
| 718 | | if (percent % 10 === 0) { |
| 732 | const milestone = percent - (percent % 10); |
| 733 | if (milestone > lastLoggedDownloadMilestone) { |
| 734 | lastLoggedDownloadMilestone = milestone; |
| 719 | 735 | console.info(`[desktop-updater] Download progress: ${percent}%`); |
| 720 | 736 | } |
| 721 | 737 | }); |
| ... | ... | @@ -842,6 +858,57 @@ function stopBackend(): void { |
| 842 | 858 | } |
| 843 | 859 | } |
| 844 | 860 | |
| 861 | async function stopBackendAndWaitForExit(timeoutMs = 5_000): Promise<void> { |
| 862 | if (restartTimer) { |
| 863 | clearTimeout(restartTimer); |
| 864 | restartTimer = null; |
| 865 | } |
| 866 | |
| 867 | const child = backendProcess; |
| 868 | backendProcess = null; |
| 869 | if (!child) return; |
| 870 | const backendChild = child; |
| 871 | if (backendChild.exitCode !== null || backendChild.signalCode !== null) return; |
| 872 | |
| 873 | await new Promise<void>((resolve) => { |
| 874 | let settled = false; |
| 875 | let forceKillTimer: ReturnType<typeof setTimeout> | null = null; |
| 876 | let exitTimeoutTimer: ReturnType<typeof setTimeout> | null = null; |
| 877 | |
| 878 | function settle(): void { |
| 879 | if (settled) return; |
| 880 | settled = true; |
| 881 | backendChild.off("exit", onExit); |
| 882 | if (forceKillTimer) { |
| 883 | clearTimeout(forceKillTimer); |
| 884 | } |
| 885 | if (exitTimeoutTimer) { |
| 886 | clearTimeout(exitTimeoutTimer); |
| 887 | } |
| 888 | resolve(); |
| 889 | } |
| 890 | |
| 891 | function onExit(): void { |
| 892 | settle(); |
| 893 | } |
| 894 | |
| 895 | backendChild.once("exit", onExit); |
| 896 | backendChild.kill("SIGTERM"); |
| 897 | |
| 898 | forceKillTimer = setTimeout(() => { |
| 899 | if (backendChild.exitCode === null && backendChild.signalCode === null) { |
| 900 | backendChild.kill("SIGKILL"); |
| 901 | } |
| 902 | }, 2_000); |
| 903 | forceKillTimer.unref(); |
| 904 | |
| 905 | exitTimeoutTimer = setTimeout(() => { |
| 906 | settle(); |
| 907 | }, timeoutMs); |
| 908 | exitTimeoutTimer.unref(); |
| 909 | }); |
| 910 | } |
| 911 | |
| 845 | 912 | function registerIpcHandlers(): void { |
| 846 | 913 | ipcMain.removeHandler(PICK_FOLDER_CHANNEL); |
| 847 | 914 | ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { |