Skip to content

fix(cloudxr): refuse to start over a live runtime; explain -35 - #911

Open
jiwenc-nv wants to merge 1 commit into
mainfrom
jiwenc-nv/IT908
Open

fix(cloudxr): refuse to start over a live runtime; explain -35#911
jiwenc-nv wants to merge 1 commit into
mainfrom
jiwenc-nv/IT908

Conversation

@jiwenc-nv

@jiwenc-nv jiwenc-nv commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Description

Items 1 and 2 of #736.

_cleanup_stale_runtime treated an existing ipc_cloudxr as proof of staleness and fuser -k -TERMed its holder — including a healthy runtime mid-session, which the first session saw as a broken pipe. With fuser absent the error was swallowed and the socket unlinked anyway, so the old process survived; hence the manual rm.

Liveness is now a connect(). Live → refuse, naming the resolved env file and --no-launch-cloudxr-runtime; replacing it means stopping it yourself. Dead → clean, at WARNING. Ambiguous probe errors count as live. Nothing signals another process now, so the undeclared fuser/psmisc dependency goes too.

Both -35 throw sites now name XR_ERROR_FORM_FACTOR_UNAVAILABLE and point at the one new docs section, carrying the -51 contrast and the checklist. The banner prints the resolved device profile, and the two device-profile defaults collapse onto one constant — docs said auto-webrtc, Quest3 always won.

#908 also proposed --force and --cloudxr-device-profile; both dropped as unnecessary.

Fixes #908

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Testing

  • 193 tests pass (src/core/cloudxr_tests/python/), covering the probe and the refusal.
  • C++ compiles; clang-format, Sphinx, and SKIP=check-copyright-year pre-commit run --all-files clean.
  • Jetson Thor, live runtime: second launch refused, exit 1, runtime survived. It unlinks its socket on clean exit, so the WARNING marks a crash.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the linter and formatter with SKIP=check-copyright-year pre-commit run --all-files
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix/feature works (or explained why not)
  • I have signed off all my commits (git commit -s) per the DCO

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

CloudXR runtime startup now probes ipc_cloudxr to distinguish live runtimes from stale socket files. A second launch fails unless --force is used, which sends SIGTERM and waits for termination. The default NV_DEVICE_PROFILE is now Quest3, and startup reports the resolved profile. OpenXR errors for unavailable headsets now include diagnostics. Documentation and tests cover runtime takeover, profile resolution, socket liveness, and wait behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant CloudXRLauncher
  participant RuntimeIPC
  participant ExistingRuntime
  CLI->>CloudXRLauncher: Start with optional --force
  CloudXRLauncher->>RuntimeIPC: Probe ipc_cloudxr
  RuntimeIPC-->>CloudXRLauncher: Return liveness
  alt Existing runtime is live
    CloudXRLauncher->>ExistingRuntime: Send SIGTERM when forced
    CloudXRLauncher->>RuntimeIPC: Wait for termination
  else Socket is stale
    CloudXRLauncher->>RuntimeIPC: Remove stale socket
  end
  CloudXRLauncher-->>CLI: Start runtime or report error
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR meets the liveness, takeover, cleanup, profile, -35, testing, and documentation objectives, but omits the requested device-profile CLI flag [#908]. Add a --cloudxr-device-profile option and pass its value into launcher or environment configuration before startup.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The code, tests, and documentation changes directly support the linked issue objectives and do not introduce unrelated scope.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: preventing startup over a live runtime and improving explanation of error -35.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jiwenc-nv/IT908

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/python/isaacteleop/cloudxr/launcher.py`:
- Around line 594-636: The launch flow currently relies on the point-in-time
is_runtime_live check and force-mode socket disappearance, so runtime ownership
is not atomic and shutdown is not confirmed. Add an exclusive run-directory
ownership lock that remains held from startup through the runtime lifetime,
acquire it before checking or removing stale state, and ensure it is released
only when the launcher exits. Update _terminate_live_runtime to identify and
wait for the prior owner process to exit, not merely until is_runtime_live
returns false, before cleanup and replacement startup proceed.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 90cb1f88-3f35-4695-a691-bdb2dabd7a7d

📥 Commits

Reviewing files that changed from the base of the PR and between 805b95f and 14c8c2e.

📒 Files selected for processing (11)
  • docs/source/getting_started/quick_start.rst
  • docs/source/getting_started/televiz.rst
  • docs/source/references/cloudxr.rst
  • src/core/cloudxr_tests/python/test_launcher.py
  • src/core/cloudxr_tests/python/test_runtime.py
  • src/core/oxr/cpp/oxr_session.cpp
  • src/python/isaacteleop/cloudxr/__main__.py
  • src/python/isaacteleop/cloudxr/env_config.py
  • src/python/isaacteleop/cloudxr/launcher.py
  • src/python/isaacteleop/cloudxr/runtime.py
  • src/viz/xr/cpp/openxr_session.cpp

Comment on lines +594 to +636
if is_runtime_live(run_dir):
if not force:
raise RuntimeError(
f"A CloudXR runtime is already serving {run_dir}; starting a "
"second one would drop the live session. To use the running "
f"runtime: source {env_cfg.env_filepath()} and pass "
"--no-launch-cloudxr-runtime. To take it over: "
"python -m isaacteleop.cloudxr --force, or "
"CloudXRLauncher(force=True)."
)
if result.returncode == 0:
time.sleep(1)
logger.info("Sent SIGTERM to processes holding stale IPC socket")
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
CloudXRLauncher._terminate_live_runtime(run_dir, ipc_socket)

for name in ("ipc_cloudxr", "runtime_started", "monado.pid", "cloudxr.pid"):
path = os.path.join(run_dir, name)
try:
os.remove(ipc_socket)
os.remove(path)
except FileNotFoundError:
pass
continue
logger.warning("Removed stale CloudXR runtime file %s", path)

for name in ("runtime_started", "monado.pid", "cloudxr.pid"):
try:
os.remove(os.path.join(run_dir, name))
except FileNotFoundError:
pass
@staticmethod
def _terminate_live_runtime(run_dir: str, ipc_socket: str) -> None:
"""SIGTERM whoever holds ``ipc_socket`` and wait for the socket to go dead."""
logger.warning(
"--force: terminating the CloudXR runtime serving %s", ipc_socket
)
try:
subprocess.run(
["fuser", "-k", "-TERM", ipc_socket], capture_output=True, timeout=5
)
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
raise RuntimeError(
f"Cannot take over the runtime serving {ipc_socket}: 'fuser' is "
"unavailable (install psmisc), so the running process cannot be "
"identified. Stop it manually and retry."
) from exc

deadline = time.monotonic() + RUNTIME_TERMINATE_TIMEOUT_SEC
while time.monotonic() < deadline:
if not is_runtime_live(run_dir):
logger.warning("Previous CloudXR runtime stopped")
return
time.sleep(0.2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make runtime ownership atomic and confirm shutdown.

is_runtime_live() is only a point-in-time check. Two launchers can both observe no listener while one runtime starts. They can then remove markers and start against the same run directory. A later unlink can detach the first runtime's live socket name.

In force mode, a closed IPC socket does not prove that the prior process exited. Its signal handler can close the listener before it releases other runtime resources.

Use an exclusive ownership lock that spans startup and runtime lifetime. When force=True, wait for the identified owner process to exit before removing files or starting the replacement runtime.

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 620-622: Command coming from incoming request
Context: subprocess.run(
["fuser", "-k", "-TERM", ipc_socket], capture_output=True, timeout=5
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/python/isaacteleop/cloudxr/launcher.py` around lines 594 - 636, The
launch flow currently relies on the point-in-time is_runtime_live check and
force-mode socket disappearance, so runtime ownership is not atomic and shutdown
is not confirmed. Add an exclusive run-directory ownership lock that remains
held from startup through the runtime lifetime, acquire it before checking or
removing stale state, and ensure it is released only when the launcher exits.
Update _terminate_live_runtime to identify and wait for the prior owner process
to exit, not merely until is_runtime_live returns false, before cleanup and
replacement startup proceed.

A second launch treated the existence of `ipc_cloudxr` as proof of a stale
runtime and `fuser -k -TERM`ed whoever held it — including a healthy runtime
mid-session, which the first session saw as a broken pipe. When `fuser` was
missing the error was swallowed and the socket unlinked anyway, leaving the old
process running and the operator to clean `~/.cloudxr/run/` by hand.

Liveness is now decided by connecting to the socket rather than by stat-ing it.
A live runtime makes the launcher refuse, pointing at the env file and
`--no-launch-cloudxr-runtime`; replacing it means stopping it yourself. A dead
socket is cleaned with a warning. Ambiguous probe errors count as live, since
refusing is recoverable and clobbering a session is not.

`-35` is thrown after `xrCreateInstance` succeeds, so the runtime was found and
only the headset is missing, but library defaults are fail-fast and a direct
consumer got a bare `Failed to get OpenXR system: -35`. Both throw sites now
name `XR_ERROR_FORM_FACTOR_UNAVAILABLE` and point at the docs. The device
profile is the usual culprit, so the startup banner prints the resolved value
and the launcher/env-config defaults agree on one constant — the docs claimed
`auto-webrtc` where `Quest3` always won. Overriding it stays an env-file or
`CloudXRLauncher(device_profile=...)` job; Apple Vision Pro needs
`auto-native`.

Closes #908

Signed-off-by: Jiwen Cai <jiwenc@nvidia.com>
The CloudXR runtime uses the ``auto-webrtc`` device profile by default
(Pico & Quest). For Apple Vision Pro it defaults to ``auto-native``. To
override settings, write a ``KEY=value`` env file and pass it to the example
The CloudXR runtime uses the ``Quest3`` device profile by default; Apple Vision

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use auto-webrtc tho?

@jiwenc-nv jiwenc-nv Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with auto-webrtc is that you have to connect a device first start running OpenXR session, which has been a hotspot for getting people blocked... (the -35 error).

@nv-jakob already switched the default profile to Quest3 a while ago, but we just haven't update the doc yet.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we should just rename Quest3 to generic-webxr, wdyt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CloudXR runtime: a second launch SIGTERMs the live one, and -35 is unexplained outside the example entry points

2 participants