lybic-guiagents 0.1.0__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.

Potentially problematic release.


This version of lybic-guiagents might be problematic. Click here for more details.

Files changed (85) hide show
  1. desktop_env/__init__.py +1 -0
  2. desktop_env/actions.py +203 -0
  3. desktop_env/controllers/__init__.py +0 -0
  4. desktop_env/controllers/python.py +471 -0
  5. desktop_env/controllers/setup.py +882 -0
  6. desktop_env/desktop_env.py +509 -0
  7. desktop_env/evaluators/__init__.py +5 -0
  8. desktop_env/evaluators/getters/__init__.py +41 -0
  9. desktop_env/evaluators/getters/calc.py +15 -0
  10. desktop_env/evaluators/getters/chrome.py +1774 -0
  11. desktop_env/evaluators/getters/file.py +154 -0
  12. desktop_env/evaluators/getters/general.py +42 -0
  13. desktop_env/evaluators/getters/gimp.py +38 -0
  14. desktop_env/evaluators/getters/impress.py +126 -0
  15. desktop_env/evaluators/getters/info.py +24 -0
  16. desktop_env/evaluators/getters/misc.py +406 -0
  17. desktop_env/evaluators/getters/replay.py +20 -0
  18. desktop_env/evaluators/getters/vlc.py +86 -0
  19. desktop_env/evaluators/getters/vscode.py +35 -0
  20. desktop_env/evaluators/metrics/__init__.py +160 -0
  21. desktop_env/evaluators/metrics/basic_os.py +68 -0
  22. desktop_env/evaluators/metrics/chrome.py +493 -0
  23. desktop_env/evaluators/metrics/docs.py +1011 -0
  24. desktop_env/evaluators/metrics/general.py +665 -0
  25. desktop_env/evaluators/metrics/gimp.py +637 -0
  26. desktop_env/evaluators/metrics/libreoffice.py +28 -0
  27. desktop_env/evaluators/metrics/others.py +92 -0
  28. desktop_env/evaluators/metrics/pdf.py +31 -0
  29. desktop_env/evaluators/metrics/slides.py +957 -0
  30. desktop_env/evaluators/metrics/table.py +585 -0
  31. desktop_env/evaluators/metrics/thunderbird.py +176 -0
  32. desktop_env/evaluators/metrics/utils.py +719 -0
  33. desktop_env/evaluators/metrics/vlc.py +524 -0
  34. desktop_env/evaluators/metrics/vscode.py +283 -0
  35. desktop_env/providers/__init__.py +35 -0
  36. desktop_env/providers/aws/__init__.py +0 -0
  37. desktop_env/providers/aws/manager.py +278 -0
  38. desktop_env/providers/aws/provider.py +186 -0
  39. desktop_env/providers/aws/provider_with_proxy.py +315 -0
  40. desktop_env/providers/aws/proxy_pool.py +193 -0
  41. desktop_env/providers/azure/__init__.py +0 -0
  42. desktop_env/providers/azure/manager.py +87 -0
  43. desktop_env/providers/azure/provider.py +207 -0
  44. desktop_env/providers/base.py +97 -0
  45. desktop_env/providers/gcp/__init__.py +0 -0
  46. desktop_env/providers/gcp/manager.py +0 -0
  47. desktop_env/providers/gcp/provider.py +0 -0
  48. desktop_env/providers/virtualbox/__init__.py +0 -0
  49. desktop_env/providers/virtualbox/manager.py +463 -0
  50. desktop_env/providers/virtualbox/provider.py +124 -0
  51. desktop_env/providers/vmware/__init__.py +0 -0
  52. desktop_env/providers/vmware/manager.py +455 -0
  53. desktop_env/providers/vmware/provider.py +105 -0
  54. gui_agents/__init__.py +0 -0
  55. gui_agents/agents/Action.py +209 -0
  56. gui_agents/agents/__init__.py +0 -0
  57. gui_agents/agents/agent_s.py +832 -0
  58. gui_agents/agents/global_state.py +610 -0
  59. gui_agents/agents/grounding.py +651 -0
  60. gui_agents/agents/hardware_interface.py +129 -0
  61. gui_agents/agents/manager.py +568 -0
  62. gui_agents/agents/translator.py +132 -0
  63. gui_agents/agents/worker.py +355 -0
  64. gui_agents/cli_app.py +560 -0
  65. gui_agents/core/__init__.py +0 -0
  66. gui_agents/core/engine.py +1496 -0
  67. gui_agents/core/knowledge.py +449 -0
  68. gui_agents/core/mllm.py +555 -0
  69. gui_agents/tools/__init__.py +0 -0
  70. gui_agents/tools/tools.py +727 -0
  71. gui_agents/unit_test/__init__.py +0 -0
  72. gui_agents/unit_test/run_tests.py +65 -0
  73. gui_agents/unit_test/test_manager.py +330 -0
  74. gui_agents/unit_test/test_worker.py +269 -0
  75. gui_agents/utils/__init__.py +0 -0
  76. gui_agents/utils/analyze_display.py +301 -0
  77. gui_agents/utils/common_utils.py +263 -0
  78. gui_agents/utils/display_viewer.py +281 -0
  79. gui_agents/utils/embedding_manager.py +53 -0
  80. gui_agents/utils/image_axis_utils.py +27 -0
  81. lybic_guiagents-0.1.0.dist-info/METADATA +416 -0
  82. lybic_guiagents-0.1.0.dist-info/RECORD +85 -0
  83. lybic_guiagents-0.1.0.dist-info/WHEEL +5 -0
  84. lybic_guiagents-0.1.0.dist-info/licenses/LICENSE +201 -0
  85. lybic_guiagents-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,463 @@
1
+ import logging
2
+ import os
3
+ import platform
4
+ import shutil
5
+ import subprocess
6
+ import threading
7
+ import time
8
+ import zipfile
9
+
10
+ import psutil
11
+ import requests
12
+ from filelock import FileLock
13
+ from tqdm import tqdm
14
+
15
+ from desktop_env.providers.base import VMManager
16
+
17
+ logger = logging.getLogger("desktopenv.providers.virtualbox.VirtualBoxVMManager")
18
+ logger.setLevel(logging.INFO)
19
+
20
+ MAX_RETRY_TIMES = 10
21
+ RETRY_INTERVAL = 5
22
+ UBUNTU_ARM_URL = "NOT_AVAILABLE"
23
+ UBUNTU_X86_URL = "https://huggingface.co/datasets/xlangai/ubuntu_x86_virtualbox/resolve/main/Ubuntu.zip"
24
+ DOWNLOADED_FILE_NAME = "Ubuntu.zip"
25
+ REGISTRY_PATH = '.virtualbox_vms'
26
+
27
+ LOCK_FILE_NAME = '.virtualbox_lck'
28
+ VMS_DIR = "./virtualbox_vm_data"
29
+ update_lock = threading.Lock()
30
+
31
+ if platform.system() == 'Windows':
32
+ vboxmanage_path = r"C:\Program Files\Oracle\VirtualBox"
33
+ os.environ["PATH"] += os.pathsep + vboxmanage_path
34
+
35
+
36
+ def generate_new_vm_name(vms_dir, os_type):
37
+ registry_idx = 0
38
+ while True:
39
+ attempted_new_name = f"{os_type}{registry_idx}"
40
+ if os.path.exists(
41
+ os.path.join(vms_dir, attempted_new_name, attempted_new_name, attempted_new_name + ".vbox")):
42
+ registry_idx += 1
43
+ else:
44
+ return attempted_new_name
45
+
46
+
47
+ def _install_vm(vm_name, vms_dir, downloaded_file_name, original_vm_name="Ubuntu", bridged_adapter_name=None):
48
+ os.makedirs(vms_dir, exist_ok=True)
49
+
50
+ def __download_and_unzip_vm():
51
+ # Determine the platform and CPU architecture to decide the correct VM image to download
52
+ if platform.system() == 'Darwin': # macOS
53
+ url = UBUNTU_ARM_URL
54
+ raise Exception("MacOS host is not currently supported for VirtualBox.")
55
+ elif platform.machine().lower() in ['amd64', 'x86_64']:
56
+ url = UBUNTU_X86_URL
57
+ else:
58
+ raise Exception("Unsupported platform or architecture.")
59
+
60
+ # Download the virtual machine image
61
+ logger.info("Downloading the virtual machine image...")
62
+ downloaded_size = 0
63
+
64
+ while True:
65
+ downloaded_file_path = os.path.join(vms_dir, downloaded_file_name)
66
+ headers = {}
67
+ if os.path.exists(downloaded_file_path):
68
+ downloaded_size = os.path.getsize(downloaded_file_path)
69
+ headers["Range"] = f"bytes={downloaded_size}-"
70
+
71
+ with requests.get(url, headers=headers, stream=True) as response:
72
+ if response.status_code == 416:
73
+ # This means the range was not satisfiable, possibly the file was fully downloaded
74
+ logger.info("Fully downloaded or the file size changed.")
75
+ break
76
+
77
+ response.raise_for_status()
78
+ total_size = int(response.headers.get('content-length', 0))
79
+
80
+ with open(downloaded_file_path, "ab") as file, tqdm(
81
+ desc="Progress",
82
+ total=total_size,
83
+ unit='iB',
84
+ unit_scale=True,
85
+ unit_divisor=1024,
86
+ initial=downloaded_size,
87
+ ascii=True
88
+ ) as progress_bar:
89
+ try:
90
+ for data in response.iter_content(chunk_size=1024):
91
+ size = file.write(data)
92
+ progress_bar.update(size)
93
+ except (requests.exceptions.RequestException, IOError) as e:
94
+ logger.error(f"Download error: {e}")
95
+ time.sleep(RETRY_INTERVAL)
96
+ logger.error("Retrying...")
97
+ else:
98
+ logger.info("Download succeeds.")
99
+ break # Download completed successfully
100
+
101
+ # Unzip the downloaded file
102
+ logger.info("Unzipping the downloaded file...☕️")
103
+ with zipfile.ZipFile(downloaded_file_path, 'r') as zip_ref:
104
+ zip_ref.extractall(vms_dir)
105
+ logger.info("Files have been successfully extracted to the directory: " + vms_dir)
106
+
107
+ def import_vm(vms_dir, target_vm_name, max_retries=1):
108
+ """Import the .ovf file into VirtualBox."""
109
+ logger.info(f"Starting to import VM {target_vm_name}...")
110
+ command = (
111
+ f"VBoxManage import {os.path.abspath(os.path.join(vms_dir, original_vm_name, original_vm_name + '.ovf'))} "
112
+ f"--vsys 0 "
113
+ f"--vmname {target_vm_name} "
114
+ f"--settingsfile {os.path.abspath(os.path.join(vms_dir, target_vm_name, target_vm_name + '.vbox'))} "
115
+ f"--basefolder {vms_dir} "
116
+ f"--unit 14 "
117
+ f"--disk {os.path.abspath(os.path.join(vms_dir, target_vm_name, target_vm_name + '_disk1.vmdk'))}")
118
+
119
+ for attempt in range(max_retries):
120
+ result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8",
121
+ errors='ignore')
122
+ if result.returncode == 0:
123
+ logger.info("Successfully imported VM.")
124
+ return True
125
+ else:
126
+ if not result.stderr or "Error" in result.stderr:
127
+ logger.error(f"Attempt {attempt + 1} failed with specific error: {result.stderr}")
128
+ else:
129
+ logger.error(f"Attempt {attempt + 1} failed: {result.stderr}")
130
+
131
+ if attempt == max_retries - 1:
132
+ logger.error("Maximum retry attempts reached, failed to import the virtual machine.")
133
+ return False
134
+
135
+ def configure_vm_network(vm_name, interface_name=None):
136
+ # Config of bridged network
137
+ command = f'VBoxManage modifyvm "{vm_name}" --nic1 bridged'
138
+ result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8",
139
+ errors='ignore')
140
+ if not interface_name:
141
+ output = subprocess.check_output(f"VBoxManage list bridgedifs", shell=True, stderr=subprocess.STDOUT)
142
+ output = output.decode()
143
+ output = output.splitlines()
144
+ result = []
145
+ for line in output:
146
+ entries = line.split()
147
+ if entries and entries[0] == "Name:":
148
+ name = ' '.join(entries[1:])
149
+ if entries and entries[0] == "IPAddress:":
150
+ ip = entries[1]
151
+ result.append((name, ip))
152
+ logger.info("Found the following network adapters, default to the first. If you want to change it, please set the argument -r to the name of the adapter.")
153
+ for i, (name, ip) in enumerate(result):
154
+ logger.info(f"{i+1}: {name} ({ip})")
155
+ interface_id = 1
156
+ interface_name = result[interface_id-1][0]
157
+ command = f'vboxmanage modifyvm "{vm_name}" --bridgeadapter1 "{interface_name}"'
158
+ result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8",
159
+ errors='ignore')
160
+ if result.returncode == 0:
161
+ logger.info(f"Changed to bridge adapter {interface_name}.")
162
+ return True
163
+ else:
164
+ logger.error(f"Failed to change to bridge adapter {interface_name}: {result.stderr}")
165
+ return False
166
+
167
+ # # Config of NAT network
168
+ # command = f"VBoxManage natnetwork add --netname natnet --network {nat_network} --dhcp on"
169
+ # result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8",
170
+ # errors='ignore')
171
+ # if result.returncode == 0:
172
+ # logger.info(f"Created NAT network {nat_network}.")
173
+ # else:
174
+ # logger.error(f"Failed to create NAT network {nat_network}")
175
+ # return False
176
+ # command = f"VBoxManage modifyvm {vm_name} --nic1 natnetwork"
177
+ # result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8",
178
+ # errors='ignore')
179
+ # command = f"VBoxManage modifyvm {vm_name} --natnet1 natnet"
180
+ # result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8",
181
+ # errors='ignore')
182
+ # if result.returncode == 0:
183
+ # logger.info("Switched VM to the NAT network.")
184
+ # else:
185
+ # logger.error("Failed to switch VM to the NAT network")
186
+ # return False
187
+ # logger.info("Start to configure port forwarding...")
188
+ # command = f"VBoxManage modifyvm {vm_name} --natpf1 'server,tcp,,5000,,5000'"
189
+ # result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8",
190
+ # errors='ignore')
191
+ # if result.returncode == 0:
192
+ # logger.info("Successfully created port forwarding rule.")
193
+ # return True
194
+ # logger.error("Failed to create port forwarding rule.")
195
+ # return False
196
+
197
+
198
+ vm_path = os.path.join(vms_dir, vm_name, vm_name + ".vbox")
199
+
200
+ # Execute the function to download and unzip the VM, and update the vm metadata
201
+ if not os.path.exists(vm_path):
202
+ __download_and_unzip_vm()
203
+ import_vm(vms_dir, vm_name)
204
+ if not configure_vm_network(vm_name, bridged_adapter_name):
205
+ raise Exception("Failed to configure VM network!")
206
+ else:
207
+ logger.info(f"Virtual machine exists: {vm_path}")
208
+
209
+ # Start the virtual machine
210
+ def start_vm(vm_name, max_retries=20):
211
+ command = f'VBoxManage startvm "{vm_name}" --type headless'
212
+
213
+ for attempt in range(max_retries):
214
+ result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8")
215
+ if result.returncode == 0:
216
+ logger.info("Virtual machine started.")
217
+ return True
218
+ else:
219
+ if not result.stderr or "Error" in result.stderr:
220
+ logger.error(f"Attempt {attempt + 1} failed with specific error: {result.stderr}")
221
+ else:
222
+ logger.error(f"Attempt {attempt + 1} failed: {result.stderr}")
223
+
224
+ if attempt == max_retries - 1:
225
+ logger.error("Maximum retry attempts reached, failed to start the virtual machine.")
226
+ return False
227
+
228
+ if not start_vm(vm_name):
229
+ raise ValueError("Error encountered during installation, please rerun the code for retrying.")
230
+
231
+ def get_vm_ip(vm_name):
232
+ command = f'VBoxManage guestproperty get "{vm_name}" /VirtualBox/GuestInfo/Net/0/V4/IP'
233
+ result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8")
234
+ if result.returncode == 0:
235
+ return result.stdout.strip().split()[1]
236
+ else:
237
+ logger.error(f"Get VM IP failed: {result.stderr}")
238
+ return None
239
+
240
+ def change_resolution(vm_name, resolution=(1920, 1080, 32)):
241
+ command = f'VBoxManage controlvm "{vm_name}" setvideomodehint {" ".join(map(str, resolution))}'
242
+ result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8")
243
+ if result.returncode == 0:
244
+ return True
245
+ else:
246
+ return False
247
+
248
+ # Function used to check whether the virtual machine is ready
249
+ def download_screenshot(vm_name):
250
+ ip = get_vm_ip(vm_name)
251
+ url = f"http://{ip}:5000/screenshot"
252
+ try:
253
+ # max trey times 1, max timeout 1
254
+ response = requests.get(url, timeout=(10, 10))
255
+ if response.status_code == 200:
256
+ return True
257
+ except Exception as e:
258
+ logger.error(f"Error: {e}")
259
+ logger.error(f"Type: {type(e).__name__}")
260
+ logger.error(f"Error detail: {str(e)}")
261
+ return False
262
+
263
+ # Try downloading the screenshot until successful
264
+ while not download_screenshot(vm_name):
265
+ logger.info("Check whether the virtual machine is ready...")
266
+ time.sleep(RETRY_INTERVAL)
267
+
268
+ if not change_resolution(vm_name):
269
+ logger.error(f"Change resolution failed.")
270
+ raise
271
+
272
+ logger.info("Virtual machine is ready. Start to make a snapshot on the virtual machine. It would take a while...")
273
+
274
+ def create_vm_snapshot(vm_name, max_retries=20):
275
+ logger.info("Saving VirtualBox VM state...")
276
+ command = f'VBoxManage snapshot "{vm_name}" take init_state'
277
+
278
+ for attempt in range(max_retries):
279
+ result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8")
280
+ if result.returncode == 0:
281
+ logger.info("Snapshot created.")
282
+ return True
283
+ else:
284
+ if "Error" in result.stderr:
285
+ logger.error(f"Attempt {attempt + 1} failed with specific error: {result.stderr}")
286
+ else:
287
+ logger.error(f"Attempt {attempt + 1} failed: {result.stderr}")
288
+
289
+ if attempt == max_retries - 1:
290
+ logger.error("Maximum retry attempts reached, failed to create snapshot.")
291
+ return False
292
+
293
+ # Create a snapshot of the virtual machine
294
+ if create_vm_snapshot(vm_name, max_retries=MAX_RETRY_TIMES):
295
+ return vm_path
296
+ else:
297
+ raise ValueError("Error encountered during installation, please rerun the code for retrying.")
298
+
299
+
300
+ class VirtualBoxVMManager(VMManager):
301
+ def __init__(self, registry_path=REGISTRY_PATH):
302
+ self.registry_path = registry_path
303
+ self.lock = FileLock(LOCK_FILE_NAME, timeout=60)
304
+ self.initialize_registry()
305
+
306
+ def initialize_registry(self):
307
+ with self.lock: # Locking during initialization
308
+ if not os.path.exists(self.registry_path):
309
+ with open(self.registry_path, 'w') as file:
310
+ file.write('')
311
+
312
+ def add_vm(self, vm_path, lock_needed=True):
313
+ if lock_needed:
314
+ with self.lock:
315
+ self._add_vm(vm_path)
316
+ else:
317
+ self._add_vm(vm_path)
318
+
319
+ def _add_vm(self, vm_path, region=None):
320
+ assert region in [None, 'local'], "For VirtualBox provider, the region should be neither None or 'local'."
321
+ with self.lock:
322
+ with open(self.registry_path, 'r') as file:
323
+ lines = file.readlines()
324
+ new_lines = lines + [f'{vm_path}|free\n']
325
+ with open(self.registry_path, 'w') as file:
326
+ file.writelines(new_lines)
327
+
328
+ def occupy_vm(self, vm_path, pid, lock_needed=True):
329
+ if lock_needed:
330
+ with self.lock:
331
+ self._occupy_vm(vm_path, pid)
332
+ else:
333
+ self._occupy_vm(vm_path, pid)
334
+
335
+ def _occupy_vm(self, vm_path, pid, region=None):
336
+ assert region in [None, 'local'], "For VirtualBox provider, the region should be neither None or 'local'."
337
+ with self.lock:
338
+ new_lines = []
339
+ with open(self.registry_path, 'r') as file:
340
+ lines = file.readlines()
341
+ for line in lines:
342
+ registered_vm_path, _ = line.strip().split('|')
343
+ if registered_vm_path == vm_path:
344
+ new_lines.append(f'{registered_vm_path}|{pid}\n')
345
+ else:
346
+ new_lines.append(line)
347
+ with open(self.registry_path, 'w') as file:
348
+ file.writelines(new_lines)
349
+
350
+ def delete_vm(self, vm_path, lock_needed=True):
351
+ if lock_needed:
352
+ with self.lock:
353
+ self._delete_vm(vm_path)
354
+ else:
355
+ self._delete_vm(vm_path)
356
+
357
+ def _delete_vm(self, vm_path):
358
+ raise NotImplementedError
359
+
360
+ def check_and_clean(self, vms_dir, lock_needed=True):
361
+ if lock_needed:
362
+ with self.lock:
363
+ self._check_and_clean(vms_dir)
364
+ else:
365
+ self._check_and_clean(vms_dir)
366
+
367
+ def _check_and_clean(self, vms_dir):
368
+ with self.lock: # Lock when cleaning up the registry and vms_dir
369
+ # Check and clean on the running vms, detect the released ones and mark then as 'free'
370
+ active_pids = {p.pid for p in psutil.process_iter()}
371
+ new_lines = []
372
+ vm_paths = []
373
+
374
+ with open(self.registry_path, 'r') as file:
375
+ lines = file.readlines()
376
+ for line in lines:
377
+ vm_path, pid_str = line.strip().split('|')
378
+ if not os.path.exists(vm_path):
379
+ logger.info(f"VM {vm_path} not found, releasing it.")
380
+ new_lines.append(f'{vm_path}|free\n')
381
+ continue
382
+
383
+ vm_paths.append(vm_path)
384
+ if pid_str == "free":
385
+ new_lines.append(line)
386
+ continue
387
+
388
+ if int(pid_str) in active_pids:
389
+ new_lines.append(line)
390
+ else:
391
+ new_lines.append(f'{vm_path}|free\n')
392
+ with open(self.registry_path, 'w') as file:
393
+ file.writelines(new_lines)
394
+
395
+ # Check and clean on the files inside vms_dir, delete the unregistered ones
396
+ os.makedirs(vms_dir, exist_ok=True)
397
+ vm_names = os.listdir(vms_dir)
398
+ for vm_name in vm_names:
399
+ # skip the downloaded .zip file
400
+ if vm_name == DOWNLOADED_FILE_NAME:
401
+ continue
402
+ # Skip the .DS_Store file on macOS
403
+ if vm_name == ".DS_Store":
404
+ continue
405
+
406
+ flag = True
407
+ for vm_path in vm_paths:
408
+ if vm_name + ".vbox" in vm_path:
409
+ flag = False
410
+ if flag:
411
+ shutil.rmtree(os.path.join(vms_dir, vm_name))
412
+
413
+ def list_free_vms(self, lock_needed=True):
414
+ if lock_needed:
415
+ with self.lock:
416
+ return self._list_free_vms()
417
+ else:
418
+ return self._list_free_vms()
419
+
420
+ def _list_free_vms(self):
421
+ with self.lock: # Lock when reading the registry
422
+ free_vms = []
423
+ with open(self.registry_path, 'r') as file:
424
+ lines = file.readlines()
425
+ for line in lines:
426
+ vm_path, pid_str = line.strip().split('|')
427
+ if pid_str == "free":
428
+ free_vms.append((vm_path, pid_str))
429
+ return free_vms
430
+
431
+ def get_vm_path(self, os_type, region=None, screen_size=(1920, 1080), **kwargs):
432
+ # Note: screen_size parameter is ignored for VirtualBox provider
433
+ # but kept for interface consistency with other providers
434
+ if os_type != "Ubuntu":
435
+ raise ValueError("Only support Ubuntu for now.")
436
+
437
+ with self.lock:
438
+ if not VirtualBoxVMManager.checked_and_cleaned:
439
+ VirtualBoxVMManager.checked_and_cleaned = True
440
+ self._check_and_clean(vms_dir=VMS_DIR)
441
+
442
+ allocation_needed = False
443
+ with self.lock:
444
+ free_vms_paths = self._list_free_vms()
445
+ if len(free_vms_paths) == 0:
446
+ # No free virtual machine available, generate a new one
447
+ allocation_needed = True
448
+ else:
449
+ # Choose the first free virtual machine
450
+ chosen_vm_path = free_vms_paths[0][0]
451
+ self._occupy_vm(chosen_vm_path, os.getpid())
452
+ return chosen_vm_path
453
+
454
+ if allocation_needed:
455
+ logger.info("No free virtual machine available. Generating a new one, which would take a while...☕")
456
+ new_vm_name = generate_new_vm_name(vms_dir=VMS_DIR, os_type=os_type)
457
+ new_vm_path = _install_vm(new_vm_name, vms_dir=VMS_DIR,
458
+ downloaded_file_name=DOWNLOADED_FILE_NAME,
459
+ bridged_adapter_name=region)
460
+ with self.lock:
461
+ self._add_vm(new_vm_path)
462
+ self._occupy_vm(new_vm_path, os.getpid())
463
+ return new_vm_path
@@ -0,0 +1,124 @@
1
+ import logging
2
+ import platform
3
+ import subprocess
4
+ import time
5
+ import os
6
+ from desktop_env.providers.base import Provider
7
+ import xml.etree.ElementTree as ET
8
+
9
+ logger = logging.getLogger("desktopenv.providers.virtualbox.VirtualBoxProvider")
10
+ logger.setLevel(logging.INFO)
11
+
12
+ WAIT_TIME = 3
13
+
14
+ # Note: Windows will not add command VBoxManage to PATH by default. Please add the folder where VBoxManage executable is in (Default should be "C:\Program Files\Oracle\VirtualBox" for Windows) to PATH.
15
+
16
+ class VirtualBoxProvider(Provider):
17
+ @staticmethod
18
+ def _execute_command(command: list):
19
+ result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60, text=True,
20
+ encoding="utf-8")
21
+ if result.returncode != 0:
22
+ raise Exception("\033[91m" + result.stdout + result.stderr + "\033[0m")
23
+ return result.stdout.strip()
24
+
25
+ @staticmethod
26
+ def _get_vm_uuid(path_to_vm: str):
27
+ try:
28
+ output = subprocess.check_output(f"VBoxManage list vms", shell=True, stderr=subprocess.STDOUT)
29
+ output = output.decode()
30
+ output = output.splitlines()
31
+ if path_to_vm.endswith('.vbox'):
32
+ # Load and parse the XML content from the file
33
+ tree = ET.parse(path_to_vm)
34
+ root = tree.getroot()
35
+
36
+ # Find the <Machine> element and retrieve its 'uuid' attribute
37
+ machine_element = root.find('.//{http://www.virtualbox.org/}Machine')
38
+ if machine_element is not None:
39
+ uuid = machine_element.get('uuid')[1:-1]
40
+ return uuid
41
+ else:
42
+ logger.error(f"UUID not found in file {path_to_vm}")
43
+ raise
44
+ elif any(line.split()[1] == "{" + path_to_vm + "}" for line in output):
45
+ logger.info(f"Got valid UUID {path_to_vm}.")
46
+ return path_to_vm
47
+ else:
48
+ for line in output:
49
+ if line.split()[0] == '"' + path_to_vm + '"':
50
+ uuid = line.split()[1][1:-1]
51
+ return uuid
52
+ logger.error(f"The path you provided does not match any of the \".vbox\" file, name, or UUID of VM.")
53
+ raise
54
+ except subprocess.CalledProcessError as e:
55
+ logger.error(f"Error executing command: {e.output.decode().strip()}")
56
+
57
+
58
+ def start_emulator(self, path_to_vm: str, headless: bool, os_type: str = None, *args, **kwargs):
59
+ # Note: os_type parameter is ignored for VirtualBox provider
60
+ # but kept for interface consistency with other providers
61
+ logger.info("Starting VirtualBox VM...")
62
+
63
+ while True:
64
+ try:
65
+ uuid = VirtualBoxProvider._get_vm_uuid(path_to_vm)
66
+ output = subprocess.check_output(f"VBoxManage list runningvms", shell=True, stderr=subprocess.STDOUT)
67
+ output = output.decode()
68
+ output = output.splitlines()
69
+
70
+ if any(line.split()[1] == "{" + uuid + "}" for line in output):
71
+ logger.info("VM is running.")
72
+ break
73
+ else:
74
+ logger.info("Starting VM...")
75
+ VirtualBoxProvider._execute_command(["VBoxManage", "startvm", uuid]) if not headless else \
76
+ VirtualBoxProvider._execute_command(
77
+ ["VBoxManage", "startvm", uuid, "--type", "headless"])
78
+ time.sleep(WAIT_TIME)
79
+
80
+ except subprocess.CalledProcessError as e:
81
+ logger.error(f"Error executing command: {e.output.decode().strip()}")
82
+
83
+ def get_ip_address(self, path_to_vm: str) -> str:
84
+ logger.info("Getting VirtualBox VM IP address...")
85
+ while True:
86
+ try:
87
+ uuid = VirtualBoxProvider._get_vm_uuid(path_to_vm)
88
+ output = VirtualBoxProvider._execute_command(
89
+ ["VBoxManage", "guestproperty", "get", uuid, "/VirtualBox/GuestInfo/Net/0/V4/IP"]
90
+ )
91
+ result = output.split()[1]
92
+ if result != "value":
93
+ logger.info(f"VirtualBox VM IP address: {result}")
94
+ return result
95
+ else:
96
+ logger.error("VM IP address not found. Have you installed the guest additions?")
97
+ raise
98
+ except Exception as e:
99
+ logger.error(e)
100
+ time.sleep(WAIT_TIME)
101
+ logger.info("Retrying to get VirtualBox VM IP address...")
102
+
103
+ def save_state(self, path_to_vm: str, snapshot_name: str):
104
+ logger.info("Saving VirtualBox VM state...")
105
+ uuid = VirtualBoxProvider._get_vm_uuid(path_to_vm)
106
+ VirtualBoxProvider._execute_command(["VBoxManage", "snapshot", uuid, "take", snapshot_name])
107
+ time.sleep(WAIT_TIME) # Wait for the VM to save
108
+
109
+ def revert_to_snapshot(self, path_to_vm: str, snapshot_name: str):
110
+ logger.info(f"Reverting VirtualBox VM to snapshot: {snapshot_name}...")
111
+ uuid = VirtualBoxProvider._get_vm_uuid(path_to_vm)
112
+ VirtualBoxProvider._execute_command(["VBoxManage", "controlvm", uuid, "savestate"])
113
+ time.sleep(WAIT_TIME) # Wait for the VM to stop
114
+ VirtualBoxProvider._execute_command(["VBoxManage", "snapshot", uuid, "restore", snapshot_name])
115
+ time.sleep(WAIT_TIME) # Wait for the VM to revert
116
+ return path_to_vm
117
+
118
+ def stop_emulator(self, path_to_vm: str, region=None, *args, **kwargs):
119
+ # Note: region parameter is ignored for VirtualBox provider
120
+ # but kept for interface consistency with other providers
121
+ logger.info("Stopping VirtualBox VM...")
122
+ uuid = VirtualBoxProvider._get_vm_uuid(path_to_vm)
123
+ VirtualBoxProvider._execute_command(["VBoxManage", "controlvm", uuid, "savestate"])
124
+ time.sleep(WAIT_TIME) # Wait for the VM to stop
File without changes