# Adapters Jumpstarter uses {term}`adapter`s to transform network connections established by drivers into different forms or interfaces that are more appropriate for specific use cases. ## Architecture {term}`Adapter`s in Jumpstarter follow a transformation pattern where: - {term}`Adapter`s take a driver client class as input - They transform the connection into a different interface format - The transformed interface is exposed to the user in a way that's tailored for specific scenarios The architecture consists of these key components: - **{term}`Adapter` Base** - {term}`Adapter`s typically follow a context manager pattern using Python's `with` statement for resource management. Each {term}`adapter` takes a driver client as input and transforms its connection. - **Connection Transformation** - {term}`Adapter`s create a new interface on top of an existing driver connection, such as forwarding ports, providing web interfaces, or offering terminal-like access. - **Resource Lifecycle** - {term}`Adapter`s handle proper setup and teardown of resources, ensuring connections are properly established and cleaned up. Unlike [Drivers](drivers.md), which establish the foundational connections to hardware or virtual interfaces, {term}`adapter`s focus on providing alternative ways to interact with those connections without modifying the underlying drivers. {term}`Adapter`s operate entirely on the client side and transform existing connections rather than establishing new ones directly with hardware or virtual devices. ## Types Different types of {term}`adapter`s serve different needs: - **Port Forwarding Adapters** - Convert network connections to local ports or sockets - **Interactive Adapters** - Provide interactive shells or console-like interfaces - **Protocol Adapters** - Transform connections to use different protocols (e.g., SSH, VNC) - **UI Adapters** - Create user interfaces for interacting with devices (e.g., web-based VNC) {term}`Adapter`s can be composed and extended for more complex scenarios: - **Chaining {term}`adapter`s**: Use the output of one {term}`adapter` as the input to another - **Custom {term}`adapter`s**: Create specialized {term}`adapter`s for specific hardware or software interfaces - **Extended functionality**: Add logging, monitoring, or security features on top of base {term}`adapter`s ## Implementation Patterns {term}`Adapter`s typically implement the context manager protocol (`__enter__` and `__exit__`) to ensure proper resource management. The general pattern is: 1. Initialize with a driver client reference 2. Set up the transformed connection in `__enter__` 3. Return the appropriate interface (URL, address, interactive object) 4. Clean up resources in `__exit__` This allows {term}`adapter`s to be used in `with` statements for clean, deterministic resource handling. When working with {term}`adapter`s, follow these recommended practices: 1. **Always use context managers** (`with` statements) to ensure proper resource cleanup and prevent resource leaks 2. **Consider security implications** when forwarding ports or providing network access, especially when exposing services to external networks 3. **Implement proper error handling and retries** for robust connections in unstable network environments 4. **Use appropriate timeouts** to prevent hanging connections and ensure responsiveness 5. **Consider performance implications** for long-running connections or high-throughput scenarios, especially in resource-constrained environments ## Example Implementation ```{testcode} from contextlib import contextmanager import socket import threading from typing import Tuple, Any class TcpPortforwardAdapter: """ Adapter that forwards a remote TCP port to a local TCP port. Args: client: A network driver client that provides a connection local_host: Host to bind to (default: 127.0.0.1) local_port: Port to bind to (default: 0, which selects a random port) Returns: A tuple of (host, port) when used as a context manager """ def __init__(self, client, local_host="127.0.0.1", local_port=0): self.client = client self.local_host = local_host self.local_port = local_port self._server = None self._thread = None def __enter__(self) -> Tuple[str, int]: # Create a socket server self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._server.bind((self.local_host, self.local_port)) self._server.listen(5) # Get the actual port (if we used port 0) self.local_host, self.local_port = self._server.getsockname() # Start a thread to handle connections self._thread = threading.Thread(target=self._handle_connections, daemon=True) self._thread.start() return (self.local_host, self.local_port) def __exit__(self, exc_type, exc_val, exc_tb): if self._server: self._server.close() self._server = None # Thread will exit because it's a daemon self._thread = None def _handle_connections(self): while True: try: client_socket, _ = self._server.accept() # For each connection, establish a connection to the remote # and set up bidirectional forwarding remote_conn = self.client.connect() self._start_forwarding(client_socket, remote_conn) except Exception: # Server was closed or other error break def _start_forwarding(self, local_socket, remote_conn): # Set up bidirectional forwarding between local_socket and remote_conn # Typically done with two threads, one for each direction # Implementation details depend on the specific driver client interface pass # Example usage: def example_usage(): # Assuming 'client' is a network driver client with TcpPortforwardAdapter(client, local_port=8080) as (host, port): print(f"Service available at {host}:{port}") # The service is now accessible at the local address # while this context is active ```