@@ -20,6 +20,7 @@ import * as Effect from "effect/Effect";
2020import type {
2121 DesktopTheme ,
2222 DesktopUpdateActionResult ,
23+ DesktopUpdateCheckResult ,
2324 DesktopUpdateState ,
2425} from "@t3tools/contracts" ;
2526import { autoUpdater } from "electron-updater" ;
@@ -56,6 +57,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
5657const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state" ;
5758const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download" ;
5859const UPDATE_INSTALL_CHANNEL = "desktop:update-install" ;
60+ const UPDATE_CHECK_CHANNEL = "desktop:update-check" ;
5961const GET_WS_URL_CHANNEL = "desktop:get-ws-url" ;
6062const BASE_DIR = process . env . T3CODE_HOME ?. trim ( ) || Path . join ( OS . homedir ( ) , ".t3" ) ;
6163const STATE_DIR = Path . join ( BASE_DIR , "userdata" ) ;
@@ -287,10 +289,12 @@ let updatePollTimer: ReturnType<typeof setInterval> | null = null;
287289let updateStartupTimer : ReturnType < typeof setTimeout > | null = null ;
288290let updateCheckInFlight = false ;
289291let updateDownloadInFlight = false ;
292+ let updateInstallInFlight = false ;
290293let updaterConfigured = false ;
291294let updateState : DesktopUpdateState = initialUpdateState ( ) ;
292295
293296function resolveUpdaterErrorContext ( ) : DesktopUpdateErrorContext {
297+ if ( updateInstallInFlight ) return "install" ;
294298 if ( updateDownloadInFlight ) return "download" ;
295299 if ( updateCheckInFlight ) return "check" ;
296300 return updateState . errorContext ;
@@ -754,26 +758,28 @@ function shouldEnableAutoUpdates(): boolean {
754758 ) ;
755759}
756760
757- async function checkForUpdates ( reason : string ) : Promise < void > {
758- if ( isQuitting || ! updaterConfigured || updateCheckInFlight ) return ;
761+ async function checkForUpdates ( reason : string ) : Promise < boolean > {
762+ if ( isQuitting || ! updaterConfigured || updateCheckInFlight ) return false ;
759763 if ( updateState . status === "downloading" || updateState . status === "downloaded" ) {
760764 console . info (
761765 `[desktop-updater] Skipping update check (${ reason } ) while status=${ updateState . status } .` ,
762766 ) ;
763- return ;
767+ return false ;
764768 }
765769 updateCheckInFlight = true ;
766770 setUpdateState ( reduceDesktopUpdateStateOnCheckStart ( updateState , new Date ( ) . toISOString ( ) ) ) ;
767771 console . info ( `[desktop-updater] Checking for updates (${ reason } )...` ) ;
768772
769773 try {
770774 await autoUpdater . checkForUpdates ( ) ;
775+ return true ;
771776 } catch ( error : unknown ) {
772777 const message = error instanceof Error ? error . message : String ( error ) ;
773778 setUpdateState (
774779 reduceDesktopUpdateStateOnCheckFailure ( updateState , message , new Date ( ) . toISOString ( ) ) ,
775780 ) ;
776781 console . error ( `[desktop-updater] Failed to check for updates: ${ message } ` ) ;
782+ return true ;
777783 } finally {
778784 updateCheckInFlight = false ;
779785 }
@@ -807,13 +813,22 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed
807813 }
808814
809815 isQuitting = true ;
816+ updateInstallInFlight = true ;
810817 clearUpdatePollTimer ( ) ;
811818 try {
812819 await stopBackendAndWaitForExit ( ) ;
813- autoUpdater . quitAndInstall ( ) ;
814- return { accepted : true , completed : true } ;
820+ // Destroy all windows before launching the NSIS installer to avoid the installer finding live windows it needs to close.
821+ for ( const win of BrowserWindow . getAllWindows ( ) ) {
822+ win . destroy ( ) ;
823+ }
824+ // `quitAndInstall()` only starts the handoff to the updater. The actual
825+ // install may still fail asynchronously, so keep the action incomplete
826+ // until we either quit or receive an updater error.
827+ autoUpdater . quitAndInstall ( true , true ) ;
828+ return { accepted : true , completed : false } ;
815829 } catch ( error : unknown ) {
816830 const message = formatErrorMessage ( error ) ;
831+ updateInstallInFlight = false ;
817832 isQuitting = false ;
818833 setUpdateState ( reduceDesktopUpdateStateOnInstallFailure ( updateState , message ) ) ;
819834 console . error ( `[desktop-updater] Failed to install update: ${ message } ` ) ;
@@ -850,6 +865,13 @@ function configureAutoUpdater(): void {
850865 }
851866 }
852867
868+ if ( process . env . T3CODE_DESKTOP_MOCK_UPDATES ) {
869+ autoUpdater . setFeedURL ( {
870+ provider : "generic" ,
871+ url : `http://localhost:${ process . env . T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT ?? 3000 } ` ,
872+ } ) ;
873+ }
874+
853875 autoUpdater . autoDownload = false ;
854876 autoUpdater . autoInstallOnAppQuit = false ;
855877 // Keep alpha branding, but force all installs onto the stable update track.
@@ -886,6 +908,13 @@ function configureAutoUpdater(): void {
886908 } ) ;
887909 autoUpdater . on ( "error" , ( error ) => {
888910 const message = formatErrorMessage ( error ) ;
911+ if ( updateInstallInFlight ) {
912+ updateInstallInFlight = false ;
913+ isQuitting = false ;
914+ setUpdateState ( reduceDesktopUpdateStateOnInstallFailure ( updateState , message ) ) ;
915+ console . error ( `[desktop-updater] Updater error: ${ message } ` ) ;
916+ return ;
917+ }
889918 if ( ! updateCheckInFlight && ! updateDownloadInFlight ) {
890919 setUpdateState ( {
891920 status : "error" ,
@@ -1141,6 +1170,7 @@ function registerIpcHandlers(): void {
11411170 id : item . id ,
11421171 label : item . label ,
11431172 destructive : item . destructive === true ,
1173+ disabled : item . disabled === true ,
11441174 } ) ) ;
11451175 if ( normalizedItems . length === 0 ) {
11461176 return null ;
@@ -1171,6 +1201,7 @@ function registerIpcHandlers(): void {
11711201 }
11721202 const itemOption : MenuItemConstructorOptions = {
11731203 label : item . label ,
1204+ enabled : ! item . disabled ,
11741205 click : ( ) => resolve ( item . id ) ,
11751206 } ;
11761207 if ( item . destructive ) {
@@ -1236,6 +1267,21 @@ function registerIpcHandlers(): void {
12361267 state : updateState ,
12371268 } satisfies DesktopUpdateActionResult ;
12381269 } ) ;
1270+
1271+ ipcMain . removeHandler ( UPDATE_CHECK_CHANNEL ) ;
1272+ ipcMain . handle ( UPDATE_CHECK_CHANNEL , async ( ) => {
1273+ if ( ! updaterConfigured ) {
1274+ return {
1275+ checked : false ,
1276+ state : updateState ,
1277+ } satisfies DesktopUpdateCheckResult ;
1278+ }
1279+ const checked = await checkForUpdates ( "web-ui" ) ;
1280+ return {
1281+ checked,
1282+ state : updateState ,
1283+ } satisfies DesktopUpdateCheckResult ;
1284+ } ) ;
12391285}
12401286
12411287function getIconOption ( ) : { icon : string } | Record < string , never > {
@@ -1359,6 +1405,7 @@ async function bootstrap(): Promise<void> {
13591405
13601406app . on ( "before-quit" , ( ) => {
13611407 isQuitting = true ;
1408+ updateInstallInFlight = false ;
13621409 writeDesktopLogHeader ( "before-quit received" ) ;
13631410 clearUpdatePollTimer ( ) ;
13641411 stopBackend ( ) ;
@@ -1388,7 +1435,7 @@ app
13881435 } ) ;
13891436
13901437app . on ( "window-all-closed" , ( ) => {
1391- if ( process . platform !== "darwin" ) {
1438+ if ( process . platform !== "darwin" && ! isQuitting ) {
13921439 app . quit ( ) ;
13931440 }
13941441} ) ;
0 commit comments