This repository was archived by the owner on Jan 23, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
feat(driver-vnc): create vnc driver #775
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../../../../packages/jumpstarter-driver-vnc/README.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # Vnc Driver | ||
|
|
||
| `jumpstarter-driver-vnc` provides functionality for interacting with VNC servers. It allows you to create a secure, tunneled VNC session in your browser. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```shell | ||
| pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-vnc | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| The VNC driver is a composite driver that requires a TCP child driver to establish the underlying network connection. The TCP driver should be configured to point to the VNC server's host and port, which is often `127.0.0.1` from the perspective of the Jumpstarter server. | ||
|
|
||
| Example `exporter.yaml` configuration: | ||
|
|
||
| ```yaml | ||
| export: | ||
| vnc: | ||
| type: jumpstarter_driver_vnc.driver.Vnc | ||
| # You can set the default encryption behavior for the `j vnc session` command. | ||
| # If not set, it defaults to False (unencrypted). | ||
| default_encrypt: false | ||
| children: | ||
| tcp: | ||
| type: jumpstarter_driver_network.driver.TcpNetwork | ||
| config: | ||
| host: "127.0.0.1" | ||
| port: 5901 # Default VNC port for display :1 | ||
| ``` | ||
|
|
||
| ## API Reference | ||
|
|
||
| The client class for this driver is `jumpstarter_driver_vnc.client.VNClient`. | ||
|
|
||
| ### `vnc.session()` | ||
|
|
||
| This asynchronous context manager establishes a connection to the remote VNC server and provides a local web server to view the session. | ||
|
|
||
| **Usage:** | ||
|
|
||
| ```python | ||
| async with vnc.session() as novnc_adapter: | ||
| print(f"VNC session available at: {novnc_adapter.url}") | ||
| # The session remains open until the context block is exited. | ||
| await novnc_adapter.wait() | ||
| ``` | ||
aesteve-rh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ### CLI: `j vnc session` | ||
|
|
||
| This driver provides a convenient CLI command within the `jmp shell`. By default, it will open the session URL in your default web browser. | ||
|
|
||
| **Usage:** | ||
|
|
||
| ```shell | ||
| # This will start the local server and open a browser. | ||
| j vnc session | ||
|
|
||
| # To prevent it from opening a browser automatically: | ||
| j vnc session --no-browser | ||
|
|
||
| # To force an encrypted (wss://) or unencrypted (ws://) connection, overriding | ||
| # the default set in the exporter configuration: | ||
| j vnc session --encrypt | ||
| j vnc session --no-encrypt | ||
| ``` | ||
|
|
||
| > **Note:** Using an encrypted connection is intended for advanced scenarios where the local proxy can be configured with a TLS certificate that your browser trusts. For standard local development, modern browsers will likely reject the self-signed certificate and the connection will fail. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| apiVersion: jumpstarter.dev/v1alpha1 | ||
| kind: ExporterConfig | ||
| metadata: | ||
| namespace: default | ||
| name: demo | ||
| endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082 | ||
| token: "<token>" | ||
| export: | ||
| vnc: | ||
| type: jumpstarter_driver_vnc.driver.Vnc | ||
| # You can set the default encryption behavior for the `j vnc session` command. | ||
| # If not set, it defaults to False (unencrypted). | ||
| default_encrypt: false | ||
| children: | ||
| tcp: | ||
| type: jumpstarter_driver_network.driver.TcpNetwork | ||
| config: | ||
| host: "127.0.0.1" | ||
| port: 5901 # Default VNC port for display :1 |
3 changes: 3 additions & 0 deletions
3
packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .client import VNClient | ||
|
|
||
| VNClient = VNClient |
125 changes: 125 additions & 0 deletions
125
packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/client.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| from __future__ import annotations | ||
|
|
||
| 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: | ||
aesteve-rh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| 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"]) | ||
aesteve-rh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def stream(self, method="connect"): | ||
| """Create a new stream, proxied to the underlying TCP driver.""" | ||
| return self.tcp.stream(method) | ||
|
|
||
| async def stream_async(self, method="connect"): | ||
| """Create a new async stream, proxied to the underlying TCP driver.""" | ||
| return await self.tcp.stream_async(method) | ||
|
|
||
| @contextlib.contextmanager | ||
| def session(self, *, encrypt: bool = True) -> typing.Iterator[str]: | ||
| """ | ||
| Open a noVNC session and yield the connection URL. | ||
|
|
||
| Parameters: | ||
| encrypt (bool): If True, request an encrypted vnc connection. | ||
|
|
||
| Returns: | ||
| url (str): The URL to connect to the VNC session. | ||
| """ | ||
| with NovncAdapter(client=self.tcp, method="connect", encrypt=encrypt) as adapter: | ||
| yield adapter | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def get_default_encrypt(self) -> bool: | ||
| """Fetch the default encryption setting from the remote driver.""" | ||
| return typing.cast(bool, self.call("get_default_encrypt")) | ||
|
|
||
| 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. | ||
| """ | ||
|
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @vnc.command() | ||
| @click.option("--browser/--no-browser", default=True, help="Open the session in a web browser.") | ||
aesteve-rh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @click.option( | ||
| "--encrypt", | ||
| "encrypt_override", | ||
| flag_value=True, | ||
| default=None, | ||
| help="Force an encrypted vnc connection. Overrides the driver default.", | ||
| ) | ||
| @click.option( | ||
| "--no-encrypt", | ||
| "encrypt_override", | ||
| flag_value=False, | ||
| help="Force an unencrypted vnc connection. Overrides the driver default.", | ||
| ) | ||
| def session(browser: bool, encrypt_override: bool | None): | ||
| """ | ||
| 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_override (bool | None): If provided, overrides the driver's default | ||
| encryption setting. True for encrypted, | ||
| False for unencrypted, None to use driver default. | ||
| """ | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
aesteve-rh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| encrypt = encrypt_override if encrypt_override is not None else self.get_default_encrypt() | ||
| # 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(anyio.sleep_forever) | ||
| except (KeyboardInterrupt, anyio.get_cancelled_exc_class()): | ||
| click.echo("\nClosing VNC session.") | ||
|
|
||
| return vnc | ||
46 changes: 46 additions & 0 deletions
46
packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
| from jumpstarter_driver_composite.driver import Composite | ||
|
|
||
| from jumpstarter.common.exceptions import ConfigurationError | ||
| from jumpstarter.driver import export | ||
|
|
||
|
|
||
| @dataclass | ||
| class Vnc(Composite): | ||
| """A VNC driver. | ||
|
|
||
| Members: | ||
| default_encrypt: Whether to default to an encrypted client connection. | ||
| """ | ||
|
|
||
| default_encrypt: bool = False | ||
|
|
||
| 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") | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @export | ||
| async def get_default_encrypt(self) -> bool: | ||
| """Return the default encryption setting.""" | ||
| return self.default_encrypt | ||
|
|
||
| @classmethod | ||
| def client(cls) -> str: | ||
| """ | ||
| Client class path for this driver. | ||
|
|
||
| Returns: | ||
| str: Dotted import path of the client class. | ||
| """ | ||
| return "jumpstarter_driver_vnc.client.VNClient" | ||
42 changes: 42 additions & 0 deletions
42
packages/jumpstarter-driver-vnc/jumpstarter_driver_vnc/driver_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
| from jumpstarter_driver_composite.client import CompositeClient | ||
|
|
||
| from jumpstarter_driver_vnc.driver import Vnc | ||
|
|
||
| from jumpstarter.client import DriverClient | ||
| from jumpstarter.common.exceptions import ConfigurationError | ||
| from jumpstarter.common.utils import serve | ||
| from jumpstarter.driver import Driver | ||
|
|
||
|
|
||
| class FakeTcpDriver(Driver): | ||
| @classmethod | ||
| def client(cls) -> str: | ||
| return "jumpstarter.client.DriverClient" | ||
|
|
||
|
|
||
| def test_vnc_client_is_composite(): | ||
| """Test that the Vnc driver produces a composite client.""" | ||
| instance = Vnc( | ||
| children={"tcp": FakeTcpDriver()}, | ||
| ) | ||
|
|
||
| with serve(instance) as client: | ||
| assert isinstance(client, CompositeClient) | ||
| assert isinstance(client.tcp, DriverClient) | ||
|
|
||
|
|
||
| def test_vnc_driver_raises_error_without_tcp_child(): | ||
| """Test that the Vnc driver raises a ConfigurationError if the tcp child is missing.""" | ||
| with pytest.raises(ConfigurationError, match="A tcp child is required for Vnc"): | ||
| Vnc(children={}) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("expected", [True, False]) | ||
| def test_vnc_driver_default_encrypt(expected): | ||
| """Test that the default_encrypt parameter is correctly handled.""" | ||
| instance = Vnc(children={"tcp": FakeTcpDriver()}, default_encrypt=expected) | ||
| with serve(instance) as client: | ||
| assert client.get_default_encrypt() is expected |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.