Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@

@blocking
@asynccontextmanager
async def NovncAdapter(*, client: DriverClient, method: str = "connect"):
async def NovncAdapter(*, client: DriverClient, method: str = "connect", encrypt: bool = False):
"""
Provide a noVNC URL that proxies a temporary local TCP listener to a remote driver stream via a WebSocket bridge.

Check failure on line 16 in packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py:16:1: W293 Blank line contains whitespace
Parameters:
client (DriverClient): Client used to open the remote stream that will be bridged to the local listener.
method (str): Name of the async stream method to call on the client (default "connect").
encrypt (bool): If True use "https" in the generated URL; if False use "http" and include `encrypt=0` in the URL query.

Check failure on line 20 in packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py:20:121: E501 Line too long (127 > 120)

Check failure on line 21 in packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py:21:1: W293 Blank line contains whitespace
Returns:
str: A fully constructed noVNC URL pointing at the temporary listener (host and port encoded in the query).
"""
async def handler(conn):
async with conn:
async with client.stream_async(method) as stream:
Expand All @@ -19,13 +30,23 @@
pass

async with TemporaryTcpListener(handler) as addr:
scheme = "https" if encrypt else "http"
params = {
"autoconnect": 1,
"reconnect": 1,
"host": addr[0],
"port": addr[1],
}
if not encrypt:
params["encrypt"] = 0

yield urlunparse(
(
"https",
scheme,
"novnc.com",
"/noVNC/vnc.html",
"",
urlencode({"autoconnect": 1, "reconnect": 1, "host": addr[0], "port": addr[1]}),
urlencode(params),
"",
)
)
)

Check failure on line 52 in packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W292)

packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py:52:10: W292 No newline at end of file
100 changes: 100 additions & 0 deletions packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from __future__ import annotations

import asyncio
import contextlib
import typing
import webbrowser

import anyio
import click
from jumpstarter_driver_composite.client import CompositeClient
from jumpstarter_driver_network.adapters.novnc import NovncAdapter

from jumpstarter.client.decorators import driver_click_group

if typing.TYPE_CHECKING:
from jumpstarter_driver_network.client import TCPClient


class VNClient(CompositeClient):
"""Client for interacting with a VNC server."""

@property
def tcp(self) -> TCPClient:
"""
Access the underlying TCP client.

Check failure on line 26 in packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py:26:1: W293 Blank line contains whitespace
Returns:
TCPClient: The TCP client instance stored in this composite client's children mapping.
"""
return typing.cast("TCPClient", self.children["tcp"])

@contextlib.contextmanager
def session(self, *, encrypt: bool = False) -> typing.Iterator[str]:
"""
Open a noVNC session and yield the connection URL.

Check failure on line 36 in packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py:36:1: W293 Blank line contains whitespace
Parameters:
encrypt (bool): If True, request an encrypted WebSocket (use `wss://`); otherwise use `ws://`.

Check failure on line 39 in packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py:39:1: W293 Blank line contains whitespace
Returns:
url (str): The URL to connect to the VNC session.
"""
with NovncAdapter(client=self.tcp, method="connect", encrypt=encrypt) as adapter:
yield adapter

def cli(self) -> click.Command:
"""
Provide a Click command group for running VNC sessions.

Check failure on line 49 in packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py:49:1: W293 Blank line contains whitespace
The returned command exposes a `session` subcommand that opens a VNC session, prints the connection URL, optionally opens it in the user's browser, and waits until the user cancels the session.

Check failure on line 50 in packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py:50:121: E501 Line too long (201 > 120)

Check failure on line 51 in packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py:51:1: W293 Blank line contains whitespace
Returns:
click.Command: Click command group with a `session` subcommand that accepts
`--browser/--no-browser` and `--encrypt/--no-encrypt` options.
"""

@driver_click_group(self)
def vnc():
"""
Open a VNC session and block until the user closes it.

When invoked, prints the connection URL for the noVNC session, optionally opens that URL in the user's web browser, and waits for user-initiated termination (for example, Ctrl+C). On exit, prints a message indicating the session is closing.

Parameters:
browser (bool): If True, open the session URL in the default web browser.
encrypt (bool): If True, request an encrypted (wss://) connection.
"""

@vnc.command()
@click.option("--browser/--no-browser", default=True, help="Open the session in a web browser.")
@click.option(
"--encrypt/--no-encrypt",
default=False,
help="Use an encrypted connection (wss://).",
)
def session(browser: bool, encrypt: bool):
"""
Open an interactive VNC session and wait for the user to terminate it.

Starts a VNC session using the client's session context, prints the connection URL, optionally opens that URL in a web browser, and blocks until the user cancels (e.g., Ctrl+C), then closes the session.

Parameters:
browser (bool): If True, open the session URL in the default web browser.
encrypt (bool): If True, request an encrypted (wss://) connection for the session.
"""
# The NovncAdapter is a blocking context manager that runs in a thread.
# We can enter it, open the browser, and then just wait for the user
# to press Ctrl+C to exit. The adapter handles the background work.
with self.session(encrypt=encrypt) as url:
click.echo(f"To connect, please visit: {url}")
if browser:
webbrowser.open(url)
click.echo("Press Ctrl+C to close the VNC session.")
try:
# Use the client's own portal to wait for cancellation.
self.portal.call(asyncio.Event().wait)
except (KeyboardInterrupt, anyio.get_cancelled_exc_class()):
click.echo("\nClosing VNC session.")

return vnc
31 changes: 31 additions & 0 deletions packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from jumpstarter.common.exceptions import ConfigurationError
from jumpstarter.driver import Driver


class Vnc(Driver):
"""A driver for VNC."""

def __post_init__(self):
"""
Validate the VNC driver's post-initialization configuration.

Ensures the driver has a "tcp" child configured.

Raises:
ConfigurationError: If a "tcp" child is not present.
"""
super().__post_init__()
if "tcp" not in self.children:
raise ConfigurationError("A tcp child is required for Vnc")

@classmethod
def client(cls) -> str:
"""
Client class path for this driver.

Returns:
str: Dotted import path of the client class, e.g. "jumpstarter_driver_vnc.client.VNClient".
"""
return "jumpstarter_driver_vnc.client.VNClient"
Loading