Skip to content

fix(cloudxr): sync and serve webpack lazy chunks for OOB static client#684

Open
gareth-morgan-nv wants to merge 5 commits into
mainfrom
gmorgan/MultipleChunks
Open

fix(cloudxr): sync and serve webpack lazy chunks for OOB static client#684
gareth-morgan-nv wants to merge 5 commits into
mainfrom
gmorgan/MultipleChunks

Conversation

@gareth-morgan-nv

@gareth-morgan-nv gareth-morgan-nv commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Description

Production webpack builds of the WebXR client emit a main bundle.js plus lazy
[id].bundle.js chunks (UIKit msdf text, desktop XR emulation, etc.). OOB modes
(--host-client, --usb-local) only downloaded and served index.html and
bundle.js, so lazy chunks 404'd on the headset and in-VR UI text did not
render (#602).

A temporary workaround inlined all async chunks into one ~9.5 MiB bundle.js
(asyncChunks: false in webpack.prod.js). This change removes that workaround
and fixes static hosting end-to-end:

  • Webpack: Re-enable lazy chunks; add AssetManifestPlugin to write
    asset-manifest.json; enable output.clean on prod builds.
  • Python sync: require_web_client_static_dir() reads the manifest (local or
    from the versioned GitHub Pages client) and downloads every listed asset.
    Falls back to index.html + bundle.js when no manifest exists (older
    published clients).
  • Python serve: WSS /client/ serves the full static tree with
    path-traversal-safe resolution and correct MIME types (not a two-file
    whitelist).
  • Docs: Prod build layout, air-gap staging, and OOB troubleshooting for
    missing UI text.
  • Tests: Manifest-driven chunk download; legacy two-file fallback unchanged.

Fixes #602

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

Unit tests

pytest src/core/cloudxr_tests/python/test_oob_teleop_env.py -k require_web_client_static_dir -v

Covers legacy two-file sync (manifest 404) and manifest-driven download of a
lazy chunk (553.bundle.js).

Web client prod build

cd deps/cloudxr/webxr_client
USE_LOCAL_WEBXR_ASSETS=0 npm run build
ls build/asset-manifest.json build/*.bundle.js

Manual / integration (Quest, USB-local OOB)

export TELEOP_WEB_CLIENT_STATIC_DIR=/path/to/deps/cloudxr/webxr_client/build  # optional
python -m isaacteleop.cloudxr --accept-eula --setup-oob --usb-local

Verified: static cache includes manifest-listed chunks; headset loads UI with
text after cert acceptance. Tested on Linux host with Quest 3 (adb + Wi‑Fi for
ICE).

Pre-commit (run before merge):

SKIP=check-copyright-year pre-commit run --all-files

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

Summary by CodeRabbit

  • New Features

    • WebXR client now supports manifest-driven asset synchronization for complete offline caching and deployment of all required web assets.
  • Documentation

    • Added production build process documentation with generated manifest output details.
    • Improved offline caching and air-gapped deployment setup instructions.
    • Enhanced troubleshooting guidance for asset configuration issues.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7ed49c43-f809-42b8-83e2-544368e6e2b2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements manifest-driven lazy-chunk asset delivery for the WebXR web client. The Webpack build now generates asset-manifest.json listing all emitted assets. The launcher syncs assets idempotently: it checks for a local manifest (fast path), fetches the remote manifest if missing, downloads only absent files, and falls back to legacy two-file syncing for older releases. The WSS proxy now serves arbitrary files from the client build tree with path-traversal protection and MIME type detection. Documentation and help text are updated to reflect the new sync behavior and the requirement for a complete asset tree when using lazy chunks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The changes introduce stateful asset-sync logic with manifest parsing, URL fetching, deduplication, and atomic writes. The launcher's conditional fast/slow paths and fallback behavior warrant close review. WSS proxy file resolution requires path-traversal validation. Large docstring updates across the module increase surface area but are straightforward.

Possibly related PRs

  • NVIDIA/IsaacTeleop#671: Introduces --host-client wiring through __main__.py and launcher.py; this PR's WSS changes extend its /client/ serving to handle the full lazy-chunk asset tree.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: enabling webpack lazy chunk syncing and serving for OOB static client deployment to fix missing UI text.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #602 by implementing manifest-driven chunk syncing, proper serving of lazy chunks, and fallback to legacy behavior when manifest unavailable.
Out of Scope Changes check ✅ Passed All changes directly support lazy chunk syncing and serving for OOB static client; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gmorgan/MultipleChunks

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

@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/core/cloudxr/python/oob_teleop_env.py`:
- Around line 247-270: The _manifest_file_names function currently accepts
arbitrary strings from the manifest; update it to validate that each entry is a
safe basename by rejecting any entry that contains path separators ("/" or
"\\"), any parent-segment (".."), or is an absolute path, before appending to
names; ensure validation happens where entries are checked (inside the for entry
in files loop) so only clean basenames are returned and downstream code that
uses names (e.g., the download/destination logic) cannot write outside the
intended directory.
🪄 Autofix (Beta)

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: a1de5c26-d3ab-4cff-a609-b89686b81eae

📥 Commits

Reviewing files that changed from the base of the PR and between b212cfb and ea4bb10.

📒 Files selected for processing (9)
  • deps/cloudxr/webxr_client/webpack.common.js
  • deps/cloudxr/webxr_client/webpack.prod.js
  • docs/source/getting_started/build_from_source/webxr.rst
  • docs/source/getting_started/quick_start.rst
  • docs/source/references/oob_teleop_control.rst
  • src/core/cloudxr/python/__main__.py
  • src/core/cloudxr/python/oob_teleop_env.py
  • src/core/cloudxr/python/wss.py
  • src/core/cloudxr_tests/python/test_oob_teleop_env.py

Comment on lines +247 to +270
def _manifest_file_names(payload: object) -> list[str] | None:
"""Parse ``asset-manifest.json`` payload into a deduplicated file list.

Args:
payload: Decoded JSON object from ``asset-manifest.json`` (expects a
top-level ``files`` array of non-empty strings).

Returns:
Deduplicated basenames to sync, or ``None`` if *payload* is not a valid
manifest (wrong shape, empty list, or no usable entries).
"""
# Reject non-object JSON (or wrong schema version).
if not isinstance(payload, dict):
return None
files = payload.get("files")
# Require a non-empty array of basenames.
if not isinstance(files, list) or not files:
return None
# Preserve manifest order; skip duplicates and non-string entries.
names: list[str] = []
for entry in files:
if isinstance(entry, str) and entry and entry not in names:
names.append(entry)
return names or None

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Path traversal via malicious manifest filenames.

_manifest_file_names accepts arbitrary strings from the manifest without validating they're safe basenames. If a compromised or malicious manifest contains entries like ../../../.bashrc, the download loop at line 427 (dest = p / name) would write outside the static directory.

Add validation to reject entries containing path separators or .. segments:

Proposed fix
     # Preserve manifest order; skip duplicates and non-string entries.
     names: list[str] = []
     for entry in files:
-        if isinstance(entry, str) and entry and entry not in names:
+        if isinstance(entry, str) and entry and entry not in names:
+            # Reject path traversal attempts and subdirectory paths.
+            if "/" in entry or "\\" in entry or entry == ".." or entry.startswith(".."):
+                continue
             names.append(entry)
     return names or None
🤖 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/core/cloudxr/python/oob_teleop_env.py` around lines 247 - 270, The
_manifest_file_names function currently accepts arbitrary strings from the
manifest; update it to validate that each entry is a safe basename by rejecting
any entry that contains path separators ("/" or "\\"), any parent-segment
(".."), or is an absolute path, before appending to names; ensure validation
happens where entries are checked (inside the for entry in files loop) so only
clean basenames are returned and downstream code that uses names (e.g., the
download/destination logic) cannot write outside the intended directory.

@yanziz-nvidia

Copy link
Copy Markdown
Contributor

Do we want to split bundle.js for lazy loading, which is original behavior and is what I disabled. It is less deterministic than having a single bundle.js. Are we able to keep a single bundle.js but reduce the size by removing not needed packages?

@gareth-morgan-nv gareth-morgan-nv force-pushed the gmorgan/MultipleChunks branch from b446f75 to 0949d28 Compare June 15, 2026 16:42
3. Build & Run
--------------

Production build

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.

I felt the description here is not needed (hidden from robotics/teleop people)

@yanziz-nvidia yanziz-nvidia 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.

Reviewed by yanziz-reviewer-bot

Summary

Removes asyncChunks: false to split the ~9.5 MiB bundle.js into bundle.js + bundle.emulator.js; the guard in chunkFilename that throws on unexpected chunk names is a good defensive pattern, and the _OPTIONAL_WEB_CLIENT_ASSETS fallback path handles older deployments correctly.

Legend: 🚫 Blocker · 💡 Suggestion · 🔍 Nit

Finding
🚫 deps/cloudxr/webxr_client/webpack.common.js:114chunkFilename lives in webpack.common.js but chunkOptimization (which renames the chunk to 'emulator') is not applied in webpack.dev.js. In dev mode webpack 5 names the chunk from its module path ('emulate', not 'emulator'), so npm start would throw Unexpected async chunk "emulate". Move both chunkFilename and chunkOptimization into webpack.prod.js, or add a /* webpackChunkName: "emulator" */ annotation on the dynamic import and document the assumption.
💡 src/core/cloudxr/python/wss.py:344except OSError returns HTTP 503; a permanently absent optional file should be 404.
💡 src/core/cloudxr/python/oob_teleop_env.py:174 — Asset list is hardcoded, not manifest-driven; any new async chunk will silently 404 on OOB hosts until added here. Add a comment noting the limitation.
🔍 src/core/cloudxr/python/__main__.py:89 — Help string is 119 chars; surrounding lines are ≤80.
🔍 deps/cloudxr/webxr_client/webpack.chunkNames.js:1 — SPDX-only header; other webpack configs in this directory carry the full Apache-2.0 boilerplate.

Actionables (for bots — copy-paste-ready for AI)

Fix if it makes sense in context — these are agent-generated suggestions, not human-vetted obligations. Skip anything that's wrong, already addressed, or not worth the churn.

  • webpack.common.js:114 — Move chunkFilename + chunkOptimization into webpack.prod.js so the throw-guard only fires where the cacheGroup guarantees the 'emulator' name; restore the default in webpack.common.js.
  • wss.py:344 — Change except OSError response status from 503 to 404 for missing optional static files.
  • oob_teleop_env.py:174 — Add a comment above _OPTIONAL_WEB_CLIENT_ASSETS that the list is hardcoded and new chunks must be added manually here.
  • __main__.py:89 — Wrap the 119-char help string at ≤80 chars to match surrounding lines.
  • webpack.chunkNames.js:1 — Expand SPDX-only header to the full Apache-2.0 boilerplate used by webpack.common.js.

@gareth-morgan-nv

Copy link
Copy Markdown
Contributor Author

LGTM. We should be sure this works on all combinations of browsers/devices (and with public vs private released from Pico)

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.

Texts Are Missing on Client UI

2 participants