@@ -82,6 +82,26 @@ def teardown_simulation_app(suppress_exceptions: bool = False, make_new_stage: b
8282 omni .usd .get_context ().new_stage ()
8383
8484
85+ def _kill_child_processes () -> None :
86+ """SIGKILL all direct child processes of the current process via /proc."""
87+ import signal
88+
89+ my_pid = os .getpid ()
90+ with suppress (FileNotFoundError , PermissionError ):
91+ for entry in os .scandir ("/proc" ):
92+ if not entry .name .isdigit ():
93+ continue
94+ try :
95+ with open (f"/proc/{ entry .name } /status" ) as f :
96+ for line in f :
97+ if line .startswith ("PPid:" ):
98+ if int (line .split ()[1 ]) == my_pid :
99+ os .kill (int (entry .name ), signal .SIGKILL )
100+ break
101+ except (FileNotFoundError , PermissionError , ProcessLookupError , ValueError ):
102+ continue
103+
104+
85105class SimulationAppContext :
86106 """Context manager for launching and closing a simulation app."""
87107
@@ -115,12 +135,17 @@ def __exit__(self, exc_type, exc_val, exc_tb):
115135 os ._exit (1 )
116136
117137 # When launched as a test subprocess, skip app.close() which can hang
118- # indefinitely in Kit's shutdown path. The parent process owns the
119- # lifetime via process-group kill (see run_subprocess).
138+ # indefinitely in Kit's shutdown path.
120139 if os .environ .get ("ISAACLAB_ARENA_FORCE_EXIT_ON_COMPLETE" ) == "1" :
121140 print ("Force-exiting subprocess (ISAACLAB_ARENA_FORCE_EXIT_ON_COMPLETE=1)" )
122141 sys .stdout .flush ()
123142 sys .stderr .flush ()
143+ # SIGKILL orphaned Kit children (shader compiler, GPU workers, …)
144+ # so they don't hold GPU resources and block the next test subprocess.
145+ # We target each child individually via /proc to avoid signalling
146+ # ourselves (Kit installs a C-level SIGTERM handler that overrides
147+ # Python's SIG_IGN, so os.killpg is not safe here).
148+ _kill_child_processes ()
124149 os ._exit (0 )
125150
126151 # Normal interactive / non-test path: attempt a clean Kit shutdown.
0 commit comments