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.
- desktop_env/__init__.py +1 -0
- desktop_env/actions.py +203 -0
- desktop_env/controllers/__init__.py +0 -0
- desktop_env/controllers/python.py +471 -0
- desktop_env/controllers/setup.py +882 -0
- desktop_env/desktop_env.py +509 -0
- desktop_env/evaluators/__init__.py +5 -0
- desktop_env/evaluators/getters/__init__.py +41 -0
- desktop_env/evaluators/getters/calc.py +15 -0
- desktop_env/evaluators/getters/chrome.py +1774 -0
- desktop_env/evaluators/getters/file.py +154 -0
- desktop_env/evaluators/getters/general.py +42 -0
- desktop_env/evaluators/getters/gimp.py +38 -0
- desktop_env/evaluators/getters/impress.py +126 -0
- desktop_env/evaluators/getters/info.py +24 -0
- desktop_env/evaluators/getters/misc.py +406 -0
- desktop_env/evaluators/getters/replay.py +20 -0
- desktop_env/evaluators/getters/vlc.py +86 -0
- desktop_env/evaluators/getters/vscode.py +35 -0
- desktop_env/evaluators/metrics/__init__.py +160 -0
- desktop_env/evaluators/metrics/basic_os.py +68 -0
- desktop_env/evaluators/metrics/chrome.py +493 -0
- desktop_env/evaluators/metrics/docs.py +1011 -0
- desktop_env/evaluators/metrics/general.py +665 -0
- desktop_env/evaluators/metrics/gimp.py +637 -0
- desktop_env/evaluators/metrics/libreoffice.py +28 -0
- desktop_env/evaluators/metrics/others.py +92 -0
- desktop_env/evaluators/metrics/pdf.py +31 -0
- desktop_env/evaluators/metrics/slides.py +957 -0
- desktop_env/evaluators/metrics/table.py +585 -0
- desktop_env/evaluators/metrics/thunderbird.py +176 -0
- desktop_env/evaluators/metrics/utils.py +719 -0
- desktop_env/evaluators/metrics/vlc.py +524 -0
- desktop_env/evaluators/metrics/vscode.py +283 -0
- desktop_env/providers/__init__.py +35 -0
- desktop_env/providers/aws/__init__.py +0 -0
- desktop_env/providers/aws/manager.py +278 -0
- desktop_env/providers/aws/provider.py +186 -0
- desktop_env/providers/aws/provider_with_proxy.py +315 -0
- desktop_env/providers/aws/proxy_pool.py +193 -0
- desktop_env/providers/azure/__init__.py +0 -0
- desktop_env/providers/azure/manager.py +87 -0
- desktop_env/providers/azure/provider.py +207 -0
- desktop_env/providers/base.py +97 -0
- desktop_env/providers/gcp/__init__.py +0 -0
- desktop_env/providers/gcp/manager.py +0 -0
- desktop_env/providers/gcp/provider.py +0 -0
- desktop_env/providers/virtualbox/__init__.py +0 -0
- desktop_env/providers/virtualbox/manager.py +463 -0
- desktop_env/providers/virtualbox/provider.py +124 -0
- desktop_env/providers/vmware/__init__.py +0 -0
- desktop_env/providers/vmware/manager.py +455 -0
- desktop_env/providers/vmware/provider.py +105 -0
- gui_agents/__init__.py +0 -0
- gui_agents/agents/Action.py +209 -0
- gui_agents/agents/__init__.py +0 -0
- gui_agents/agents/agent_s.py +832 -0
- gui_agents/agents/global_state.py +610 -0
- gui_agents/agents/grounding.py +651 -0
- gui_agents/agents/hardware_interface.py +129 -0
- gui_agents/agents/manager.py +568 -0
- gui_agents/agents/translator.py +132 -0
- gui_agents/agents/worker.py +355 -0
- gui_agents/cli_app.py +560 -0
- gui_agents/core/__init__.py +0 -0
- gui_agents/core/engine.py +1496 -0
- gui_agents/core/knowledge.py +449 -0
- gui_agents/core/mllm.py +555 -0
- gui_agents/tools/__init__.py +0 -0
- gui_agents/tools/tools.py +727 -0
- gui_agents/unit_test/__init__.py +0 -0
- gui_agents/unit_test/run_tests.py +65 -0
- gui_agents/unit_test/test_manager.py +330 -0
- gui_agents/unit_test/test_worker.py +269 -0
- gui_agents/utils/__init__.py +0 -0
- gui_agents/utils/analyze_display.py +301 -0
- gui_agents/utils/common_utils.py +263 -0
- gui_agents/utils/display_viewer.py +281 -0
- gui_agents/utils/embedding_manager.py +53 -0
- gui_agents/utils/image_axis_utils.py +27 -0
- lybic_guiagents-0.1.0.dist-info/METADATA +416 -0
- lybic_guiagents-0.1.0.dist-info/RECORD +85 -0
- lybic_guiagents-0.1.0.dist-info/WHEEL +5 -0
- lybic_guiagents-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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
|