Skip to content

refactor(omnidreams/webrtc): serve packaged web assets via importlib.resources#343

Open
mvanhorn wants to merge 1 commit into
NVIDIA:mainfrom
mvanhorn:refactor/329-serve-omnidreams-assets-via-importlib-resources
Open

refactor(omnidreams/webrtc): serve packaged web assets via importlib.resources#343
mvanhorn wants to merge 1 commit into
NVIDIA:mainfrom
mvanhorn:refactor/329-serve-omnidreams-assets-via-importlib-resources

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

What

Refactors the Omnidreams WebRTC server to discover its packaged web/ directory through importlib.resources instead of Path(__file__), following the canonical pattern from #329.

server.py now:

  • Resolves the bundled assets with files("omnidreams.webrtc").joinpath("web") and materializes them via as_file(...) inside an ExitStack.
  • Stores the stack on the aiohttp app (app["package_resource_stack"]) and closes it from an on_cleanup handler, so the directory stays valid for the app's entire lifetime and is cleaned up on shutdown.
  • Keeps the create_app signature and the public route surface unchanged.

Why

As called out in #329, deriving packaged assets from Path(__file__).resolve() can break in non-standard installs (zip-imports, editable wheels, relocated site-packages). The importlib.resources path resolves correctly regardless of how the package is laid out on disk, and the ExitStack/as_file wrapping is the defensive layer the issue describes for cases where the resource is not already a real filesystem path.

Backward-compatible /assets/ mount

The optional repo-root REPO_ASSETS_DIR = Path(__file__).resolve().parents[4] / "assets" mount is left intact and remains guarded by if REPO_ASSETS_DIR.is_dir():. In a checkout it still mounts /assets/; in a wheel-installed deployment where the repo root is absent, the mount is simply skipped rather than raising. A short comment notes this.

Tests

Two focused tests added to test_webrtc_server_routes.py:

  • test_create_app_keeps_package_web_resource_materialized — asserts the ExitStack is registered on the app, the _close_package_resources cleanup is wired into on_cleanup, and the materialized web/ directory is real and readable.
  • test_create_app_skips_absent_repo_assets_mount — monkeypatches REPO_ASSETS_DIR to a missing path (installed-wheel layout) and asserts create_app still constructs and registers no /assets/ route.

The existing route assertions are unchanged and continue to exercise the new path automatically through the aiohttp TestServer/TestClient lifecycle.

Scope

This PR covers the omnidreams webrtc/server.py case the issue centers on (the closest sibling to #328). The other __file__ sites listed in the issue (lingbot, hy_worldplay, flashvsr, interactive_drive CLI, ludus_renderer JIT) each need their own resource-bundling decisions and are intentionally left out of this change.

AI was used for assistance.

Refs #329

…resources

Discover the packaged omnidreams.webrtc web/ directory through
importlib.resources (files + as_file) inside an ExitStack instead of
deriving it from Path(__file__), and close the stack on app cleanup.
This keeps the static asset directory resolvable in non-standard
installs (zip-imports, editable wheels, relocated site-packages),
following the canonical pattern from the issue.

The optional repo-root /assets/ mount stays backward-compatible: it is
still mounted when the directory exists (checkout case) and skipped
otherwise, so wheel-installed deployments no longer depend on a repo
root four parents up.

Refs NVIDIA#329

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the Path(__file__) web asset resolution in the Omnidreams WebRTC server with importlib.resources (files() + as_file() + ExitStack), following the canonical pattern from #329 to support zip-imports and non-standard wheel layouts. Two focused synchronous tests are added to verify the ExitStack is registered on the app and that the absent-repo-assets path is gracefully skipped.

  • server.py: WEB_DIR_RESOURCE is now resolved via files(\"omnidreams.webrtc\").joinpath(\"web\"); an ExitStack materializes the directory inside create_app, is stored on the aiohttp app, and is torn down via an on_cleanup handler.
  • REPO_ASSETS_DIR (the optional /assets/ mount) retains its Path(__file__) derivation with an is_dir() guard, correctly handling wheel-installed deployments where the repo root is absent.
  • Two new tests cover the resource registration and the skipped-assets-mount path; both inherit the module-level ci_gpu marker even though neither touches a GPU.

Confidence Score: 3/5

The core create_app function has an unguarded window where resource_stack is live but can be silently abandoned if create_webrtc_app raises before the cleanup handler is registered.

The ExitStack is entered before create_webrtc_app is called, but the on_cleanup handler that closes it is only wired in after create_webrtc_app returns successfully. Any exception thrown by create_webrtc_app leaves the stack open with no teardown path — in zip-installed deployments this leaks a temporary extracted directory. This is a present defect in the changed path that should be fixed before merging.

integrations/omnidreams/omnidreams/webrtc/server.py — the create_app setup block around lines 132–142 needs a try/except guard.

Important Files Changed

Filename Overview
integrations/omnidreams/omnidreams/webrtc/server.py Switches web asset resolution to importlib.resources/ExitStack. The resource lifecycle has a gap: if create_webrtc_app() raises between ExitStack.enter_context and registering the cleanup handler, the stack is never closed.
integrations/omnidreams/tests/test_webrtc_server_routes.py Adds two synchronous tests covering ExitStack registration and absent-REPO_ASSETS_DIR behavior. Tests are correct but inherit the module-level ci_gpu marker despite requiring no GPU.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant caller as Caller
    participant create_app
    participant ExitStack
    participant as_file as as_file(WEB_DIR_RESOURCE)
    participant create_webrtc_app
    participant aiohttp as aiohttp App

    caller->>create_app: create_app(...)
    create_app->>ExitStack: ExitStack()
    create_app->>as_file: enter_context(as_file(WEB_DIR_RESOURCE))
    as_file-->>create_app: web_dir (Path)
    create_app->>create_webrtc_app: "create_webrtc_app(web_dir=web_dir, ...)"
    Note over create_app,create_webrtc_app: If this raises, ExitStack is never closed
    create_webrtc_app-->>create_app: app
    create_app->>aiohttp: "app[package_resource_stack] = resource_stack"
    create_app->>aiohttp: app.on_cleanup.append(_close_package_resources)
    create_app-->>caller: app

    Note over aiohttp: On aiohttp shutdown
    aiohttp->>create_app: _close_package_resources(app)
    create_app->>ExitStack: resource_stack.close()
    ExitStack->>as_file: __exit__ (cleanup temp dir if needed)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant caller as Caller
    participant create_app
    participant ExitStack
    participant as_file as as_file(WEB_DIR_RESOURCE)
    participant create_webrtc_app
    participant aiohttp as aiohttp App

    caller->>create_app: create_app(...)
    create_app->>ExitStack: ExitStack()
    create_app->>as_file: enter_context(as_file(WEB_DIR_RESOURCE))
    as_file-->>create_app: web_dir (Path)
    create_app->>create_webrtc_app: "create_webrtc_app(web_dir=web_dir, ...)"
    Note over create_app,create_webrtc_app: If this raises, ExitStack is never closed
    create_webrtc_app-->>create_app: app
    create_app->>aiohttp: "app[package_resource_stack] = resource_stack"
    create_app->>aiohttp: app.on_cleanup.append(_close_package_resources)
    create_app-->>caller: app

    Note over aiohttp: On aiohttp shutdown
    aiohttp->>create_app: _close_package_resources(app)
    create_app->>ExitStack: resource_stack.close()
    ExitStack->>as_file: __exit__ (cleanup temp dir if needed)
Loading

Reviews (1): Last reviewed commit: "refactor(omnidreams/webrtc): serve packa..." | Re-trigger Greptile

Comment on lines +132 to +142
resource_stack = ExitStack()
web_dir = resource_stack.enter_context(as_file(WEB_DIR_RESOURCE))

app = create_webrtc_app(
web_dir=WEB_DIR,
web_dir=web_dir,
session_manager=manager,
preload_name="Omnidreams",
request_session_url=request_session_url,
)
app["package_resource_stack"] = resource_stack
app.on_cleanup.append(_close_package_resources)

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.

P1 ExitStack leaks if create_webrtc_app raises

resource_stack is entered (and potentially extracts a temp dir via as_file) before create_webrtc_app is called. If create_webrtc_app raises — or if the subsequent app["package_resource_stack"] = resource_stack line ever fails — the stack is never closed and the temporary directory is orphaned. The cleanup handler is only registered after everything succeeds, so the exception path has no teardown at all.

The defensive fix is to guard the setup block: wrap lines 132–142 in a try/except that calls resource_stack.close() then re-raises, so the stack is always closed on failure.

Comment on lines +65 to +87
def test_create_app_keeps_package_web_resource_materialized() -> None:
app = create_app(
session_manager=FakeSessionManager(),
request_session_url="http://127.0.0.1:8080/request_session",
)
try:
assert isinstance(app["package_resource_stack"], ExitStack)
assert _close_package_resources in app.on_cleanup

static_resources = [
resource
for resource in app.router.resources()
if getattr(resource, "canonical", "") == "/static"
or resource.get_info().get("prefix") in {"/static", "/static/"}
]
assert len(static_resources) == 1
web_dir = static_resources[0].get_info()["directory"]
assert web_dir.is_dir()
assert "Omnidreams WebRTC Drive" in (
web_dir / "request_session.html"
).read_text()
finally:
app["package_resource_stack"].close()

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.

P2 New unit tests gated behind ci_gpu marker unnecessarily

pytestmark = pytest.mark.ci_gpu at module level marks every test in the file. The two new tests (test_create_app_keeps_package_web_resource_materialized and test_create_app_skips_absent_repo_assets_mount) never touch a GPU — they only verify app construction, route registration, and ExitStack wiring. Running them only on GPU runners means they're skipped in all standard CI environments, which is exactly where pure unit tests should run earliest. Consider overriding the marker or moving these two tests to a separate CPU-only test module.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

1 participant