Skip to content

Commit ddcfcfa

Browse files
authored
[major fix]update the code base to avoid isaacsim shutdown issue, add auto-skill scripts, update task import order issue, skip task scanning if not using (#761)
1 parent 485e7bf commit ddcfcfa

17 files changed

Lines changed: 110 additions & 42 deletions

File tree

docs/source/metasim/concept/task.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,11 @@ env = task_cls(scenario=scenario, device=device)
8787
`make_vec` provides a standardized helper that wraps task instantiation in a **Gym‑compatible API**. It is the recommended entry point for creating environments.
8888

8989
```python
90-
import metasim # triggering the registration of tasks with Gymnasium
90+
import metasim
9191
from gymnasium import make_vec
9292

93+
metasim.register_gym_envs() # register RoboVerse/* with Gymnasium before make_vec
94+
9395
env = make_vec(
9496
env_id, # e.g., "example.my_task"
9597
num_envs=args.num_envs,
@@ -109,15 +111,17 @@ env = make_vec(
109111

110112
---
111113

112-
## 4. Task Registration & Auto‑Import
114+
## 4. Task Registration & Discovery
113115

114-
### 4.1 Auto‑import paths
116+
### 4.1 Discovery paths
115117

116-
Task modules under the following directories are **auto‑imported and registered** at runtime:
118+
Task modules under these packages are found when discovery runs (not on every `import metasim`):
117119

118120
* `metasim/example/example_pack/tasks`
119121
* `roboverse_pack/tasks`
120122

123+
Discovery is triggered by `get_task_class`, `list_tasks`, or `metasim.register_gym_envs()`.
124+
121125
> For new project tasks, place modules under **`roboverse_pack/tasks`**.
122126
123127
### 4.2 How to register a task

get_started/0_static_scene.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ def __post_init__(self):
119119
]
120120

121121
log.info(f"Using simulator: {args.sim}")
122+
if args.sim == "isaacsim":
123+
# Enable timeout-guarded close in IsaacSim handler for one-shot script usage.
124+
# GUI mode can also hang on SimulationApp.close() on some systems.
125+
os.environ["METASIM_FORCE_EXIT_ON_CLOSE"] = "1"
126+
os.environ.setdefault("METASIM_CLOSE_TIMEOUT_SEC", "8")
122127
handler = get_handler(scenario)
123128
init_states = [
124129
{

get_started/9_cfg_task.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import torch
2222
from gymnasium import make_vec
2323

24-
import metasim # noqa: F401
24+
import metasim
25+
26+
metasim.register_gym_envs()
2527
from metasim.scenario.cameras import PinholeCameraCfg
2628
from metasim.utils import configclass
2729
from metasim.utils.obs_utils import ObsSaver

get_started/dexhands/0_control_dexhands.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import torch
2121
from gymnasium import make_vec
2222

23-
import metasim # noqa: F401
23+
import metasim
24+
25+
metasim.register_gym_envs()
2426

2527
# Ensure the new env class is imported so its @register_task decorators run.
2628
# Adjust the import path if you place xhand_env.py elsewhere.

get_started/rl/0_ppo_gym_style.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
# from metasim.task.gym_registration import make_vec
2727
from gymnasium import make_vec
2828

29-
import metasim # noqa: F401
29+
import metasim
30+
31+
metasim.register_gym_envs()
3032
from metasim.scenario.cameras import PinholeCameraCfg
3133
from metasim.utils.obs_utils import ObsSaver
3234

metasim/__init__.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
"""All metasim packages.
2-
3-
Importing this top-level package ensures tasks are discovered and registered
4-
with Gymnasium (single and vector) so users can call gymnasium.make and
5-
gymnasium.make_vec without manual registration calls.
6-
"""
7-
8-
# Trigger task discovery and Gymnasium registration on package import.
9-
try: # pragma: no cover
10-
import metasim.task # noqa: F401
11-
except Exception:
12-
# Best-effort; individual scripts can still register on-demand if needed.
13-
pass
1+
"""All metasim packages."""
2+
3+
from __future__ import annotations
4+
5+
6+
def register_gym_envs() -> None:
7+
"""Discover task modules and register ``RoboVerse/<task>`` with Gymnasium.
8+
9+
Call this before ``gymnasium.make`` / ``make_vec`` with RoboVerse env ids.
10+
Sim-only code paths do not need this.
11+
"""
12+
from metasim.task.gym_registration import register_all_tasks_with_gym
13+
14+
register_all_tasks_with_gym()

metasim/queries/contact_force.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import TYPE_CHECKING
4+
35
try:
46
import isaacgym
57
except ImportError:
@@ -11,7 +13,9 @@
1113
import torch
1214

1315
from metasim.queries.base import BaseQueryType
14-
from metasim.sim.base import BaseSimHandler
16+
17+
if TYPE_CHECKING:
18+
from metasim.sim.base import BaseSimHandler
1519

1620
try:
1721
import mujoco

metasim/sim/isaacsim/isaacsim.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import argparse
66
import math
77
import os
8+
import threading
89
from copy import deepcopy
910

1011
import numpy as np
@@ -74,6 +75,10 @@ def __init__(self, scenario_cfg: ScenarioCfg, optional_queries: list[BaseQueryTy
7475
self._is_closed = False
7576
self.render_interval = self.scenario.decimation # TODO: fix hardcode
7677
self._manual_pd_on = []
78+
self._owns_simulation_app = False
79+
self.simulation_app = None
80+
self.scene = None
81+
self.sim = None
7782

7883
if self.headless:
7984
self._render_viewport = False
@@ -94,8 +99,10 @@ def _init_scene(self, simulation_app=None, args=None) -> None:
9499
args.headless = self.headless
95100
app_launcher = AppLauncher(args)
96101
self.simulation_app = app_launcher.app
102+
self._owns_simulation_app = True
97103
else:
98104
self.simulation_app = simulation_app
105+
self._owns_simulation_app = False
99106

100107
# physics context
101108
from isaaclab.scene import InteractiveScene, InteractiveSceneCfg
@@ -216,7 +223,44 @@ def launch(self, simulation_app=None, simulation_args=None) -> None:
216223
def close(self) -> None:
217224
log.info("close Isaacsim Handler")
218225
if not self._is_closed:
219-
self.simulation_app.close()
226+
force_exit_on_close_hang = os.getenv("METASIM_FORCE_EXIT_ON_CLOSE", "0") == "1"
227+
close_timeout_s = float(os.getenv("METASIM_CLOSE_TIMEOUT_SEC", "8"))
228+
simulation_app = getattr(self, "simulation_app", None)
229+
owns_simulation_app = getattr(self, "_owns_simulation_app", False)
230+
231+
if simulation_app is not None and force_exit_on_close_hang and owns_simulation_app:
232+
close_error: dict[str, Exception] = {}
233+
234+
def _close_sim_app() -> None:
235+
try:
236+
# Isaac Sim may hang on shutdown waiting for Replicator workers.
237+
simulation_app.close(wait_for_replicator=False)
238+
except TypeError:
239+
# Backward compatibility with versions where close() has no kwargs.
240+
simulation_app.close()
241+
except Exception as err:
242+
close_error["error"] = err
243+
244+
close_thread = threading.Thread(target=_close_sim_app, daemon=True)
245+
close_thread.start()
246+
close_thread.join(timeout=close_timeout_s)
247+
if close_thread.is_alive():
248+
log.warning(
249+
f"SimulationApp.close() exceeded {close_timeout_s:.1f}s. "
250+
"Forcing process exit to avoid shutdown hang."
251+
)
252+
import sys
253+
254+
sys.stdout.flush()
255+
sys.stderr.flush()
256+
os._exit(0)
257+
if "error" in close_error:
258+
raise close_error["error"]
259+
elif simulation_app is not None:
260+
try:
261+
simulation_app.close(wait_for_replicator=False)
262+
except TypeError:
263+
simulation_app.close()
220264
if self.scene is not None:
221265
del self.scene
222266
if self.sim is not None:

metasim/task/__init__.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
1-
"""Auto-import all submodules in the tasks package.
1+
"""Task registry, base env types, and optional Gymnasium integration.
22
3-
This ensures that all tasks decorated with @register_task are registered
4-
when the package is imported.
3+
Submodules here are not bulk-imported on package load. Discovery runs only when
4+
you call ``get_task_class`` / ``list_tasks`` or ``register_all_tasks_with_gym``
5+
(see ``metasim.register_gym_envs``), so sim-only scripts stay lightweight.
56
"""
67

78
from __future__ import annotations
8-
9-
# After tasks are discoverable, automatically register all tasks with Gymnasium
10-
# so users can call gymnasium.make / gymnasium.make_vec without manual steps.
11-
try:
12-
from .gym_registration import register_all_tasks_with_gym as _register_all_tasks_with_gym
13-
from .registry import _discover_task_modules
14-
15-
_discover_task_modules()
16-
_register_all_tasks_with_gym()
17-
except Exception:
18-
# Best-effort: scripts can still call register_task_with_gym on-demand.
19-
pass

metasim/task/registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def _discover_task_modules() -> None:
8686
try:
8787
import_module(module_name)
8888
except Exception as e:
89-
log.error(f"Task discovery: failed to import module '{module_name}': {e}")
89+
log.debug(f"Task discovery: skip module '{module_name}': {e}")
9090
except Exception as e:
9191
log.error(f"Task discovery: error scanning package '{pkg_name}': {e}")
9292

0 commit comments

Comments
 (0)