@@ -2924,31 +2924,49 @@ def health_check(self) -> bool:
29242924 return False
29252925
29262926 def force_shutdown (self ):
2927- """Forcefully shutdown worker."""
2927+ """Forcefully shutdown worker using a belt-and-suspenders approach ."""
29282928 with self .lock :
29292929 if self .process :
29302930 try :
2931- # First check if already dead (workers that ran run_cli call sys.exit)
2931+ # 1. Check if already dead
29322932 if self .process .poll () is None :
2933- # Still running — try graceful shutdown
2933+ # Try graceful shutdown via stdin first
29342934 try :
29352935 self .process .stdin .write (json .dumps ({"type" : "shutdown" }) + "\n " )
29362936 self .process .stdin .flush ()
29372937 except Exception :
29382938 pass
2939- self . process . wait ( timeout = 2 )
2940- except Exception :
2939+
2940+ # Give it a tiny window to exit gracefully
29412941 try :
2942- if not IS_WINDOWS :
2943- os .killpg (os .getpgid (self .process .pid ), signal .SIGKILL )
2944- else :
2945- self .process .terminate ()
2942+ self .process .wait (timeout = 1 )
29462943 except Exception :
29472944 pass
2945+ except Exception :
2946+ pass
2947+
2948+ # 2. THE NUCLEAR STRIKE
2949+ try :
2950+ if not IS_WINDOWS :
2951+ # Linux: Kill the entire process group
2952+ os .killpg (os .getpgid (self .process .pid ), signal .SIGKILL )
2953+ else :
2954+ # Windows: Step A - Standard Python termination
2955+ self .process .terminate ()
2956+
2957+ # Windows: Step B - Recursive Tree Kill via taskkill
2958+ # This nukes the process AND every single child/grandchild it spawned
2959+ subprocess .run (
2960+ ["taskkill" , "/F" , "/T" , "/PID" , str (self .process .pid )],
2961+ capture_output = True ,
2962+ check = False
2963+ )
2964+ except Exception :
2965+ pass
29482966 finally :
29492967 self .process = None
29502968
2951- # 🔥 FIX: Close log file handle
2969+ # Close handles to release file locks immediately
29522970 if hasattr (self , "log_file" ) and self .log_file :
29532971 try :
29542972 self .log_file .close ()
@@ -5015,8 +5033,8 @@ def _handle_shutdown(self, signum, frame):
50155033 # Shutdown executor first (non-blocking)
50165034 try :
50175035 self .executor .shutdown (wait = False )
5018- except Exception :
5019- pass
5036+ except Exception as e :
5037+ self . log ( f"[SHUTDOWN] ⚠️ Executor shutdown failed: { e } " )
50205038
50215039 # Collect all PIDs before we start killing
50225040 all_pids = []
@@ -5026,8 +5044,8 @@ def _handle_shutdown(self, signum, frame):
50265044 try :
50275045 if worker .process and worker .process .pid :
50285046 all_pids .append (worker .process .pid )
5029- except Exception :
5030- pass
5047+ except Exception as e :
5048+ self . log ( f" ⚠️ Could not retrieve PID for UV worker { py_exe } : { e } " )
50315049
50325050 # Idle CLI workers
50335051 for py_exe , pool in list (getattr (self , "idle_pools" , {}).items ()):
@@ -5036,8 +5054,8 @@ def _handle_shutdown(self, signum, frame):
50365054 w = pool .get_nowait ()
50375055 if w .process and w .process .pid :
50385056 all_pids .append (w .process .pid )
5039- except Exception :
5040- pass
5057+ except Exception as e :
5058+ self . log ( f" ⚠️ Could not retrieve PID from idle pool { py_exe } : { e } " )
50415059
50425060 # Spec workers
50435061 with self .pool_lock :
@@ -5049,13 +5067,14 @@ def _handle_shutdown(self, signum, frame):
50495067 if w and w .process and w .process .pid :
50505068 all_pids .append (w .process .pid )
50515069 info ["worker" ].force_shutdown ()
5052- except Exception :
5053- pass
5070+ except Exception as e :
5071+ self . log ( f" 🚨 Critical failure during worker force_shutdown: { e } " )
50545072
50555073 # Wait up to 2s for graceful termination
50565074 import time as _st
50575075 deadline = _st .time () + 2.0
50585076 remaining = list (all_pids )
5077+ self .log (f"[SHUTDOWN] Waiting for { len (remaining )} processes to exit gracefully..." )
50595078 while remaining and _st .time () < deadline :
50605079 _st .sleep (0.05 )
50615080 still_alive = []
@@ -5071,23 +5090,36 @@ def _handle_shutdown(self, signum, frame):
50715090 os .kill (pid , 0 )
50725091 still_alive .append (pid )
50735092 except (OSError , Exception ):
5074- pass
5093+ # Process is gone - this is the desired outcome
5094+ pass
50755095 remaining = still_alive
50765096
50775097 # Force kill anything still alive
50785098 for pid in remaining :
50795099 try :
50805100 if IS_WINDOWS :
5101+ # 1. THE NUCLEAR STRIKE FIRST
5102+ # We kill the tree while the parent is still alive
5103+ # so Windows can correctly identify and kill all descendants.
5104+ subprocess .run (
5105+ ["taskkill" , "/F" , "/T" , "/PID" , str (pid )],
5106+ capture_output = True , check = False
5107+ )
5108+
5109+ # 2. THE CLEANUP (Belt and Suspenders)
5110+ # Just in case taskkill missed the root for some bizarre reason
50815111 import ctypes
50825112 handle = ctypes .windll .kernel32 .OpenProcess (1 , False , pid )
50835113 if handle :
50845114 ctypes .windll .kernel32 .TerminateProcess (handle , 1 )
50855115 ctypes .windll .kernel32 .CloseHandle (handle )
50865116 else :
50875117 os .kill (pid , signal .SIGKILL )
5088- self .log (f" Force-killed straggler PID { pid } " )
5089- except Exception :
5090- pass
5118+
5119+ self .log (f" Force-killed tree for straggler PID { pid } " )
5120+ except Exception as e :
5121+ # 🔥 LOG THE ERROR: Now we know if it was AccessDenied or ProcessNotFound
5122+ self .log (f" 🚨 Failed to kill PID { pid } : { e } " )
50915123 # Stop fs_watcher BEFORE cleaning up socket/pid files
50925124 # The watchdog Observer thread is non-daemon and will block sys.exit forever
50935125 # if we don't explicitly stop it here.
@@ -5106,14 +5138,18 @@ def _handle_shutdown(self, signum, frame):
51065138 continue
51075139 try :
51085140 os .unlink (path )
5109- except OSError :
5110- pass
5141+ except FileNotFoundError :
5142+ pass # Already gone, no need to log
5143+ except Exception as e :
5144+ # 🔥 LOG THE ERROR: This tells us if a process is still locking the PID file
5145+ self .log (f" ⚠️ Failed to unlink { path } : { e } " )
51115146 # Also clean connection announcement file
51125147 try :
51135148 conn_file = os .path .join (tempfile .gettempdir (), "omnipkg" , "daemon_connection.txt" )
51145149 os .unlink (conn_file )
5115- except OSError :
5116- pass
5150+ except Exception as e :
5151+ # 🔥 LOG THE ERROR: Now we can see if the temp folder has permission issues
5152+ self .log (f" ⚠️ Failed to unlink connection file { conn_file } : { e } " )
51175153
51185154 @classmethod
51195155 def is_running (cls ) -> bool :
0 commit comments