Skip to content
Open
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
41 changes: 34 additions & 7 deletions libs/python/computer/computer/computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,26 @@ def __exit__(self, exc_type, exc_val, exc_tb):
loop = asyncio.get_event_loop()
loop.run_until_complete(self.__aexit__(exc_type, exc_val, exc_tb))

def _resolve_interface_port(self) -> Optional[int]:
"""Determine which port the computer interface should target."""
provider_port: Optional[int] = None
if hasattr(self, "config") and getattr(self, "config", None) is not None:
vm_provider = getattr(self.config, "vm_provider", None)
provider_port = getattr(vm_provider, "api_port", None) if vm_provider else None

if isinstance(provider_port, int):
return provider_port

provider_type_str = (
self.provider_type.name.lower()
if isinstance(self.provider_type, VMProviderType)
else str(self.provider_type).lower()
)
if self.port is not None and provider_type_str == "docker":
return self.port

return None

async def run(self) -> Optional[str]:
"""Initialize the VM and computer interface."""
if TYPE_CHECKING:
Expand All @@ -252,7 +272,9 @@ async def run(self) -> Optional[str]:
self._interface = cast(
BaseComputerInterface,
InterfaceFactory.create_interface_for_os(
os=self.os_type, ip_address=ip_address # type: ignore[arg-type]
os=self.os_type,
ip_address=ip_address, # type: ignore[arg-type]
port=self._resolve_interface_port(),
),
)

Expand Down Expand Up @@ -464,6 +486,8 @@ async def run(self) -> Optional[str]:
self.logger.info(f"Initializing interface for {self.os_type} at {ip_address}")
from .interface.base import BaseComputerInterface

interface_port = self._resolve_interface_port()

# Pass authentication credentials if using cloud provider
if self.provider_type == VMProviderType.CLOUD and self.api_key and self.config.name:
self._interface = cast(
Expand All @@ -472,30 +496,33 @@ async def run(self) -> Optional[str]:
os=self.os_type,
ip_address=ip_address,
api_key=self.api_key,
vm_name=self.config.name
vm_name=self.config.name,
port=interface_port,
),
)
else:
self._interface = cast(
BaseComputerInterface,
InterfaceFactory.create_interface_for_os(
os=self.os_type,
ip_address=ip_address
os=self.os_type,
ip_address=ip_address,
port=interface_port,
),
)

# Wait for the WebSocket interface to be ready
self.logger.info("Connecting to WebSocket interface...")
self.logger.info(f"Connecting to WebSocket interface at {self._interface.ws_uri}...")

try:
# Use a single timeout for the entire connection process
# The VM should already be ready at this point, so we're just establishing the connection
await self._interface.wait_for_ready(timeout=30)
self.logger.info("WebSocket interface connected successfully")
except TimeoutError as e:
self.logger.error(f"Failed to connect to WebSocket interface at {ip_address}")
ws_uri = getattr(self._interface, "ws_uri", f"{ip_address}:unknown")
self.logger.error(f"Failed to connect to WebSocket interface at {ws_uri}")
raise TimeoutError(
f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}"
f"Could not connect to WebSocket interface at {ws_uri}: {str(e)}"
)
# self.logger.warning(
# f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}, expect missing functionality"
Expand Down
12 changes: 11 additions & 1 deletion libs/python/computer/computer/interface/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@
class BaseComputerInterface(ABC):
"""Base class for computer control interfaces."""

def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
def __init__(
self,
ip_address: str,
username: str = "lume",
password: str = "lume",
api_key: Optional[str] = None,
vm_name: Optional[str] = None,
port: Optional[int] = None,
):
"""Initialize interface.

Args:
Expand All @@ -17,12 +25,14 @@ def __init__(self, ip_address: str, username: str = "lume", password: str = "lum
password: Password for authentication
api_key: Optional API key for cloud authentication
vm_name: Optional VM name for cloud authentication
port: Optional custom port for the Computer API server
"""
self.ip_address = ip_address
self.username = username
self.password = password
self.api_key = api_key
self.vm_name = vm_name
self.port = port
self.logger = Logger("cua.interface", LogLevel.NORMAL)

# Optional default delay time between commands (in seconds)
Expand Down
9 changes: 5 additions & 4 deletions libs/python/computer/computer/interface/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def create_interface_for_os(
os: Literal['macos', 'linux', 'windows'],
ip_address: str,
api_key: Optional[str] = None,
vm_name: Optional[str] = None
vm_name: Optional[str] = None,
port: Optional[int] = None,
) -> BaseComputerInterface:
"""Create an interface for the specified OS.

Expand All @@ -33,10 +34,10 @@ def create_interface_for_os(
from .windows import WindowsComputerInterface

if os == 'macos':
return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name, port=port)
elif os == 'linux':
return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name, port=port)
elif os == 'windows':
return WindowsComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
return WindowsComputerInterface(ip_address, api_key=api_key, vm_name=vm_name, port=port)
else:
raise ValueError(f"Unsupported OS type: {os}")
18 changes: 13 additions & 5 deletions libs/python/computer/computer/interface/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@
class GenericComputerInterface(BaseComputerInterface):
"""Generic interface with common functionality for all supported platforms (Windows, Linux, macOS)."""

def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None, logger_name: str = "computer.interface.generic"):
super().__init__(ip_address, username, password, api_key, vm_name)
def __init__(
self,
ip_address: str,
username: str = "lume",
password: str = "lume",
api_key: Optional[str] = None,
vm_name: Optional[str] = None,
logger_name: str = "computer.interface.generic",
port: Optional[int] = None,
):
super().__init__(ip_address, username, password, api_key, vm_name, port)
self._ws = None
self._reconnect_task = None
self._closed = False
Expand Down Expand Up @@ -56,7 +65,7 @@ def ws_uri(self) -> str:
WebSocket URI for the Computer API Server
"""
protocol = "wss" if self.api_key else "ws"
port = "8443" if self.api_key else "8000"
port = self.port if self.port is not None else (8443 if self.api_key else 8000)
return f"{protocol}://{self.ip_address}:{port}/ws"

@property
Expand All @@ -67,7 +76,7 @@ def rest_uri(self) -> str:
REST URI for the Computer API Server
"""
protocol = "https" if self.api_key else "http"
port = "8443" if self.api_key else "8000"
port = self.port if self.port is not None else (8443 if self.api_key else 8000)
return f"{protocol}://{self.ip_address}:{port}/cmd"

# Mouse actions
Expand Down Expand Up @@ -970,4 +979,3 @@ def force_close(self):
if self._ws:
asyncio.create_task(self._ws.close())
self._ws = None

20 changes: 18 additions & 2 deletions libs/python/computer/computer/interface/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,21 @@
class LinuxComputerInterface(GenericComputerInterface):
"""Interface for Linux."""

def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
super().__init__(ip_address, username, password, api_key, vm_name, "computer.interface.linux")
def __init__(
self,
ip_address: str,
username: str = "lume",
password: str = "lume",
api_key: Optional[str] = None,
vm_name: Optional[str] = None,
port: Optional[int] = None,
):
super().__init__(
ip_address,
username,
password,
api_key,
vm_name,
"computer.interface.linux",
port,
)
22 changes: 19 additions & 3 deletions libs/python/computer/computer/interface/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,25 @@
class MacOSComputerInterface(GenericComputerInterface):
"""Interface for macOS."""

def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
super().__init__(ip_address, username, password, api_key, vm_name, "computer.interface.macos")
def __init__(
self,
ip_address: str,
username: str = "lume",
password: str = "lume",
api_key: Optional[str] = None,
vm_name: Optional[str] = None,
port: Optional[int] = None,
):
super().__init__(
ip_address,
username,
password,
api_key,
vm_name,
"computer.interface.macos",
port,
)

async def diorama_cmd(self, action: str, arguments: Optional[dict] = None) -> dict:
"""Send a diorama command to the server (macOS only)."""
return await self._send_command("diorama_cmd", {"action": action, "arguments": arguments or {}})
return await self._send_command("diorama_cmd", {"action": action, "arguments": arguments or {}})
20 changes: 18 additions & 2 deletions libs/python/computer/computer/interface/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,21 @@
class WindowsComputerInterface(GenericComputerInterface):
"""Interface for Windows."""

def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
super().__init__(ip_address, username, password, api_key, vm_name, "computer.interface.windows")
def __init__(
self,
ip_address: str,
username: str = "lume",
password: str = "lume",
api_key: Optional[str] = None,
vm_name: Optional[str] = None,
port: Optional[int] = None,
):
super().__init__(
ip_address,
username,
password,
api_key,
vm_name,
"computer.interface.windows",
port,
)
10 changes: 6 additions & 4 deletions libs/python/computer/computer/providers/docker/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
except (subprocess.SubprocessError, FileNotFoundError):
HAS_DOCKER = False

DEFAULT_API_PORT = 8000


class DockerProvider(BaseVMProvider):
"""
Expand All @@ -36,8 +38,8 @@ class DockerProvider(BaseVMProvider):
"""

def __init__(
self,
port: Optional[int] = 8000,
self,
port: Optional[int] = DEFAULT_API_PORT,
host: str = "localhost",
storage: Optional[str] = None,
shared_path: Optional[str] = None,
Expand All @@ -49,7 +51,7 @@ def __init__(
"""Initialize the Docker VM Provider.

Args:
port: Currently unused (VM provider port)
port: Host port for the computer-server API (default: DEFAULT_API_PORT)
host: Hostname for the API server (default: localhost)
storage: Path for persistent VM storage
shared_path: Path for shared folder between host and container
Expand All @@ -62,7 +64,7 @@ def __init__(
vnc_port: Port for VNC interface (default: 6901)
"""
self.host = host
self.api_port = 8000
self.api_port = DEFAULT_API_PORT if port is None else port
self.vnc_port = vnc_port
self.ephemeral = ephemeral

Expand Down
25 changes: 25 additions & 0 deletions libs/python/computer/tests/test_interface_ports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from computer.computer.interface.linux import LinuxComputerInterface


def test_linux_interface_default_port():
interface = LinuxComputerInterface("127.0.0.1")
assert interface.ws_uri == "ws://127.0.0.1:8000/ws"
assert interface.rest_uri == "http://127.0.0.1:8000/cmd"


def test_linux_interface_custom_port():
interface = LinuxComputerInterface("127.0.0.1", port=18000)
assert interface.ws_uri == "ws://127.0.0.1:18000/ws"
assert interface.rest_uri == "http://127.0.0.1:18000/cmd"


def test_linux_interface_secure_default_port():
interface = LinuxComputerInterface("example.com", api_key="secret")
assert interface.ws_uri == "wss://example.com:8443/ws"
assert interface.rest_uri == "https://example.com:8443/cmd"


def test_linux_interface_secure_custom_port():
interface = LinuxComputerInterface("example.com", api_key="secret", port=18000)
assert interface.ws_uri == "wss://example.com:18000/ws"
assert interface.rest_uri == "https://example.com:18000/cmd"