|
31 | 31 | _AT_LEAST_ONE_TEST_FAILED = False |
32 | 32 |
|
33 | 33 |
|
34 | | -def run_subprocess(cmd, env=None): |
35 | | - print(f"Running command: {cmd}") |
| 34 | +_SUBPROCESS_TIMEOUT_SEC = int(os.environ.get("ISAACLAB_ARENA_SUBPROCESS_TIMEOUT", "600")) |
| 35 | + |
| 36 | + |
| 37 | +def run_subprocess( |
| 38 | + cmd, |
| 39 | + env=None, |
| 40 | + timeout_sec: int | None = None, |
| 41 | + capture_output: bool = False, |
| 42 | +) -> subprocess.CompletedProcess | None: |
| 43 | + """Run a command in a subprocess with timeout. |
| 44 | +
|
| 45 | + The child is launched with ``start_new_session=True`` so it lives in its |
| 46 | + own process group. The child-side ``SimulationAppContext`` uses this to |
| 47 | + SIGTERM its entire group before ``os._exit()``, preventing orphaned Kit |
| 48 | + children (shader compiler, GPU workers, …) from holding GPU resources and |
| 49 | + blocking the next subprocess. |
| 50 | +
|
| 51 | + Args: |
| 52 | + cmd: Command to run (list of strings). |
| 53 | + env: Optional environment dict. Defaults to inheriting the parent env. |
| 54 | + timeout_sec: Per-subprocess wall-clock timeout in seconds. |
| 55 | + Defaults to ``_SUBPROCESS_TIMEOUT_SEC`` (env ``ISAACLAB_ARENA_SUBPROCESS_TIMEOUT``, fallback 600). |
| 56 | + capture_output: If True, capture stdout/stderr and return a |
| 57 | + ``CompletedProcess``. When False (default) output streams to |
| 58 | + the parent process and the function returns None on success. |
| 59 | +
|
| 60 | + Returns: |
| 61 | + ``CompletedProcess`` when *capture_output* is True, else None. |
| 62 | + """ |
| 63 | + if timeout_sec is None: |
| 64 | + timeout_sec = _SUBPROCESS_TIMEOUT_SEC |
| 65 | + |
| 66 | + print(f"Running command (timeout={timeout_sec}s): {cmd}") |
36 | 67 | global _AT_LEAST_ONE_TEST_FAILED |
| 68 | + |
| 69 | + if env is None: |
| 70 | + env = os.environ.copy() |
| 71 | + env["ISAACLAB_ARENA_FORCE_EXIT_ON_COMPLETE"] = "1" |
| 72 | + |
37 | 73 | try: |
38 | 74 | result = subprocess.run( |
39 | 75 | cmd, |
40 | | - check=True, |
41 | 76 | env=env, |
42 | | - # Don't capture output, let it flow through in real-time |
43 | | - capture_output=False, |
44 | | - text=True, |
45 | | - # Explicitly set stdout and stderr to None to use parent process's pipes |
46 | | - stdout=None, |
47 | | - stderr=None, |
| 77 | + timeout=timeout_sec, |
| 78 | + capture_output=capture_output, |
| 79 | + text=capture_output, |
| 80 | + start_new_session=True, |
48 | 81 | ) |
49 | | - print(f"Command completed with return code: {result.returncode}") |
50 | | - except subprocess.CalledProcessError as e: |
51 | | - sys.stderr.write(f"Command failed with return code {e.returncode}: {e}\n") |
| 82 | + except subprocess.TimeoutExpired: |
| 83 | + sys.stderr.write(f"\n[isaaclab-arena] Subprocess timed out after {timeout_sec}s\n") |
52 | 84 | _AT_LEAST_ONE_TEST_FAILED = True |
53 | | - raise e |
| 85 | + raise subprocess.SubprocessError(f"Subprocess timed out after {timeout_sec}s: {cmd}") |
| 86 | + |
| 87 | + print(f"Command completed with return code: {result.returncode}") |
| 88 | + if result.returncode != 0: |
| 89 | + sys.stderr.write(f"Command failed with return code {result.returncode}\n") |
| 90 | + if capture_output and result.stderr: |
| 91 | + sys.stderr.write(result.stderr) |
| 92 | + _AT_LEAST_ONE_TEST_FAILED = True |
| 93 | + raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr) |
| 94 | + |
| 95 | + if capture_output: |
| 96 | + return result |
| 97 | + return None |
54 | 98 |
|
55 | 99 |
|
56 | 100 | class _IsolatedArgv: |
@@ -108,7 +152,7 @@ def get_persistent_simulation_app(headless: bool, enable_cameras: bool = False) |
108 | 152 | first_headless, first_enable_cameras = _PERSISTENT_INIT_ARGS |
109 | 153 | if (headless != first_headless) or (enable_cameras != first_enable_cameras): |
110 | 154 | print( |
111 | | - "[isaac-arena] Warning: persistent SimulationApp already initialized with " |
| 155 | + "[isaaclab-arena] Warning: persistent SimulationApp already initialized with " |
112 | 156 | f"headless={first_headless}, enable_cameras={first_enable_cameras}. " |
113 | 157 | "Ignoring new values." |
114 | 158 | ) |
|
0 commit comments