| ... | @@ -425,7 +425,9 @@ function dispatchMenuAction(action: string): void { | ... | @@ -425,7 +425,9 @@ function dispatchMenuAction(action: string): void { |
| 425 | | 425 | |
| 426 | function handleCheckForUpdatesMenuClick(): void { | 426 | function handleCheckForUpdatesMenuClick(): void { |
| 427 | if (updateState.status === "downloaded") { | 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 | return; | 431 | return; |
| 430 | } | 432 | } |
| 431 | | 433 | |
| ... | @@ -576,7 +578,11 @@ function shouldEnableAutoUpdates(): boolean { | ... | @@ -576,7 +578,11 @@ function shouldEnableAutoUpdates(): boolean { |
| 576 | | 578 | |
| 577 | async function checkForUpdates(reason: string): Promise<void> { | 579 | async function checkForUpdates(reason: string): Promise<void> { |
| 578 | if (!updaterConfigured || updateCheckInFlight) return; | 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 | console.info( | 586 | console.info( |
| 581 | `[desktop-updater] Skipping update check (${reason}) while status=${updateState.status}.`, | 587 | `[desktop-updater] Skipping update check (${reason}) while status=${updateState.status}.`, |
| 582 | ); | 588 | ); |
| ... | @@ -639,8 +645,9 @@ function installDownloadedUpdate(): void { | ... | @@ -639,8 +645,9 @@ function installDownloadedUpdate(): void { |
| 639 | | 645 | |
| 640 | isQuitting = true; | 646 | isQuitting = true; |
| 641 | clearUpdatePollTimer(); | 647 | clearUpdatePollTimer(); |
| 642 | stopBackend(); | 648 | void stopBackendAndWaitForExit().finally(() => { |
| 643 | autoUpdater.quitAndInstall(); | 649 | autoUpdater.quitAndInstall(); |
| | 650 | }); |
| 644 | } | 651 | } |
| 645 | | 652 | |
| 646 | function configureAutoUpdater(): void { | 653 | function configureAutoUpdater(): void { |
| ... | @@ -674,6 +681,7 @@ function configureAutoUpdater(): void { | ... | @@ -674,6 +681,7 @@ function configureAutoUpdater(): void { |
| 674 | autoUpdater.autoDownload = false; | 681 | autoUpdater.autoDownload = false; |
| 675 | autoUpdater.autoInstallOnAppQuit = false; | 682 | autoUpdater.autoInstallOnAppQuit = false; |
| 676 | autoUpdater.allowPrerelease = app.getVersion().includes("-"); | 683 | autoUpdater.allowPrerelease = app.getVersion().includes("-"); |
| | 684 | let lastLoggedDownloadMilestone = -1; |
| 677 | | 685 | |
| 678 | autoUpdater.on("checking-for-update", () => { | 686 | autoUpdater.on("checking-for-update", () => { |
| 679 | console.info("[desktop-updater] Looking for updates..."); | 687 | console.info("[desktop-updater] Looking for updates..."); |
| ... | @@ -687,6 +695,7 @@ function configureAutoUpdater(): void { | ... | @@ -687,6 +695,7 @@ function configureAutoUpdater(): void { |
| 687 | checkedAt: new Date().toISOString(), | 695 | checkedAt: new Date().toISOString(), |
| 688 | message: null, | 696 | message: null, |
| 689 | }); | 697 | }); |
| | 698 | lastLoggedDownloadMilestone = -1; |
| 690 | console.info(`[desktop-updater] Update available: ${info.version}`); | 699 | console.info(`[desktop-updater] Update available: ${info.version}`); |
| 691 | }); | 700 | }); |
| 692 | autoUpdater.on("update-not-available", () => { | 701 | autoUpdater.on("update-not-available", () => { |
| ... | @@ -698,14 +707,19 @@ function configureAutoUpdater(): void { | ... | @@ -698,14 +707,19 @@ function configureAutoUpdater(): void { |
| 698 | checkedAt: new Date().toISOString(), | 707 | checkedAt: new Date().toISOString(), |
| 699 | message: null, | 708 | message: null, |
| 700 | }); | 709 | }); |
| | 710 | lastLoggedDownloadMilestone = -1; |
| 701 | console.info("[desktop-updater] No updates available."); | 711 | console.info("[desktop-updater] No updates available."); |
| 702 | }); | 712 | }); |
| 703 | autoUpdater.on("error", (error) => { | 713 | autoUpdater.on("error", (error) => { |
| 704 | const message = error instanceof Error ? error.message : String(error); | 714 | const message = formatErrorMessage(error); |
| 705 | setUpdateState({ | 715 | if (!updateCheckInFlight && !updateDownloadInFlight) { |
| 706 | status: "error", | 716 | setUpdateState({ |
| 707 | message, | 717 | status: "error", |
| 708 | }); | 718 | message, |
| | 719 | checkedAt: new Date().toISOString(), |
| | 720 | downloadPercent: null, |
| | 721 | }); |
| | 722 | } |
| 709 | console.error(`[desktop-updater] Updater error: ${message}`); | 723 | console.error(`[desktop-updater] Updater error: ${message}`); |
| 710 | }); | 724 | }); |
| 711 | autoUpdater.on("download-progress", (progress) => { | 725 | autoUpdater.on("download-progress", (progress) => { |
| ... | @@ -715,7 +729,9 @@ function configureAutoUpdater(): void { | ... | @@ -715,7 +729,9 @@ function configureAutoUpdater(): void { |
| 715 | downloadPercent: progress.percent, | 729 | downloadPercent: progress.percent, |
| 716 | message: null, | 730 | message: null, |
| 717 | }); | 731 | }); |
| 718 | if (percent % 10 === 0) { | 732 | const milestone = percent - (percent % 10); |
| | 733 | if (milestone > lastLoggedDownloadMilestone) { |
| | 734 | lastLoggedDownloadMilestone = milestone; |
| 719 | console.info(`[desktop-updater] Download progress: ${percent}%`); | 735 | console.info(`[desktop-updater] Download progress: ${percent}%`); |
| 720 | } | 736 | } |
| 721 | }); | 737 | }); |
| ... | @@ -842,6 +858,57 @@ function stopBackend(): void { | ... | @@ -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 | function registerIpcHandlers(): void { | 912 | function registerIpcHandlers(): void { |
| 846 | ipcMain.removeHandler(PICK_FOLDER_CHANNEL); | 913 | ipcMain.removeHandler(PICK_FOLDER_CHANNEL); |
| 847 | ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { | 914 | ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { |