Skip to content

Commit 18c8e36

Browse files
authored
Add CloudXRLauncher for programmatic runtime lifecycle management (#353)
* Working launcher.py script
1 parent 9fd148e commit 18c8e36

13 files changed

Lines changed: 1113 additions & 73 deletions

File tree

.github/workflows/build-ubuntu.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ jobs:
235235
env:
236236
ACCEPT_CLOUDXR_EULA: Y
237237
CXR_BUILD_CONTEXT: ${{ github.workspace }}
238+
CXR_RUNTIME_NETWORK_MODE: bridge
238239
PYTHON_VERSION: ${{ matrix.python_version }}
239240
ISAACTELEOP_PIP_DEBUG: "0"
240241
ISAACTELEOP_PIP_FIND_LINKS: /workspace/install/wheels

deps/cloudxr/docker-compose.test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

44
# Test docker-compose override for running Isaac Teleop tests with CloudXR.

src/core/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ if(BUILD_TESTING)
6565
# Teleop session manager tests (Python)
6666
add_subdirectory(teleop_session_manager_tests)
6767

68+
# CloudXR tests (Python — requires the built wheel from python/)
69+
if(BUILD_PYTHON_BINDINGS)
70+
add_subdirectory(cloudxr_tests)
71+
endif()
72+
6873
# MCAP tests (C++)
6974
add_subdirectory(mcap_tests/cpp)
7075
endif()

src/core/cloudxr/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ add_custom_target(cloudxr_python ALL
108108
"${CMAKE_CURRENT_SOURCE_DIR}/__init__.py"
109109
"${CMAKE_CURRENT_SOURCE_DIR}/__main__.py"
110110
"${CMAKE_CURRENT_SOURCE_DIR}/env_config.py"
111+
"${CMAKE_CURRENT_SOURCE_DIR}/launcher.py"
111112
"${CMAKE_CURRENT_SOURCE_DIR}/runtime.py"
112113
"${CMAKE_CURRENT_SOURCE_DIR}/wss.py"
113114
"${CLOUDXR_PYTHON_DIR}/"

src/core/cloudxr/python/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
"""CloudXR integration for isaacteleop."""
5+
6+
from .launcher import CloudXRLauncher
7+
8+
__all__ = ["CloudXRLauncher"]

src/core/cloudxr/python/__main__.py

Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,18 @@
44
"""Entry point for python -m isaacteleop.cloudxr. Runs CloudXR runtime and WSS proxy; main process winds both down on exit."""
55

66
import argparse
7-
import asyncio
8-
import multiprocessing
97
import os
108
import signal
11-
import sys
12-
from datetime import datetime, timezone
9+
import time
1310

1411
from isaacteleop import __version__ as isaacteleop_version
15-
from isaacteleop.cloudxr.env_config import EnvConfig
16-
from isaacteleop.cloudxr.runtime import (
17-
check_eula,
18-
latest_runtime_log,
19-
run as runtime_run,
20-
runtime_version,
21-
terminate_or_kill_runtime,
22-
wait_for_runtime_ready,
23-
)
24-
from isaacteleop.cloudxr.wss import run as wss_run
12+
from isaacteleop.cloudxr.env_config import get_env_config
13+
from isaacteleop.cloudxr.launcher import CloudXRLauncher
14+
from isaacteleop.cloudxr.runtime import latest_runtime_log, runtime_version
2515

2616

2717
def _parse_args() -> argparse.Namespace:
18+
"""Parse command-line arguments for the CloudXR runtime entry point."""
2819
parser = argparse.ArgumentParser(description="CloudXR runtime and WSS proxy")
2920
parser.add_argument(
3021
"--cloudxr-install-dir",
@@ -48,60 +39,50 @@ def _parse_args() -> argparse.Namespace:
4839
return parser.parse_args()
4940

5041

51-
async def _main_async() -> None:
42+
def main() -> None:
43+
"""Launch the CloudXR runtime and WSS proxy, then block until interrupted."""
5244
args = _parse_args()
53-
env_cfg = EnvConfig.from_args(args.cloudxr_install_dir, args.cloudxr_env_config)
54-
check_eula(accept_eula=args.accept_eula or None)
55-
logs_dir_path = env_cfg.ensure_logs_dir()
56-
wss_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")
57-
wss_log_path = logs_dir_path / f"wss.{wss_ts}.log"
58-
59-
runtime_proc = multiprocessing.Process(target=runtime_run)
60-
runtime_proc.start()
61-
62-
cxr_ver = runtime_version()
63-
print(
64-
f"Running Isaac Teleop \033[36m{isaacteleop_version}\033[0m, CloudXR Runtime \033[36m{cxr_ver}\033[0m"
65-
)
66-
67-
try:
68-
ready = await wait_for_runtime_ready(runtime_proc)
69-
if not ready:
70-
if not runtime_proc.is_alive() and runtime_proc.exitcode != 0:
71-
sys.exit(
72-
runtime_proc.exitcode if runtime_proc.exitcode is not None else 1
73-
)
74-
print("CloudXR runtime failed to start, terminating...")
75-
sys.exit(1)
76-
77-
stop = asyncio.get_running_loop().create_future()
7845

79-
def on_signal() -> None:
80-
if not stop.done():
81-
stop.set_result(None)
82-
83-
loop = asyncio.get_running_loop()
84-
for sig in (signal.SIGINT, signal.SIGTERM):
85-
loop.add_signal_handler(sig, on_signal)
46+
with CloudXRLauncher(
47+
install_dir=args.cloudxr_install_dir,
48+
env_config=args.cloudxr_env_config,
49+
accept_eula=args.accept_eula,
50+
) as launcher:
51+
cxr_ver = runtime_version()
52+
print(
53+
f"Running Isaac Teleop \033[36m{isaacteleop_version}\033[0m, CloudXR Runtime \033[36m{cxr_ver}\033[0m"
54+
)
8655

56+
env_cfg = get_env_config()
57+
logs_dir_path = env_cfg.ensure_logs_dir()
8758
cxr_log = latest_runtime_log() or logs_dir_path
8859
print(
8960
f"CloudXR runtime: \033[36mrunning\033[0m, log file: \033[90m{cxr_log}\033[0m"
9061
)
62+
wss_log = launcher.wss_log_path
9163
print(
92-
f"CloudXR WSS proxy: \033[36mrunning\033[0m, log file: \033[90m{wss_log_path}\033[0m"
64+
f"CloudXR WSS proxy: \033[36mrunning\033[0m, log file: \033[90m{wss_log}\033[0m"
9365
)
9466
print(
9567
f"Activate CloudXR environment in another terminal: \033[1;32msource {env_cfg.env_filepath()}\033[0m"
9668
)
9769
print("\033[33mKeep this terminal open, Ctrl+C to terminate.\033[0m")
9870

99-
await wss_run(log_file_path=wss_log_path, stop_future=stop)
100-
finally:
101-
terminate_or_kill_runtime(runtime_proc)
71+
stop = False
72+
73+
def on_signal(sig, frame):
74+
nonlocal stop
75+
stop = True
76+
77+
signal.signal(signal.SIGINT, on_signal)
78+
signal.signal(signal.SIGTERM, on_signal)
79+
80+
while not stop:
81+
launcher.health_check()
82+
time.sleep(0.1)
10283

10384
print("Stopped.")
10485

10586

10687
if __name__ == "__main__":
107-
asyncio.run(_main_async())
88+
main()

0 commit comments

Comments
 (0)