1- import { app , BrowserWindow , ipcMain , dialog , Menu , shell } from 'electron'
1+ import { app , autoUpdater as nativeAutoUpdater , BrowserWindow , ipcMain , dialog , Menu , shell } from 'electron'
22import { autoUpdater } from 'electron-updater'
33import { existsSync , readdirSync , statSync } from 'fs'
44import { homedir } from 'os'
@@ -705,14 +705,19 @@ function registerIpcHandlers(): void {
705705 } )
706706
707707 ipcMain . handle ( 'updater:quitAndInstall' , ( ) => {
708- log ( 'updater' , 'quitAndInstall requested — cleaning up for fast exit ' )
708+ log ( 'updater' , 'quitAndInstall requested — tearing down before handing off to Squirrel ' )
709709 try {
710710 stopWatchingStatus ?.( )
711711 stopWatchingStatus = null
712712 } catch ( err ) {
713713 log ( 'updater' , 'stopWatchingStatus failed' , err instanceof Error ? err . message : String ( err ) )
714714 }
715715 try {
716+ // Kill the whole PTY process group (zsh + claude + any grandchildren),
717+ // not just the direct shell child. Leaving descendants alive keeps
718+ // libuv handles attached on our side, which makes Electron's quit
719+ // sequence hang — and Squirrel.Mac then bails with "original process
720+ // did not end" before ShipIt swaps the bundle.
716721 ptyManager . killAll ( 'SIGKILL' )
717722 } catch ( err ) {
718723 log ( 'updater' , 'ptyManager.killAll failed' , err instanceof Error ? err . message : String ( err ) )
@@ -724,18 +729,10 @@ function registerIpcHandlers(): void {
724729 log ( 'updater' , 'final persistence failed' , err instanceof Error ? err . message : String ( err ) )
725730 }
726731
727- // Skip our before-quit handler — it's already been done above, and it can
728- // hang waiting for PTY fds to drain. We just want Squirrel to see us gone.
732+ // Skip our before-quit handler — we just did its work above.
729733 app . removeAllListeners ( 'before-quit' )
730734
731- // Hard-exit fallback. If Squirrel/Electron's quit takes longer than ~1.5s,
732- // force-kill the process so ShipIt's "target still running" check passes.
733- setTimeout ( ( ) => {
734- log ( 'updater' , 'fallback app.exit(0) — Squirrel should take over' )
735- app . exit ( 0 )
736- } , 1500 )
737-
738- autoUpdater . quitAndInstall ( true , false )
735+ autoUpdater . quitAndInstall ( true , true )
739736 return true
740737 } )
741738
@@ -888,10 +885,23 @@ function setupAutoUpdater(): void {
888885 broadcastToAllWindows ( 'updater:status' , { state : 'downloaded' , version : info . version } )
889886 } )
890887
891- // Check on startup, then every 10 minutes
892- autoUpdater . checkForUpdatesAndNotify ( ) . catch ( ( err ) => log ( 'updater' , 'check failed' , err . message ) )
888+ // Also log native Squirrel.Mac errors. electron-updater wraps Squirrel via
889+ // its own MacUpdater but doesn't surface errors from the native side, so
890+ // things like "target app still running" or codesign mismatches would
891+ // otherwise be invisible. These are the errors that would have diagnosed
892+ // previous OTA loops in one glance.
893+ if ( process . platform === 'darwin' ) {
894+ nativeAutoUpdater . on ( 'error' , ( err ) => {
895+ log ( 'updater' , `[error] Squirrel.Mac: ${ err . message } ` )
896+ } )
897+ }
898+
899+ // Check on startup, then every 10 minutes. We use checkForUpdates (not
900+ // checkForUpdatesAndNotify) so there's no native OS notification — the
901+ // renderer shows an in-app banner based on the updater:status events.
902+ autoUpdater . checkForUpdates ( ) . catch ( ( err ) => log ( 'updater' , 'check failed' , err . message ) )
893903 setInterval ( ( ) => {
894- autoUpdater . checkForUpdatesAndNotify ( ) . catch ( ( ) => { } )
904+ autoUpdater . checkForUpdates ( ) . catch ( ( ) => { } )
895905 } , 10 * 60 * 1000 )
896906}
897907
@@ -951,24 +961,14 @@ app.on('window-all-closed', () => {
951961 }
952962} )
953963
954- let quitWatchdogArmed = false
955964app . on ( 'before-quit' , ( ) => {
956965 stopWatchingStatus ?.( )
957966 stopWatchingStatus = null
958- ptyManager . killAll ( )
967+ // SIGKILL the whole PTY process group so zsh + claude + grandchildren
968+ // all die immediately and release their libuv handles. Without this the
969+ // main process can hang draining fds and Squirrel.Mac will abort an
970+ // in-flight bundle swap with "original process did not end".
971+ ptyManager . killAll ( 'SIGKILL' )
959972 sealAllActive ( )
960973 saveConfigSync ( config )
961-
962- // Force-exit watchdog: if a PTY child (stuck claude / shell) won't die after
963- // SIGHUP, the main process can hang indefinitely draining fds. Give the
964- // graceful path ~1.5s, then hard-exit. electron-updater's ShipIt helper has
965- // already been spawned by this point if an update is pending, so force-exit
966- // is safe — ShipIt runs independently and swaps the bundle once we're gone.
967- if ( ! quitWatchdogArmed ) {
968- quitWatchdogArmed = true
969- setTimeout ( ( ) => {
970- log ( 'app' , 'quit watchdog fired — force-exiting' )
971- app . exit ( 0 )
972- } , 1500 ) . unref ( )
973- }
974974} )
0 commit comments