cua-computer 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- computer/computer.py +21 -1
- computer/providers/base.py +1 -0
- computer/providers/docker/__init__.py +13 -0
- computer/providers/docker/provider.py +502 -0
- computer/providers/factory.py +24 -0
- {cua_computer-0.4.1.dist-info → cua_computer-0.4.3.dist-info}/METADATA +1 -1
- {cua_computer-0.4.1.dist-info → cua_computer-0.4.3.dist-info}/RECORD +9 -7
- {cua_computer-0.4.1.dist-info → cua_computer-0.4.3.dist-info}/WHEEL +0 -0
- {cua_computer-0.4.1.dist-info → cua_computer-0.4.3.dist-info}/entry_points.txt +0 -0
computer/computer.py
CHANGED
@@ -43,7 +43,7 @@ class Computer:
|
|
43
43
|
cpu: str = "4",
|
44
44
|
os_type: OSType = "macos",
|
45
45
|
name: str = "",
|
46
|
-
image: str =
|
46
|
+
image: Optional[str] = None,
|
47
47
|
shared_directories: Optional[List[str]] = None,
|
48
48
|
use_host_computer_server: bool = False,
|
49
49
|
verbosity: Union[int, LogLevel] = logging.INFO,
|
@@ -88,6 +88,13 @@ class Computer:
|
|
88
88
|
self.logger = Logger("computer", verbosity)
|
89
89
|
self.logger.info("Initializing Computer...")
|
90
90
|
|
91
|
+
if not image:
|
92
|
+
if os_type == "macos":
|
93
|
+
image = "macos-sequoia-cua:latest"
|
94
|
+
elif os_type == "linux":
|
95
|
+
image = "trycua/cua-ubuntu:latest"
|
96
|
+
image = str(image)
|
97
|
+
|
91
98
|
# Store original parameters
|
92
99
|
self.image = image
|
93
100
|
self.port = port
|
@@ -301,6 +308,19 @@ class Computer:
|
|
301
308
|
storage=storage,
|
302
309
|
verbose=verbose,
|
303
310
|
ephemeral=ephemeral,
|
311
|
+
noVNC_port=noVNC_port,
|
312
|
+
)
|
313
|
+
elif self.provider_type == VMProviderType.DOCKER:
|
314
|
+
self.config.vm_provider = VMProviderFactory.create_provider(
|
315
|
+
self.provider_type,
|
316
|
+
port=port,
|
317
|
+
host=host,
|
318
|
+
storage=storage,
|
319
|
+
shared_path=shared_path,
|
320
|
+
image=image or "trycua/cua-ubuntu:latest",
|
321
|
+
verbose=verbose,
|
322
|
+
ephemeral=ephemeral,
|
323
|
+
noVNC_port=noVNC_port,
|
304
324
|
)
|
305
325
|
else:
|
306
326
|
raise ValueError(f"Unsupported provider type: {self.provider_type}")
|
computer/providers/base.py
CHANGED
@@ -0,0 +1,13 @@
|
|
1
|
+
"""Docker provider for running containers with computer-server."""
|
2
|
+
|
3
|
+
from .provider import DockerProvider
|
4
|
+
|
5
|
+
# Check if Docker is available
|
6
|
+
try:
|
7
|
+
import subprocess
|
8
|
+
subprocess.run(["docker", "--version"], capture_output=True, check=True)
|
9
|
+
HAS_DOCKER = True
|
10
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
11
|
+
HAS_DOCKER = False
|
12
|
+
|
13
|
+
__all__ = ["DockerProvider", "HAS_DOCKER"]
|
@@ -0,0 +1,502 @@
|
|
1
|
+
"""
|
2
|
+
Docker VM provider implementation.
|
3
|
+
|
4
|
+
This provider uses Docker containers running the CUA Ubuntu image to create
|
5
|
+
Linux VMs with computer-server. It handles VM lifecycle operations through Docker
|
6
|
+
commands and container management.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import logging
|
10
|
+
import json
|
11
|
+
import asyncio
|
12
|
+
from typing import Dict, List, Optional, Any
|
13
|
+
import subprocess
|
14
|
+
import time
|
15
|
+
import re
|
16
|
+
|
17
|
+
from ..base import BaseVMProvider, VMProviderType
|
18
|
+
|
19
|
+
# Setup logging
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
# Check if Docker is available
|
23
|
+
try:
|
24
|
+
subprocess.run(["docker", "--version"], capture_output=True, check=True)
|
25
|
+
HAS_DOCKER = True
|
26
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
27
|
+
HAS_DOCKER = False
|
28
|
+
|
29
|
+
|
30
|
+
class DockerProvider(BaseVMProvider):
|
31
|
+
"""
|
32
|
+
Docker VM Provider implementation using Docker containers.
|
33
|
+
|
34
|
+
This provider uses Docker to run containers with the CUA Ubuntu image
|
35
|
+
that includes computer-server for remote computer use.
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
port: Optional[int] = 8000,
|
41
|
+
host: str = "localhost",
|
42
|
+
storage: Optional[str] = None,
|
43
|
+
shared_path: Optional[str] = None,
|
44
|
+
image: str = "trycua/cua-ubuntu:latest",
|
45
|
+
verbose: bool = False,
|
46
|
+
ephemeral: bool = False,
|
47
|
+
vnc_port: Optional[int] = 6901,
|
48
|
+
):
|
49
|
+
"""Initialize the Docker VM Provider.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
port: Currently unused (VM provider port)
|
53
|
+
host: Hostname for the API server (default: localhost)
|
54
|
+
storage: Path for persistent VM storage
|
55
|
+
shared_path: Path for shared folder between host and container
|
56
|
+
image: Docker image to use (default: "trycua/cua-ubuntu:latest")
|
57
|
+
verbose: Enable verbose logging
|
58
|
+
ephemeral: Use ephemeral (temporary) storage
|
59
|
+
vnc_port: Port for VNC interface (default: 6901)
|
60
|
+
"""
|
61
|
+
self.host = host
|
62
|
+
self.api_port = 8000
|
63
|
+
self.vnc_port = vnc_port
|
64
|
+
self.ephemeral = ephemeral
|
65
|
+
|
66
|
+
# Handle ephemeral storage (temporary directory)
|
67
|
+
if ephemeral:
|
68
|
+
self.storage = "ephemeral"
|
69
|
+
else:
|
70
|
+
self.storage = storage
|
71
|
+
|
72
|
+
self.shared_path = shared_path
|
73
|
+
self.image = image
|
74
|
+
self.verbose = verbose
|
75
|
+
self._container_id = None
|
76
|
+
self._running_containers = {} # Track running containers by name
|
77
|
+
|
78
|
+
@property
|
79
|
+
def provider_type(self) -> VMProviderType:
|
80
|
+
"""Return the provider type."""
|
81
|
+
return VMProviderType.DOCKER
|
82
|
+
|
83
|
+
def _parse_memory(self, memory_str: str) -> str:
|
84
|
+
"""Parse memory string to Docker format.
|
85
|
+
|
86
|
+
Examples:
|
87
|
+
"8GB" -> "8g"
|
88
|
+
"1024MB" -> "1024m"
|
89
|
+
"512" -> "512m"
|
90
|
+
"""
|
91
|
+
if isinstance(memory_str, int):
|
92
|
+
return f"{memory_str}m"
|
93
|
+
|
94
|
+
if isinstance(memory_str, str):
|
95
|
+
# Extract number and unit
|
96
|
+
match = re.match(r"(\d+)([A-Za-z]*)", memory_str)
|
97
|
+
if match:
|
98
|
+
value, unit = match.groups()
|
99
|
+
unit = unit.upper()
|
100
|
+
|
101
|
+
if unit == "GB" or unit == "G":
|
102
|
+
return f"{value}g"
|
103
|
+
elif unit == "MB" or unit == "M" or unit == "":
|
104
|
+
return f"{value}m"
|
105
|
+
|
106
|
+
# Default fallback
|
107
|
+
logger.warning(f"Could not parse memory string '{memory_str}', using 4g default")
|
108
|
+
return "4g" # Default to 4GB
|
109
|
+
|
110
|
+
async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
111
|
+
"""Get VM information by name.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
name: Name of the VM to get information for
|
115
|
+
storage: Optional storage path override. If provided, this will be used
|
116
|
+
instead of the provider's default storage path.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
Dictionary with VM information including status, IP address, etc.
|
120
|
+
"""
|
121
|
+
try:
|
122
|
+
# Check if container exists and get its status
|
123
|
+
cmd = ["docker", "inspect", name]
|
124
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
125
|
+
|
126
|
+
if result.returncode != 0:
|
127
|
+
# Container doesn't exist
|
128
|
+
return {
|
129
|
+
"name": name,
|
130
|
+
"status": "not_found",
|
131
|
+
"ip_address": None,
|
132
|
+
"ports": {},
|
133
|
+
"image": self.image,
|
134
|
+
"provider": "docker"
|
135
|
+
}
|
136
|
+
|
137
|
+
# Parse container info
|
138
|
+
container_info = json.loads(result.stdout)[0]
|
139
|
+
state = container_info["State"]
|
140
|
+
network_settings = container_info["NetworkSettings"]
|
141
|
+
|
142
|
+
# Determine status
|
143
|
+
if state["Running"]:
|
144
|
+
status = "running"
|
145
|
+
elif state["Paused"]:
|
146
|
+
status = "paused"
|
147
|
+
else:
|
148
|
+
status = "stopped"
|
149
|
+
|
150
|
+
# Get IP address
|
151
|
+
ip_address = network_settings.get("IPAddress", "")
|
152
|
+
if not ip_address and "Networks" in network_settings:
|
153
|
+
# Try to get IP from bridge network
|
154
|
+
for network_name, network_info in network_settings["Networks"].items():
|
155
|
+
if network_info.get("IPAddress"):
|
156
|
+
ip_address = network_info["IPAddress"]
|
157
|
+
break
|
158
|
+
|
159
|
+
# Get port mappings
|
160
|
+
ports = {}
|
161
|
+
if "Ports" in network_settings and network_settings["Ports"]:
|
162
|
+
# network_settings["Ports"] is a dict like:
|
163
|
+
# {'6901/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '6901'}, ...], ...}
|
164
|
+
for container_port, port_mappings in network_settings["Ports"].items():
|
165
|
+
if port_mappings: # Check if there are any port mappings
|
166
|
+
# Take the first mapping (usually the IPv4 one)
|
167
|
+
for mapping in port_mappings:
|
168
|
+
if mapping.get("HostPort"):
|
169
|
+
ports[container_port] = mapping["HostPort"]
|
170
|
+
break # Use the first valid mapping
|
171
|
+
|
172
|
+
return {
|
173
|
+
"name": name,
|
174
|
+
"status": status,
|
175
|
+
"ip_address": ip_address or "127.0.0.1", # Use localhost if no IP
|
176
|
+
"ports": ports,
|
177
|
+
"image": container_info["Config"]["Image"],
|
178
|
+
"provider": "docker",
|
179
|
+
"container_id": container_info["Id"][:12], # Short ID
|
180
|
+
"created": container_info["Created"],
|
181
|
+
"started": state.get("StartedAt", ""),
|
182
|
+
}
|
183
|
+
|
184
|
+
except Exception as e:
|
185
|
+
logger.error(f"Error getting VM info for {name}: {e}")
|
186
|
+
import traceback
|
187
|
+
traceback.print_exc()
|
188
|
+
return {
|
189
|
+
"name": name,
|
190
|
+
"status": "error",
|
191
|
+
"error": str(e),
|
192
|
+
"provider": "docker"
|
193
|
+
}
|
194
|
+
|
195
|
+
async def list_vms(self) -> List[Dict[str, Any]]:
|
196
|
+
"""List all Docker containers managed by this provider."""
|
197
|
+
try:
|
198
|
+
# List all containers (running and stopped) with the CUA image
|
199
|
+
cmd = ["docker", "ps", "-a", "--filter", f"ancestor={self.image}", "--format", "json"]
|
200
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
201
|
+
|
202
|
+
containers = []
|
203
|
+
if result.stdout.strip():
|
204
|
+
for line in result.stdout.strip().split('\n'):
|
205
|
+
if line.strip():
|
206
|
+
container_data = json.loads(line)
|
207
|
+
vm_info = await self.get_vm(container_data["Names"])
|
208
|
+
containers.append(vm_info)
|
209
|
+
|
210
|
+
return containers
|
211
|
+
|
212
|
+
except subprocess.CalledProcessError as e:
|
213
|
+
logger.error(f"Error listing containers: {e.stderr}")
|
214
|
+
return []
|
215
|
+
except Exception as e:
|
216
|
+
logger.error(f"Error listing VMs: {e}")
|
217
|
+
import traceback
|
218
|
+
traceback.print_exc()
|
219
|
+
return []
|
220
|
+
|
221
|
+
async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
222
|
+
"""Run a VM with the given options.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
image: Name/tag of the Docker image to use
|
226
|
+
name: Name of the container to run
|
227
|
+
run_opts: Options for running the VM, including:
|
228
|
+
- memory: Memory limit (e.g., "4GB", "2048MB")
|
229
|
+
- cpu: CPU limit (e.g., 2 for 2 cores)
|
230
|
+
- vnc_port: Specific port for VNC interface
|
231
|
+
- api_port: Specific port for computer-server API
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Dictionary with VM status information
|
235
|
+
"""
|
236
|
+
try:
|
237
|
+
# Check if container already exists
|
238
|
+
existing_vm = await self.get_vm(name, storage)
|
239
|
+
if existing_vm["status"] == "running":
|
240
|
+
logger.info(f"Container {name} is already running")
|
241
|
+
return existing_vm
|
242
|
+
elif existing_vm["status"] in ["stopped", "paused"]:
|
243
|
+
# Start existing container
|
244
|
+
logger.info(f"Starting existing container {name}")
|
245
|
+
start_cmd = ["docker", "start", name]
|
246
|
+
result = subprocess.run(start_cmd, capture_output=True, text=True, check=True)
|
247
|
+
|
248
|
+
# Wait for container to be ready
|
249
|
+
await self._wait_for_container_ready(name)
|
250
|
+
return await self.get_vm(name, storage)
|
251
|
+
|
252
|
+
# Use provided image or default
|
253
|
+
docker_image = image if image != "default" else self.image
|
254
|
+
|
255
|
+
# Build docker run command
|
256
|
+
cmd = ["docker", "run", "-d", "--name", name]
|
257
|
+
|
258
|
+
# Add memory limit if specified
|
259
|
+
if "memory" in run_opts:
|
260
|
+
memory_limit = self._parse_memory(run_opts["memory"])
|
261
|
+
cmd.extend(["--memory", memory_limit])
|
262
|
+
|
263
|
+
# Add CPU limit if specified
|
264
|
+
if "cpu" in run_opts:
|
265
|
+
cpu_count = str(run_opts["cpu"])
|
266
|
+
cmd.extend(["--cpus", cpu_count])
|
267
|
+
|
268
|
+
# Add port mappings
|
269
|
+
vnc_port = run_opts.get("vnc_port", self.vnc_port)
|
270
|
+
api_port = run_opts.get("api_port", self.api_port)
|
271
|
+
|
272
|
+
if vnc_port:
|
273
|
+
cmd.extend(["-p", f"{vnc_port}:6901"]) # VNC port
|
274
|
+
if api_port:
|
275
|
+
cmd.extend(["-p", f"{api_port}:8000"]) # computer-server API port
|
276
|
+
|
277
|
+
# Add volume mounts if storage is specified
|
278
|
+
storage_path = storage or self.storage
|
279
|
+
if storage_path and storage_path != "ephemeral":
|
280
|
+
# Mount storage directory
|
281
|
+
cmd.extend(["-v", f"{storage_path}:/home/kasm-user/storage"])
|
282
|
+
|
283
|
+
# Add shared path if specified
|
284
|
+
if self.shared_path:
|
285
|
+
cmd.extend(["-v", f"{self.shared_path}:/home/kasm-user/shared"])
|
286
|
+
|
287
|
+
# Add environment variables
|
288
|
+
cmd.extend(["-e", "VNC_PW=password"]) # Set VNC password
|
289
|
+
cmd.extend(["-e", "VNCOPTIONS=-disableBasicAuth"]) # Disable VNC basic auth
|
290
|
+
|
291
|
+
# Add the image
|
292
|
+
cmd.append(docker_image)
|
293
|
+
|
294
|
+
logger.info(f"Running Docker container with command: {' '.join(cmd)}")
|
295
|
+
|
296
|
+
# Run the container
|
297
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
298
|
+
container_id = result.stdout.strip()
|
299
|
+
|
300
|
+
logger.info(f"Container {name} started with ID: {container_id[:12]}")
|
301
|
+
|
302
|
+
# Store container info
|
303
|
+
self._container_id = container_id
|
304
|
+
self._running_containers[name] = container_id
|
305
|
+
|
306
|
+
# Wait for container to be ready
|
307
|
+
await self._wait_for_container_ready(name)
|
308
|
+
|
309
|
+
# Return VM info
|
310
|
+
vm_info = await self.get_vm(name, storage)
|
311
|
+
vm_info["container_id"] = container_id[:12]
|
312
|
+
|
313
|
+
return vm_info
|
314
|
+
|
315
|
+
except subprocess.CalledProcessError as e:
|
316
|
+
error_msg = f"Failed to run container {name}: {e.stderr}"
|
317
|
+
logger.error(error_msg)
|
318
|
+
return {
|
319
|
+
"name": name,
|
320
|
+
"status": "error",
|
321
|
+
"error": error_msg,
|
322
|
+
"provider": "docker"
|
323
|
+
}
|
324
|
+
except Exception as e:
|
325
|
+
error_msg = f"Error running VM {name}: {e}"
|
326
|
+
logger.error(error_msg)
|
327
|
+
return {
|
328
|
+
"name": name,
|
329
|
+
"status": "error",
|
330
|
+
"error": error_msg,
|
331
|
+
"provider": "docker"
|
332
|
+
}
|
333
|
+
|
334
|
+
async def _wait_for_container_ready(self, container_name: str, timeout: int = 60) -> bool:
|
335
|
+
"""Wait for the Docker container to be fully ready.
|
336
|
+
|
337
|
+
Args:
|
338
|
+
container_name: Name of the Docker container to check
|
339
|
+
timeout: Maximum time to wait in seconds (default: 60 seconds)
|
340
|
+
|
341
|
+
Returns:
|
342
|
+
True if the container is running and ready
|
343
|
+
"""
|
344
|
+
logger.info(f"Waiting for container {container_name} to be ready...")
|
345
|
+
|
346
|
+
start_time = time.time()
|
347
|
+
while time.time() - start_time < timeout:
|
348
|
+
try:
|
349
|
+
# Check if container is running
|
350
|
+
vm_info = await self.get_vm(container_name)
|
351
|
+
if vm_info["status"] == "running":
|
352
|
+
logger.info(f"Container {container_name} is running")
|
353
|
+
|
354
|
+
# Additional check: try to connect to computer-server API
|
355
|
+
# This is optional - we'll just wait a bit more for services to start
|
356
|
+
await asyncio.sleep(5)
|
357
|
+
return True
|
358
|
+
|
359
|
+
except Exception as e:
|
360
|
+
logger.debug(f"Container {container_name} not ready yet: {e}")
|
361
|
+
|
362
|
+
await asyncio.sleep(2)
|
363
|
+
|
364
|
+
logger.warning(f"Container {container_name} did not become ready within {timeout} seconds")
|
365
|
+
return False
|
366
|
+
|
367
|
+
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
368
|
+
"""Stop a running VM by stopping the Docker container."""
|
369
|
+
try:
|
370
|
+
logger.info(f"Stopping container {name}")
|
371
|
+
|
372
|
+
# Stop the container
|
373
|
+
cmd = ["docker", "stop", name]
|
374
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
375
|
+
|
376
|
+
# Remove from running containers tracking
|
377
|
+
if name in self._running_containers:
|
378
|
+
del self._running_containers[name]
|
379
|
+
|
380
|
+
logger.info(f"Container {name} stopped successfully")
|
381
|
+
|
382
|
+
return {
|
383
|
+
"name": name,
|
384
|
+
"status": "stopped",
|
385
|
+
"message": "Container stopped successfully",
|
386
|
+
"provider": "docker"
|
387
|
+
}
|
388
|
+
|
389
|
+
except subprocess.CalledProcessError as e:
|
390
|
+
error_msg = f"Failed to stop container {name}: {e.stderr}"
|
391
|
+
logger.error(error_msg)
|
392
|
+
return {
|
393
|
+
"name": name,
|
394
|
+
"status": "error",
|
395
|
+
"error": error_msg,
|
396
|
+
"provider": "docker"
|
397
|
+
}
|
398
|
+
except Exception as e:
|
399
|
+
error_msg = f"Error stopping VM {name}: {e}"
|
400
|
+
logger.error(error_msg)
|
401
|
+
return {
|
402
|
+
"name": name,
|
403
|
+
"status": "error",
|
404
|
+
"error": error_msg,
|
405
|
+
"provider": "docker"
|
406
|
+
}
|
407
|
+
|
408
|
+
async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
409
|
+
"""Update VM configuration.
|
410
|
+
|
411
|
+
Note: Docker containers cannot be updated while running.
|
412
|
+
This method will return an error suggesting to recreate the container.
|
413
|
+
"""
|
414
|
+
return {
|
415
|
+
"name": name,
|
416
|
+
"status": "error",
|
417
|
+
"error": "Docker containers cannot be updated while running. Please stop and recreate the container with new options.",
|
418
|
+
"provider": "docker"
|
419
|
+
}
|
420
|
+
|
421
|
+
async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
|
422
|
+
"""Get the IP address of a VM, waiting indefinitely until it's available.
|
423
|
+
|
424
|
+
Args:
|
425
|
+
name: Name of the VM to get the IP for
|
426
|
+
storage: Optional storage path override
|
427
|
+
retry_delay: Delay between retries in seconds (default: 2)
|
428
|
+
|
429
|
+
Returns:
|
430
|
+
IP address of the VM when it becomes available
|
431
|
+
"""
|
432
|
+
logger.info(f"Getting IP address for container {name}")
|
433
|
+
|
434
|
+
total_attempts = 0
|
435
|
+
while True:
|
436
|
+
total_attempts += 1
|
437
|
+
|
438
|
+
try:
|
439
|
+
vm_info = await self.get_vm(name, storage)
|
440
|
+
|
441
|
+
if vm_info["status"] == "error":
|
442
|
+
raise Exception(f"VM is in error state: {vm_info.get('error', 'Unknown error')}")
|
443
|
+
|
444
|
+
# TODO: for now, return localhost
|
445
|
+
# it seems the docker container is not accessible from the host
|
446
|
+
# on WSL2, unless you port forward? not sure
|
447
|
+
if True:
|
448
|
+
logger.warning("Overriding container IP with localhost")
|
449
|
+
return "localhost"
|
450
|
+
|
451
|
+
# Check if we got a valid IP
|
452
|
+
ip = vm_info.get("ip_address", None)
|
453
|
+
if ip and ip != "unknown" and not ip.startswith("0.0.0.0"):
|
454
|
+
logger.info(f"Got valid container IP address: {ip}")
|
455
|
+
return ip
|
456
|
+
|
457
|
+
# For Docker containers, we can also use localhost if ports are mapped
|
458
|
+
if vm_info["status"] == "running" and vm_info.get("ports"):
|
459
|
+
logger.info(f"Container is running with port mappings, using localhost")
|
460
|
+
return "127.0.0.1"
|
461
|
+
|
462
|
+
# Check the container status
|
463
|
+
status = vm_info.get("status", "unknown")
|
464
|
+
|
465
|
+
if status == "stopped":
|
466
|
+
logger.info(f"Container status is {status}, but still waiting for it to start")
|
467
|
+
elif status != "running":
|
468
|
+
logger.info(f"Container is not running yet (status: {status}). Waiting...")
|
469
|
+
else:
|
470
|
+
logger.info("Container is running but no valid IP address yet. Waiting...")
|
471
|
+
|
472
|
+
except Exception as e:
|
473
|
+
logger.warning(f"Error getting container {name} IP: {e}, continuing to wait...")
|
474
|
+
|
475
|
+
# Wait before next retry
|
476
|
+
await asyncio.sleep(retry_delay)
|
477
|
+
|
478
|
+
# Add progress log every 10 attempts
|
479
|
+
if total_attempts % 10 == 0:
|
480
|
+
logger.info(f"Still waiting for container {name} IP after {total_attempts} attempts...")
|
481
|
+
|
482
|
+
async def __aenter__(self):
|
483
|
+
"""Async context manager entry."""
|
484
|
+
logger.debug("Entering DockerProvider context")
|
485
|
+
return self
|
486
|
+
|
487
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
488
|
+
"""Async context manager exit.
|
489
|
+
|
490
|
+
This method handles cleanup of running containers if needed.
|
491
|
+
"""
|
492
|
+
logger.debug(f"Exiting DockerProvider context, handling exceptions: {exc_type}")
|
493
|
+
try:
|
494
|
+
# Optionally stop running containers on context exit
|
495
|
+
# For now, we'll leave containers running as they might be needed
|
496
|
+
# Users can manually stop them if needed
|
497
|
+
pass
|
498
|
+
except Exception as e:
|
499
|
+
logger.error(f"Error during DockerProvider cleanup: {e}")
|
500
|
+
if exc_type is None:
|
501
|
+
raise
|
502
|
+
return False
|
computer/providers/factory.py
CHANGED
@@ -134,5 +134,29 @@ class VMProviderFactory:
|
|
134
134
|
"pywinsandbox is required for WinSandboxProvider. "
|
135
135
|
"Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
|
136
136
|
) from e
|
137
|
+
elif provider_type == VMProviderType.DOCKER:
|
138
|
+
try:
|
139
|
+
from .docker import DockerProvider, HAS_DOCKER
|
140
|
+
if not HAS_DOCKER:
|
141
|
+
raise ImportError(
|
142
|
+
"Docker is required for DockerProvider. "
|
143
|
+
"Please install Docker and ensure it is running."
|
144
|
+
)
|
145
|
+
return DockerProvider(
|
146
|
+
port=port,
|
147
|
+
host=host,
|
148
|
+
storage=storage,
|
149
|
+
shared_path=shared_path,
|
150
|
+
image=image or "trycua/cua-ubuntu:latest",
|
151
|
+
verbose=verbose,
|
152
|
+
ephemeral=ephemeral,
|
153
|
+
vnc_port=noVNC_port
|
154
|
+
)
|
155
|
+
except ImportError as e:
|
156
|
+
logger.error(f"Failed to import DockerProvider: {e}")
|
157
|
+
raise ImportError(
|
158
|
+
"Docker is required for DockerProvider. "
|
159
|
+
"Please install Docker and ensure it is running."
|
160
|
+
) from e
|
137
161
|
else:
|
138
162
|
raise ValueError(f"Unsupported provider type: {provider_type}")
|
@@ -1,5 +1,5 @@
|
|
1
1
|
computer/__init__.py,sha256=44ZBq815dMihgAHmBKn1S_GFNbElCXyZInh3hle1k9Y,1237
|
2
|
-
computer/computer.py,sha256=
|
2
|
+
computer/computer.py,sha256=u-M9pZM3Tc0PEtV13M6Dj8_yaW6HqMHJwlJLVbA7XtQ,42398
|
3
3
|
computer/diorama_computer.py,sha256=jOP7_eXxxU6SMIoE25ni0YXPK0E7p5sZeLKmkYLh6G8,3871
|
4
4
|
computer/helpers.py,sha256=iHkO2WhuCLc15g67kfMnpQWxfNRlz2YeJNEvYaL9jlM,1826
|
5
5
|
computer/interface/__init__.py,sha256=xQvYjq5PMn9ZJOmRR5mWtONTl_0HVd8ACvW6AQnzDdw,262
|
@@ -13,10 +13,12 @@ computer/interface/windows.py,sha256=Ww4YKVzTvI-TeQvmZFY8HnPfau0Og4jBw0TtQvwJL30
|
|
13
13
|
computer/logger.py,sha256=UVvnmZGOWVF9TCsixEbeQnDZ3wBPAJ2anW3Zp-MoJ8Y,2896
|
14
14
|
computer/models.py,sha256=iFNM1QfZArD8uf66XJXb2EDIREsfrxqqA5_liLBMfrE,1188
|
15
15
|
computer/providers/__init__.py,sha256=hS9lLxmmHa1u82XJJ_xuqSKipClsYUEPx-8OK9ogtVg,194
|
16
|
-
computer/providers/base.py,sha256=
|
16
|
+
computer/providers/base.py,sha256=GB_TBuetNlp2vDkFYr3IUg5bQsHWHDv0g8BlSjsnFXQ,3676
|
17
17
|
computer/providers/cloud/__init__.py,sha256=SDAcfhI2BlmVBrBZOHxQd3i1bJZjMIfl7QgmqjXa4z8,144
|
18
18
|
computer/providers/cloud/provider.py,sha256=XEdCrnZzRwvvkPHIwfhfJl3xB6W7tZKdBI0duKEXLw4,2930
|
19
|
-
computer/providers/
|
19
|
+
computer/providers/docker/__init__.py,sha256=Sv_78qeNyx1i7YySDHUL9trgkGv3eZXrf2BwqxFCUws,386
|
20
|
+
computer/providers/docker/provider.py,sha256=_wgMhQVgxe0A984Chpb27Fu_deVy4tWuzyEmnyQ081A,20062
|
21
|
+
computer/providers/factory.py,sha256=8TmwktXpNBsIur8XdE4HC1B21pLARpAIABSRig_8vW4,6727
|
20
22
|
computer/providers/lume/__init__.py,sha256=E6hTbVQF5lLZD8JyG4rTwUnCBO4q9K8UkYNQ31R0h7c,193
|
21
23
|
computer/providers/lume/provider.py,sha256=grLZeXd4Y8iYsNq2gfNGcQq1bnTcNYNepEv-mxmROG4,20562
|
22
24
|
computer/providers/lume_api.py,sha256=i9dXJGrUhfA49VSY4p6_O6_AzeLNlRppG7jbM3jIJmU,19581
|
@@ -31,7 +33,7 @@ computer/ui/__main__.py,sha256=Jwy2oC_mGZLN0fX7WLqpjaQkbXMeM3ISrUc8WSRUG0c,284
|
|
31
33
|
computer/ui/gradio/__init__.py,sha256=5_KimixM48-X74FCsLw7LbSt39MQfUMEL8-M9amK3Cw,117
|
32
34
|
computer/ui/gradio/app.py,sha256=_V6FI-g0GJGMEk-C2iPFtxPO1Gn0juCaeCrWsBtjC4E,70395
|
33
35
|
computer/utils.py,sha256=zY50NXB7r51GNLQ6l7lhG_qv0_ufpQ8n0-SDhCei8m4,2838
|
34
|
-
cua_computer-0.4.
|
35
|
-
cua_computer-0.4.
|
36
|
-
cua_computer-0.4.
|
37
|
-
cua_computer-0.4.
|
36
|
+
cua_computer-0.4.3.dist-info/METADATA,sha256=ACsrn0_9Eyqrev62IeSun_AYPWO1mOtx-iHQCkmkIUM,5802
|
37
|
+
cua_computer-0.4.3.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
38
|
+
cua_computer-0.4.3.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
39
|
+
cua_computer-0.4.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|