From ba7fac83f8eaf6af06b0ad750305b9463b914ff9 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:19:49 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`vnc-dri?= =?UTF-8?q?ver`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @aesteve-rh. * https://github.com/jumpstarter-dev/jumpstarter/pull/775#issuecomment-3645423804 The following files were modified: * `packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py` * `packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py` * `packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py` --- .../adapters/novnc.py | 29 ++++- .../jumpstarter_driver_vnc/client.py | 100 ++++++++++++++++++ .../jumpstarter_driver_vnc/driver.py | 31 ++++++ 3 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py create mode 100644 packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py diff --git a/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py b/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py index 1a64c478b..a2a7b2739 100644 --- a/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py +++ b/packages/jumpstarter-driver-network/jumpstarter_driver_network/adapters/novnc.py @@ -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. + + 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. + + 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: @@ -19,13 +30,23 @@ async def handler(conn): 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), "", ) - ) + ) \ No newline at end of file diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py new file mode 100644 index 000000000..ff74ca4c1 --- /dev/null +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py @@ -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. + + 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. + + Parameters: + encrypt (bool): If True, request an encrypted WebSocket (use `wss://`); otherwise use `ws://`. + + 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. + + 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. + + 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 \ No newline at end of file diff --git a/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py new file mode 100644 index 000000000..71ac2af0d --- /dev/null +++ b/packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py @@ -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" \ No newline at end of file