cua-computer 0.1.29__tar.gz → 0.2.1__tar.gz
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.
- {cua_computer-0.1.29 → cua_computer-0.2.1}/PKG-INFO +7 -2
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/__init__.py +5 -1
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/computer.py +268 -140
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/interface/factory.py +4 -1
- cua_computer-0.2.1/computer/interface/linux.py +599 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/interface/macos.py +9 -1
- cua_computer-0.2.1/computer/models.py +47 -0
- cua_computer-0.2.1/computer/providers/__init__.py +4 -0
- cua_computer-0.2.1/computer/providers/base.py +105 -0
- cua_computer-0.2.1/computer/providers/cloud/__init__.py +5 -0
- cua_computer-0.2.1/computer/providers/cloud/provider.py +100 -0
- cua_computer-0.2.1/computer/providers/factory.py +118 -0
- cua_computer-0.2.1/computer/providers/lume/__init__.py +9 -0
- cua_computer-0.2.1/computer/providers/lume/provider.py +541 -0
- cua_computer-0.2.1/computer/providers/lume_api.py +559 -0
- cua_computer-0.2.1/computer/providers/lumier/__init__.py +8 -0
- cua_computer-0.2.1/computer/providers/lumier/provider.py +943 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/pyproject.toml +10 -4
- cua_computer-0.1.29/computer/interface/linux.py +0 -27
- cua_computer-0.1.29/computer/models.py +0 -35
- {cua_computer-0.1.29 → cua_computer-0.2.1}/README.md +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/interface/__init__.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/interface/base.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/interface/models.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/logger.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/telemetry.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/ui/__init__.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/ui/gradio/__init__.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/ui/gradio/app.py +0 -0
- {cua_computer-0.1.29 → cua_computer-0.2.1}/computer/utils.py +0 -0
@@ -1,20 +1,25 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: cua-computer
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: Computer-Use Interface (CUI) framework powering Cua
|
5
5
|
Author-Email: TryCua <gh@trycua.com>
|
6
6
|
Requires-Python: >=3.10
|
7
|
-
Requires-Dist: pylume>=0.1.8
|
8
7
|
Requires-Dist: pillow>=10.0.0
|
9
8
|
Requires-Dist: websocket-client>=1.8.0
|
10
9
|
Requires-Dist: websockets>=12.0
|
11
10
|
Requires-Dist: aiohttp>=3.9.0
|
12
11
|
Requires-Dist: cua-core<0.2.0,>=0.1.0
|
13
12
|
Requires-Dist: pydantic>=2.11.1
|
13
|
+
Provides-Extra: lume
|
14
|
+
Provides-Extra: lumier
|
14
15
|
Provides-Extra: ui
|
15
16
|
Requires-Dist: gradio<6.0.0,>=5.23.3; extra == "ui"
|
16
17
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.1; extra == "ui"
|
17
18
|
Requires-Dist: datasets<4.0.0,>=3.6.0; extra == "ui"
|
19
|
+
Provides-Extra: all
|
20
|
+
Requires-Dist: gradio<6.0.0,>=5.23.3; extra == "all"
|
21
|
+
Requires-Dist: python-dotenv<2.0.0,>=1.0.1; extra == "all"
|
22
|
+
Requires-Dist: datasets<4.0.0,>=3.6.0; extra == "all"
|
18
23
|
Description-Content-Type: text/markdown
|
19
24
|
|
20
25
|
<div align="center">
|
@@ -42,6 +42,10 @@ except Exception as e:
|
|
42
42
|
# Other issues with telemetry
|
43
43
|
logger.warning(f"Error initializing telemetry: {e}")
|
44
44
|
|
45
|
+
# Core components
|
45
46
|
from .computer import Computer
|
46
47
|
|
47
|
-
|
48
|
+
# Provider components
|
49
|
+
from .providers.base import VMProviderType
|
50
|
+
|
51
|
+
__all__ = ["Computer", "VMProviderType"]
|
@@ -1,6 +1,4 @@
|
|
1
1
|
from typing import Optional, List, Literal, Dict, Any, Union, TYPE_CHECKING, cast
|
2
|
-
from pylume import PyLume
|
3
|
-
from pylume.models import VMRunOpts, VMUpdateOpts, ImageRef, SharedDirectory, VMStatus
|
4
2
|
import asyncio
|
5
3
|
from .models import Computer as ComputerConfig, Display
|
6
4
|
from .interface.factory import InterfaceFactory
|
@@ -14,12 +12,11 @@ import logging
|
|
14
12
|
from .telemetry import record_computer_initialization
|
15
13
|
import os
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
if TYPE_CHECKING:
|
21
|
-
from .interface.base import BaseComputerInterface
|
15
|
+
# Import provider related modules
|
16
|
+
from .providers.base import VMProviderType
|
17
|
+
from .providers.factory import VMProviderFactory
|
22
18
|
|
19
|
+
OSType = Literal["macos", "linux", "windows"]
|
23
20
|
|
24
21
|
class Computer:
|
25
22
|
"""Computer is the main class for interacting with the computer."""
|
@@ -36,8 +33,12 @@ class Computer:
|
|
36
33
|
use_host_computer_server: bool = False,
|
37
34
|
verbosity: Union[int, LogLevel] = logging.INFO,
|
38
35
|
telemetry_enabled: bool = True,
|
39
|
-
|
36
|
+
provider_type: Union[str, VMProviderType] = VMProviderType.LUME,
|
37
|
+
port: Optional[int] = 7777,
|
38
|
+
noVNC_port: Optional[int] = 8006,
|
40
39
|
host: str = os.environ.get("PYLUME_HOST", "localhost"),
|
40
|
+
storage: Optional[str] = None,
|
41
|
+
ephemeral: bool = False
|
41
42
|
):
|
42
43
|
"""Initialize a new Computer instance.
|
43
44
|
|
@@ -49,7 +50,7 @@ class Computer:
|
|
49
50
|
Defaults to "1024x768"
|
50
51
|
memory: The VM memory allocation. Defaults to "8GB"
|
51
52
|
cpu: The VM CPU allocation. Defaults to "4"
|
52
|
-
|
53
|
+
os_type: The operating system type ('macos' or 'linux')
|
53
54
|
name: The VM name
|
54
55
|
image: The VM image name
|
55
56
|
shared_directories: Optional list of directory paths to share with the VM
|
@@ -57,8 +58,12 @@ class Computer:
|
|
57
58
|
verbosity: Logging level (standard Python logging levels: logging.DEBUG, logging.INFO, etc.)
|
58
59
|
LogLevel enum values are still accepted for backward compatibility
|
59
60
|
telemetry_enabled: Whether to enable telemetry tracking. Defaults to True.
|
60
|
-
|
61
|
-
|
61
|
+
provider_type: The VM provider type to use (lume, qemu, cloud)
|
62
|
+
port: Optional port to use for the VM provider server
|
63
|
+
noVNC_port: Optional port for the noVNC web interface (Lumier provider)
|
64
|
+
host: Host to use for VM provider connections (e.g. "localhost", "host.docker.internal")
|
65
|
+
storage: Optional path for persistent VM storage (Lumier provider)
|
66
|
+
ephemeral: Whether to use ephemeral storage
|
62
67
|
"""
|
63
68
|
|
64
69
|
self.logger = Logger("cua.computer", verbosity)
|
@@ -67,8 +72,23 @@ class Computer:
|
|
67
72
|
# Store original parameters
|
68
73
|
self.image = image
|
69
74
|
self.port = port
|
75
|
+
self.noVNC_port = noVNC_port
|
70
76
|
self.host = host
|
71
77
|
self.os_type = os_type
|
78
|
+
self.provider_type = provider_type
|
79
|
+
self.ephemeral = ephemeral
|
80
|
+
|
81
|
+
if ephemeral:
|
82
|
+
self.storage = "ephemeral"
|
83
|
+
else:
|
84
|
+
self.storage = storage
|
85
|
+
|
86
|
+
# For Lumier provider, store the first shared directory path to use
|
87
|
+
# for VM file sharing
|
88
|
+
self.shared_path = None
|
89
|
+
if shared_directories and len(shared_directories) > 0:
|
90
|
+
self.shared_path = shared_directories[0]
|
91
|
+
self.logger.info(f"Using first shared directory for VM file sharing: {self.shared_path}")
|
72
92
|
|
73
93
|
# Store telemetry preference
|
74
94
|
self._telemetry_enabled = telemetry_enabled
|
@@ -116,25 +136,17 @@ class Computer:
|
|
116
136
|
memory=memory,
|
117
137
|
cpu=cpu,
|
118
138
|
)
|
119
|
-
# Initialize
|
120
|
-
self.config.
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
139
|
+
# Initialize VM provider but don't start it yet - we'll do that in run()
|
140
|
+
self.config.vm_provider = None # Will be initialized in run()
|
141
|
+
|
142
|
+
# Store shared directories config
|
143
|
+
self.shared_directories = shared_directories or []
|
144
|
+
|
145
|
+
# Placeholder for VM provider context manager
|
146
|
+
self._provider_context = None
|
126
147
|
|
127
148
|
# Initialize with proper typing - None at first, will be set in run()
|
128
149
|
self._interface = None
|
129
|
-
self.os = os
|
130
|
-
self.shared_paths = []
|
131
|
-
if shared_directories:
|
132
|
-
for path in shared_directories:
|
133
|
-
abs_path = os.path.abspath(os.path.expanduser(path))
|
134
|
-
if not os.path.exists(abs_path):
|
135
|
-
raise ValueError(f"Shared directory does not exist: {path}")
|
136
|
-
self.shared_paths.append(abs_path)
|
137
|
-
self._pylume_context = None
|
138
150
|
self.use_host_computer_server = use_host_computer_server
|
139
151
|
|
140
152
|
# Record initialization in telemetry (if enabled)
|
@@ -145,7 +157,6 @@ class Computer:
|
|
145
157
|
|
146
158
|
async def __aenter__(self):
|
147
159
|
"""Enter async context manager."""
|
148
|
-
await self.run()
|
149
160
|
return self
|
150
161
|
|
151
162
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
@@ -164,7 +175,7 @@ class Computer:
|
|
164
175
|
# We could add cleanup here if needed in the future
|
165
176
|
pass
|
166
177
|
|
167
|
-
async def run(self) ->
|
178
|
+
async def run(self) -> Optional[str]:
|
168
179
|
"""Initialize the VM and computer interface."""
|
169
180
|
if TYPE_CHECKING:
|
170
181
|
from .interface.base import BaseComputerInterface
|
@@ -199,87 +210,166 @@ class Computer:
|
|
199
210
|
else:
|
200
211
|
# Start or connect to VM
|
201
212
|
self.logger.info(f"Starting VM: {self.image}")
|
202
|
-
if not self.
|
213
|
+
if not self._provider_context:
|
203
214
|
try:
|
204
|
-
self.
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
#
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
215
|
+
provider_type_name = self.provider_type.name if isinstance(self.provider_type, VMProviderType) else self.provider_type
|
216
|
+
self.logger.verbose(f"Initializing {provider_type_name} provider context...")
|
217
|
+
|
218
|
+
# Explicitly set provider parameters
|
219
|
+
storage = "ephemeral" if self.ephemeral else self.storage
|
220
|
+
verbose = self.verbosity >= LogLevel.DEBUG
|
221
|
+
ephemeral = self.ephemeral
|
222
|
+
port = self.port if self.port is not None else 7777
|
223
|
+
host = self.host if self.host else "localhost"
|
224
|
+
image = self.image
|
225
|
+
shared_path = self.shared_path
|
226
|
+
noVNC_port = self.noVNC_port
|
227
|
+
|
228
|
+
# Create VM provider instance with explicit parameters
|
229
|
+
try:
|
230
|
+
if self.provider_type == VMProviderType.LUMIER:
|
231
|
+
self.logger.info(f"Using VM image for Lumier provider: {image}")
|
232
|
+
if shared_path:
|
233
|
+
self.logger.info(f"Using shared path for Lumier provider: {shared_path}")
|
234
|
+
if noVNC_port:
|
235
|
+
self.logger.info(f"Using noVNC port for Lumier provider: {noVNC_port}")
|
236
|
+
self.config.vm_provider = VMProviderFactory.create_provider(
|
237
|
+
self.provider_type,
|
238
|
+
port=port,
|
239
|
+
host=host,
|
240
|
+
storage=storage,
|
241
|
+
shared_path=shared_path,
|
242
|
+
image=image,
|
243
|
+
verbose=verbose,
|
244
|
+
ephemeral=ephemeral,
|
245
|
+
noVNC_port=noVNC_port,
|
246
|
+
)
|
247
|
+
elif self.provider_type == VMProviderType.LUME:
|
248
|
+
self.config.vm_provider = VMProviderFactory.create_provider(
|
249
|
+
self.provider_type,
|
250
|
+
port=port,
|
251
|
+
host=host,
|
252
|
+
storage=storage,
|
253
|
+
verbose=verbose,
|
254
|
+
ephemeral=ephemeral,
|
255
|
+
)
|
256
|
+
elif self.provider_type == VMProviderType.CLOUD:
|
257
|
+
self.config.vm_provider = VMProviderFactory.create_provider(
|
258
|
+
self.provider_type,
|
259
|
+
port=port,
|
260
|
+
host=host,
|
261
|
+
storage=storage,
|
262
|
+
verbose=verbose,
|
263
|
+
)
|
264
|
+
else:
|
265
|
+
raise ValueError(f"Unsupported provider type: {self.provider_type}")
|
266
|
+
self._provider_context = await self.config.vm_provider.__aenter__()
|
267
|
+
self.logger.verbose("VM provider context initialized successfully")
|
268
|
+
except ImportError as ie:
|
269
|
+
self.logger.error(f"Failed to import provider dependencies: {ie}")
|
270
|
+
if str(ie).find("lume") >= 0 and str(ie).find("lumier") < 0:
|
271
|
+
self.logger.error("Please install with: pip install cua-computer[lume]")
|
272
|
+
elif str(ie).find("lumier") >= 0 or str(ie).find("docker") >= 0:
|
273
|
+
self.logger.error("Please install with: pip install cua-computer[lumier] and make sure Docker is installed")
|
274
|
+
elif str(ie).find("cloud") >= 0:
|
275
|
+
self.logger.error("Please install with: pip install cua-computer[cloud]")
|
276
|
+
raise
|
227
277
|
except Exception as e:
|
228
|
-
self.logger.error(f"Failed to initialize
|
229
|
-
raise RuntimeError(f"Failed to initialize
|
278
|
+
self.logger.error(f"Failed to initialize provider context: {e}")
|
279
|
+
raise RuntimeError(f"Failed to initialize VM provider: {e}")
|
230
280
|
|
231
|
-
#
|
281
|
+
# Check if VM exists or create it
|
232
282
|
try:
|
233
|
-
|
283
|
+
if self.config.vm_provider is None:
|
284
|
+
raise RuntimeError(f"VM provider not initialized for {self.config.name}")
|
285
|
+
|
286
|
+
vm = await self.config.vm_provider.get_vm(self.config.name)
|
234
287
|
self.logger.verbose(f"Found existing VM: {self.config.name}")
|
235
288
|
except Exception as e:
|
236
289
|
self.logger.error(f"VM not found: {self.config.name}")
|
237
|
-
self.logger.error(
|
238
|
-
f"Please pull the VM first with lume pull macos-sequoia-cua-sparse:latest: {e}"
|
239
|
-
)
|
290
|
+
self.logger.error(f"Error: {e}")
|
240
291
|
raise RuntimeError(
|
241
|
-
f"VM
|
292
|
+
f"VM {self.config.name} could not be found or created."
|
242
293
|
)
|
243
294
|
|
244
|
-
# Convert paths to
|
245
|
-
|
246
|
-
for path in self.
|
295
|
+
# Convert paths to dictionary format for shared directories
|
296
|
+
shared_dirs = []
|
297
|
+
for path in self.shared_directories:
|
247
298
|
self.logger.verbose(f"Adding shared directory: {path}")
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
299
|
+
path = os.path.abspath(os.path.expanduser(path))
|
300
|
+
if os.path.exists(path):
|
301
|
+
# Add path in format expected by Lume API
|
302
|
+
shared_dirs.append({
|
303
|
+
"hostPath": path,
|
304
|
+
"readOnly": False
|
305
|
+
})
|
306
|
+
else:
|
307
|
+
self.logger.warning(f"Shared directory does not exist: {path}")
|
308
|
+
|
309
|
+
# Prepare run options to pass to the provider
|
310
|
+
run_opts = {}
|
311
|
+
|
312
|
+
# Add display information if available
|
313
|
+
if self.config.display is not None:
|
314
|
+
display_info = {
|
315
|
+
"width": self.config.display.width,
|
316
|
+
"height": self.config.display.height,
|
317
|
+
}
|
318
|
+
|
319
|
+
# Check if scale_factor exists before adding it
|
320
|
+
if hasattr(self.config.display, "scale_factor"):
|
321
|
+
display_info["scale_factor"] = self.config.display.scale_factor
|
322
|
+
|
323
|
+
run_opts["display"] = display_info
|
324
|
+
|
325
|
+
# Add shared directories if available
|
326
|
+
if self.shared_directories:
|
327
|
+
run_opts["shared_directories"] = shared_dirs.copy()
|
328
|
+
|
329
|
+
# Run the VM with the provider
|
268
330
|
try:
|
269
|
-
|
331
|
+
if self.config.vm_provider is None:
|
332
|
+
raise RuntimeError(f"VM provider not initialized for {self.config.name}")
|
333
|
+
|
334
|
+
# Use the complete run_opts we prepared earlier
|
335
|
+
# Handle ephemeral storage for run_vm method too
|
336
|
+
storage_param = "ephemeral" if self.ephemeral else self.storage
|
337
|
+
|
338
|
+
# Log the image being used
|
339
|
+
self.logger.info(f"Running VM using image: {self.image}")
|
340
|
+
|
341
|
+
# Call provider.run_vm with explicit image parameter
|
342
|
+
response = await self.config.vm_provider.run_vm(
|
343
|
+
image=self.image,
|
344
|
+
name=self.config.name,
|
345
|
+
run_opts=run_opts,
|
346
|
+
storage=storage_param
|
347
|
+
)
|
270
348
|
self.logger.info(f"VM run response: {response if response else 'None'}")
|
271
349
|
except Exception as run_error:
|
272
350
|
self.logger.error(f"Failed to run VM: {run_error}")
|
273
351
|
raise RuntimeError(f"Failed to start VM: {run_error}")
|
274
352
|
|
275
|
-
# Wait for VM to be ready with
|
276
|
-
self.logger.info("Waiting for VM to be ready...")
|
353
|
+
# Wait for VM to be ready with a valid IP address
|
354
|
+
self.logger.info("Waiting for VM to be ready with a valid IP address...")
|
277
355
|
try:
|
278
|
-
|
279
|
-
if
|
280
|
-
|
281
|
-
|
282
|
-
|
356
|
+
# Increased values for Lumier provider which needs more time for initial setup
|
357
|
+
if self.provider_type == VMProviderType.LUMIER:
|
358
|
+
max_retries = 60 # Increased for Lumier VM startup which takes longer
|
359
|
+
retry_delay = 3 # 3 seconds between retries for Lumier
|
360
|
+
else:
|
361
|
+
max_retries = 30 # Default for other providers
|
362
|
+
retry_delay = 2 # 2 seconds between retries
|
363
|
+
|
364
|
+
self.logger.info(f"Waiting up to {max_retries * retry_delay} seconds for VM to be ready...")
|
365
|
+
ip = await self.get_ip(max_retries=max_retries, retry_delay=retry_delay)
|
366
|
+
|
367
|
+
# If we get here, we have a valid IP
|
368
|
+
self.logger.info(f"VM is ready with IP: {ip}")
|
369
|
+
ip_address = ip
|
370
|
+
except TimeoutError as timeout_error:
|
371
|
+
self.logger.error(str(timeout_error))
|
372
|
+
raise RuntimeError(f"VM startup timed out: {timeout_error}")
|
283
373
|
except Exception as wait_error:
|
284
374
|
self.logger.error(f"Error waiting for VM: {wait_error}")
|
285
375
|
raise RuntimeError(f"VM failed to become ready: {wait_error}")
|
@@ -288,6 +378,10 @@ class Computer:
|
|
288
378
|
raise RuntimeError(f"Failed to initialize computer: {e}")
|
289
379
|
|
290
380
|
try:
|
381
|
+
# Verify we have a valid IP before initializing the interface
|
382
|
+
if not ip_address or ip_address == "unknown" or ip_address == "0.0.0.0":
|
383
|
+
raise RuntimeError(f"Cannot initialize interface - invalid IP address: {ip_address}")
|
384
|
+
|
291
385
|
# Initialize the interface using the factory with the specified OS
|
292
386
|
self.logger.info(f"Initializing interface for {self.os_type} at {ip_address}")
|
293
387
|
from .interface.base import BaseComputerInterface
|
@@ -304,10 +398,11 @@ class Computer:
|
|
304
398
|
|
305
399
|
try:
|
306
400
|
# Use a single timeout for the entire connection process
|
307
|
-
|
401
|
+
# The VM should already be ready at this point, so we're just establishing the connection
|
402
|
+
await self._interface.wait_for_ready(timeout=30)
|
308
403
|
self.logger.info("WebSocket interface connected successfully")
|
309
404
|
except TimeoutError as e:
|
310
|
-
self.logger.error("Failed to connect to WebSocket interface")
|
405
|
+
self.logger.error(f"Failed to connect to WebSocket interface at {ip_address}")
|
311
406
|
raise TimeoutError(
|
312
407
|
f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}"
|
313
408
|
)
|
@@ -335,41 +430,26 @@ class Computer:
|
|
335
430
|
start_time = time.time()
|
336
431
|
|
337
432
|
try:
|
338
|
-
|
339
|
-
self._running = False
|
340
|
-
self.logger.info("Stopping Computer...")
|
341
|
-
|
342
|
-
if hasattr(self, "_stop_event"):
|
343
|
-
self._stop_event.set()
|
344
|
-
if hasattr(self, "_keep_alive_task"):
|
345
|
-
await self._keep_alive_task
|
346
|
-
|
347
|
-
if self._interface: # Only try to close interface if it exists
|
348
|
-
self.logger.verbose("Closing interface...")
|
349
|
-
# For host computer server, just use normal close to keep the server running
|
350
|
-
if self.use_host_computer_server:
|
351
|
-
self._interface.close()
|
352
|
-
else:
|
353
|
-
# For VM mode, force close the connection
|
354
|
-
if hasattr(self._interface, "force_close"):
|
355
|
-
self._interface.force_close()
|
356
|
-
else:
|
357
|
-
self._interface.close()
|
433
|
+
self.logger.info("Stopping Computer...")
|
358
434
|
|
359
|
-
|
435
|
+
# In VM mode, first explicitly stop the VM, then exit the provider context
|
436
|
+
if not self.use_host_computer_server and self._provider_context and self.config.vm_provider is not None:
|
360
437
|
try:
|
361
438
|
self.logger.info(f"Stopping VM {self.config.name}...")
|
362
|
-
await self.config.
|
439
|
+
await self.config.vm_provider.stop_vm(
|
440
|
+
name=self.config.name,
|
441
|
+
storage=self.storage # Pass storage explicitly for clarity
|
442
|
+
)
|
363
443
|
except Exception as e:
|
364
|
-
self.logger.
|
365
|
-
|
366
|
-
|
367
|
-
self.
|
444
|
+
self.logger.error(f"Error stopping VM: {e}")
|
445
|
+
|
446
|
+
self.logger.verbose("Closing VM provider context...")
|
447
|
+
await self.config.vm_provider.__aexit__(None, None, None)
|
448
|
+
self._provider_context = None
|
449
|
+
|
368
450
|
self.logger.info("Computer stopped")
|
369
451
|
except Exception as e:
|
370
|
-
self.logger.debug(
|
371
|
-
f"Error during cleanup: {e}"
|
372
|
-
) # Log as debug since this might be expected
|
452
|
+
self.logger.debug(f"Error during cleanup: {e}") # Log as debug since this might be expected
|
373
453
|
finally:
|
374
454
|
# Log stop time for performance monitoring
|
375
455
|
duration_ms = (time.time() - start_time) * 1000
|
@@ -377,14 +457,44 @@ class Computer:
|
|
377
457
|
return
|
378
458
|
|
379
459
|
# @property
|
380
|
-
async def get_ip(self) -> str:
|
381
|
-
"""Get the IP address of the VM or localhost if using host computer server.
|
460
|
+
async def get_ip(self, max_retries: int = 15, retry_delay: int = 2) -> str:
|
461
|
+
"""Get the IP address of the VM or localhost if using host computer server.
|
462
|
+
|
463
|
+
This method delegates to the provider's get_ip method, which waits indefinitely
|
464
|
+
until the VM has a valid IP address.
|
465
|
+
|
466
|
+
Args:
|
467
|
+
max_retries: Unused parameter, kept for backward compatibility
|
468
|
+
retry_delay: Delay between retries in seconds (default: 2)
|
469
|
+
|
470
|
+
Returns:
|
471
|
+
IP address of the VM or localhost if using host computer server
|
472
|
+
"""
|
473
|
+
# For host computer server, always return localhost immediately
|
382
474
|
if self.use_host_computer_server:
|
383
475
|
return "127.0.0.1"
|
384
|
-
|
385
|
-
|
476
|
+
|
477
|
+
# Get IP from the provider - each provider implements its own waiting logic
|
478
|
+
if self.config.vm_provider is None:
|
479
|
+
raise RuntimeError("VM provider is not initialized")
|
480
|
+
|
481
|
+
# Log that we're waiting for the IP
|
482
|
+
self.logger.info(f"Waiting for VM {self.config.name} to get an IP address...")
|
483
|
+
|
484
|
+
# Call the provider's get_ip method which will wait indefinitely
|
485
|
+
storage_param = "ephemeral" if self.ephemeral else self.storage
|
486
|
+
ip = await self.config.vm_provider.get_ip(
|
487
|
+
name=self.config.name,
|
488
|
+
storage=storage_param,
|
489
|
+
retry_delay=retry_delay
|
490
|
+
)
|
491
|
+
|
492
|
+
# Log success
|
493
|
+
self.logger.info(f"VM {self.config.name} has IP address: {ip}")
|
494
|
+
return ip
|
495
|
+
|
386
496
|
|
387
|
-
async def wait_vm_ready(self) -> Optional[
|
497
|
+
async def wait_vm_ready(self) -> Optional[Dict[str, Any]]:
|
388
498
|
"""Wait for VM to be ready with an IP address.
|
389
499
|
|
390
500
|
Returns:
|
@@ -407,7 +517,11 @@ class Computer:
|
|
407
517
|
|
408
518
|
try:
|
409
519
|
# Keep polling for VM info
|
410
|
-
|
520
|
+
if self.config.vm_provider is None:
|
521
|
+
self.logger.error("VM provider is not initialized")
|
522
|
+
vm = None
|
523
|
+
else:
|
524
|
+
vm = await self.config.vm_provider.get_vm(self.config.name)
|
411
525
|
|
412
526
|
# Log full VM properties for debugging (every 30 attempts)
|
413
527
|
if attempts % 30 == 0:
|
@@ -447,10 +561,11 @@ class Computer:
|
|
447
561
|
self.logger.error(f"Persistent error getting VM status: {str(e)}")
|
448
562
|
self.logger.info("Trying to get VM list for debugging...")
|
449
563
|
try:
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
564
|
+
if self.config.vm_provider is not None:
|
565
|
+
vms = await self.config.vm_provider.list_vms()
|
566
|
+
self.logger.info(
|
567
|
+
f"Available VMs: {[getattr(vm, 'name', None) for vm in vms if hasattr(vm, 'name')]}"
|
568
|
+
)
|
454
569
|
except Exception as list_error:
|
455
570
|
self.logger.error(f"Failed to list VMs: {str(list_error)}")
|
456
571
|
|
@@ -462,9 +577,14 @@ class Computer:
|
|
462
577
|
|
463
578
|
# Try to get final VM status for debugging
|
464
579
|
try:
|
465
|
-
|
466
|
-
|
467
|
-
|
580
|
+
if self.config.vm_provider is not None:
|
581
|
+
vm = await self.config.vm_provider.get_vm(self.config.name)
|
582
|
+
# VM data is returned as a dictionary from the Lumier provider
|
583
|
+
status = vm.get('status', 'unknown') if vm else "unknown"
|
584
|
+
ip = vm.get('ip_address') if vm else None
|
585
|
+
else:
|
586
|
+
status = "unknown"
|
587
|
+
ip = None
|
468
588
|
self.logger.error(f"Final VM status: {status}, IP: {ip}")
|
469
589
|
except Exception as e:
|
470
590
|
self.logger.error(f"Failed to get final VM status: {str(e)}")
|
@@ -478,10 +598,18 @@ class Computer:
|
|
478
598
|
self.logger.info(
|
479
599
|
f"Updating VM settings: CPU={cpu or self.config.cpu}, Memory={memory or self.config.memory}"
|
480
600
|
)
|
481
|
-
update_opts =
|
482
|
-
cpu
|
483
|
-
|
484
|
-
|
601
|
+
update_opts = {
|
602
|
+
"cpu": cpu or int(self.config.cpu),
|
603
|
+
"memory": memory or self.config.memory
|
604
|
+
}
|
605
|
+
if self.config.vm_provider is not None:
|
606
|
+
await self.config.vm_provider.update_vm(
|
607
|
+
name=self.config.name,
|
608
|
+
update_opts=update_opts,
|
609
|
+
storage=self.storage # Pass storage explicitly for clarity
|
610
|
+
)
|
611
|
+
else:
|
612
|
+
raise RuntimeError("VM provider not initialized")
|
485
613
|
|
486
614
|
def get_screenshot_size(self, screenshot: bytes) -> Dict[str, int]:
|
487
615
|
"""Get the dimensions of a screenshot.
|
@@ -25,8 +25,11 @@ class InterfaceFactory:
|
|
25
25
|
"""
|
26
26
|
# Import implementations here to avoid circular imports
|
27
27
|
from .macos import MacOSComputerInterface
|
28
|
+
from .linux import LinuxComputerInterface
|
28
29
|
|
29
30
|
if os == 'macos':
|
30
31
|
return MacOSComputerInterface(ip_address)
|
32
|
+
elif os == 'linux':
|
33
|
+
return LinuxComputerInterface(ip_address)
|
31
34
|
else:
|
32
|
-
raise ValueError(f"Unsupported OS type: {os}")
|
35
|
+
raise ValueError(f"Unsupported OS type: {os}")
|