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,455 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import threading
|
|
7
|
+
from filelock import FileLock
|
|
8
|
+
import uuid
|
|
9
|
+
import zipfile
|
|
10
|
+
|
|
11
|
+
from time import sleep
|
|
12
|
+
import shutil
|
|
13
|
+
import psutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import requests
|
|
16
|
+
from tqdm import tqdm
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from desktop_env.providers.base import VMManager
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("desktopenv.providers.vmware.VMwareVMManager")
|
|
23
|
+
logger.setLevel(logging.INFO)
|
|
24
|
+
|
|
25
|
+
MAX_RETRY_TIMES = 10
|
|
26
|
+
RETRY_INTERVAL = 5
|
|
27
|
+
UBUNTU_ARM_URL = "https://huggingface.co/datasets/xlangai/ubuntu_osworld/resolve/main/Ubuntu-arm.zip"
|
|
28
|
+
UBUNTU_X86_URL = "https://huggingface.co/datasets/xlangai/ubuntu_osworld/resolve/main/Ubuntu-x86.zip"
|
|
29
|
+
WINDOWS_X86_URL = "https://huggingface.co/datasets/xlangai/windows_osworld/resolve/main/Windows-x86.zip"
|
|
30
|
+
|
|
31
|
+
# Determine the platform and CPU architecture to decide the correct VM image to download
|
|
32
|
+
if platform.system() == 'Darwin': # macOS
|
|
33
|
+
# if os.uname().machine == 'arm64': # Apple Silicon
|
|
34
|
+
URL = UBUNTU_ARM_URL
|
|
35
|
+
# else:
|
|
36
|
+
# url = UBUNTU_X86_URL
|
|
37
|
+
elif platform.machine().lower() in ['amd64', 'x86_64']:
|
|
38
|
+
URL = UBUNTU_X86_URL
|
|
39
|
+
else:
|
|
40
|
+
raise Exception("Unsupported platform or architecture")
|
|
41
|
+
|
|
42
|
+
DOWNLOADED_FILE_NAME = URL.split('/')[-1]
|
|
43
|
+
REGISTRY_PATH = '.vmware_vms'
|
|
44
|
+
LOCK_FILE_NAME = '.vmware_lck'
|
|
45
|
+
VMS_DIR = "vmware_vm_data"
|
|
46
|
+
update_lock = threading.Lock()
|
|
47
|
+
|
|
48
|
+
if platform.system() == 'Windows':
|
|
49
|
+
vboxmanage_path = r"C:\Program Files (x86)\VMware\VMware Workstation"
|
|
50
|
+
os.environ["PATH"] += os.pathsep + vboxmanage_path
|
|
51
|
+
|
|
52
|
+
def generate_new_vm_name(vms_dir, os_type):
|
|
53
|
+
registry_idx = 0
|
|
54
|
+
prefix = os_type
|
|
55
|
+
while True:
|
|
56
|
+
attempted_new_name = f"{prefix}{registry_idx}"
|
|
57
|
+
if os.path.exists(
|
|
58
|
+
os.path.join(vms_dir, attempted_new_name, attempted_new_name + ".vmx")):
|
|
59
|
+
registry_idx += 1
|
|
60
|
+
else:
|
|
61
|
+
return attempted_new_name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _update_vm(vmx_path, target_vm_name):
|
|
65
|
+
"""Update the VMX file with the new VM name and other parameters, so that the VM can be started successfully without conflict with the original VM."""
|
|
66
|
+
with update_lock:
|
|
67
|
+
dir_path, vmx_file = os.path.split(vmx_path)
|
|
68
|
+
|
|
69
|
+
def _generate_mac_address():
|
|
70
|
+
# VMware MAC address range starts with 00:0c:29
|
|
71
|
+
mac = [0x00, 0x0c, 0x29,
|
|
72
|
+
random.randint(0x00, 0x7f),
|
|
73
|
+
random.randint(0x00, 0xff),
|
|
74
|
+
random.randint(0x00, 0xff)]
|
|
75
|
+
return ':'.join(map(lambda x: "%02x" % x, mac))
|
|
76
|
+
|
|
77
|
+
# Backup the original file
|
|
78
|
+
with open(vmx_path, 'r') as file:
|
|
79
|
+
original_content = file.read()
|
|
80
|
+
|
|
81
|
+
# Generate new values
|
|
82
|
+
new_uuid_bios = str(uuid.uuid4())
|
|
83
|
+
new_uuid_location = str(uuid.uuid4())
|
|
84
|
+
new_mac_address = _generate_mac_address()
|
|
85
|
+
new_vmci_id = str(random.randint(-2147483648, 2147483647)) # Random 32-bit integer
|
|
86
|
+
|
|
87
|
+
# Update the content
|
|
88
|
+
updated_content = re.sub(r'displayName = ".*?"', f'displayName = "{target_vm_name}"', original_content)
|
|
89
|
+
updated_content = re.sub(r'uuid.bios = ".*?"', f'uuid.bios = "{new_uuid_bios}"', updated_content)
|
|
90
|
+
updated_content = re.sub(r'uuid.location = ".*?"', f'uuid.location = "{new_uuid_location}"', updated_content)
|
|
91
|
+
updated_content = re.sub(r'ethernet0.generatedAddress = ".*?"',
|
|
92
|
+
f'ethernet0.generatedAddress = "{new_mac_address}"',
|
|
93
|
+
updated_content)
|
|
94
|
+
updated_content = re.sub(r'vmci0.id = ".*?"', f'vmci0.id = "{new_vmci_id}"', updated_content)
|
|
95
|
+
|
|
96
|
+
# Write the updated content back to the file
|
|
97
|
+
with open(vmx_path, 'w') as file:
|
|
98
|
+
file.write(updated_content)
|
|
99
|
+
|
|
100
|
+
logger.info(".vmx file updated successfully.")
|
|
101
|
+
|
|
102
|
+
vmx_file_base_name = os.path.splitext(vmx_file)[0]
|
|
103
|
+
|
|
104
|
+
files_to_rename = ['vmx', 'nvram', 'vmsd', 'vmxf']
|
|
105
|
+
|
|
106
|
+
for ext in files_to_rename:
|
|
107
|
+
original_file = os.path.join(dir_path, f"{vmx_file_base_name}.{ext}")
|
|
108
|
+
target_file = os.path.join(dir_path, f"{target_vm_name}.{ext}")
|
|
109
|
+
os.rename(original_file, target_file)
|
|
110
|
+
|
|
111
|
+
# Update the dir_path to the target vm_name, only replace the last character
|
|
112
|
+
# Split the path into parts up to the last folder
|
|
113
|
+
path_parts = dir_path.rstrip(os.sep).split(os.sep)
|
|
114
|
+
path_parts[-1] = target_vm_name
|
|
115
|
+
target_dir_path = os.sep.join(path_parts)
|
|
116
|
+
os.rename(dir_path, target_dir_path)
|
|
117
|
+
|
|
118
|
+
logger.info("VM files renamed successfully.")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _install_vm(vm_name, vms_dir, downloaded_file_name, os_type, original_vm_name="Ubuntu"):
|
|
122
|
+
os.makedirs(vms_dir, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
def __download_and_unzip_vm():
|
|
125
|
+
# Download the virtual machine image
|
|
126
|
+
logger.info("Downloading the virtual machine image...")
|
|
127
|
+
downloaded_size = 0
|
|
128
|
+
|
|
129
|
+
if os_type == "Ubuntu":
|
|
130
|
+
if platform.system() == 'Darwin':
|
|
131
|
+
URL = UBUNTU_ARM_URL
|
|
132
|
+
elif platform.machine().lower() in ['amd64', 'x86_64']:
|
|
133
|
+
URL = UBUNTU_X86_URL
|
|
134
|
+
elif os_type == "Windows":
|
|
135
|
+
if platform.machine().lower() in ['amd64', 'x86_64']:
|
|
136
|
+
URL = WINDOWS_X86_URL
|
|
137
|
+
DOWNLOADED_FILE_NAME = URL.split('/')[-1]
|
|
138
|
+
downloaded_file_name = DOWNLOADED_FILE_NAME
|
|
139
|
+
|
|
140
|
+
while True:
|
|
141
|
+
downloaded_file_path = os.path.join(vms_dir, downloaded_file_name)
|
|
142
|
+
headers = {}
|
|
143
|
+
if os.path.exists(downloaded_file_path):
|
|
144
|
+
downloaded_size = os.path.getsize(downloaded_file_path)
|
|
145
|
+
headers["Range"] = f"bytes={downloaded_size}-"
|
|
146
|
+
|
|
147
|
+
with requests.get(URL, headers=headers, stream=True) as response:
|
|
148
|
+
if response.status_code == 416:
|
|
149
|
+
# This means the range was not satisfiable, possibly the file was fully downloaded
|
|
150
|
+
logger.info("Fully downloaded or the file size changed.")
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
response.raise_for_status()
|
|
154
|
+
total_size = int(response.headers.get('content-length', 0))
|
|
155
|
+
|
|
156
|
+
with open(downloaded_file_path, "ab") as file, tqdm(
|
|
157
|
+
desc="Progress",
|
|
158
|
+
total=total_size,
|
|
159
|
+
unit='iB',
|
|
160
|
+
unit_scale=True,
|
|
161
|
+
unit_divisor=1024,
|
|
162
|
+
initial=downloaded_size,
|
|
163
|
+
ascii=True
|
|
164
|
+
) as progress_bar:
|
|
165
|
+
try:
|
|
166
|
+
for data in response.iter_content(chunk_size=1024):
|
|
167
|
+
size = file.write(data)
|
|
168
|
+
progress_bar.update(size)
|
|
169
|
+
except (requests.exceptions.RequestException, IOError) as e:
|
|
170
|
+
logger.error(f"Download error: {e}")
|
|
171
|
+
sleep(RETRY_INTERVAL)
|
|
172
|
+
logger.error("Retrying...")
|
|
173
|
+
else:
|
|
174
|
+
logger.info("Download succeeds.")
|
|
175
|
+
break # Download completed successfully
|
|
176
|
+
|
|
177
|
+
# Unzip the downloaded file
|
|
178
|
+
logger.info("Unzipping the downloaded file...☕️")
|
|
179
|
+
with zipfile.ZipFile(downloaded_file_path, 'r') as zip_ref:
|
|
180
|
+
zip_ref.extractall(os.path.join(vms_dir, vm_name))
|
|
181
|
+
logger.info("Files have been successfully extracted to the directory: " + str(os.path.join(vms_dir, vm_name)))
|
|
182
|
+
|
|
183
|
+
vm_path = os.path.join(vms_dir, vm_name, vm_name + ".vmx")
|
|
184
|
+
|
|
185
|
+
# Execute the function to download and unzip the VM, and update the vm metadata
|
|
186
|
+
if not os.path.exists(vm_path):
|
|
187
|
+
__download_and_unzip_vm()
|
|
188
|
+
_update_vm(os.path.join(vms_dir, vm_name, original_vm_name + ".vmx"), vm_name)
|
|
189
|
+
else:
|
|
190
|
+
logger.info(f"Virtual machine exists: {vm_path}")
|
|
191
|
+
|
|
192
|
+
# Determine the platform of the host machine and decide the parameter for vmrun
|
|
193
|
+
def get_vmrun_type():
|
|
194
|
+
if platform.system() == 'Windows' or platform.system() == 'Linux':
|
|
195
|
+
return '-T ws'
|
|
196
|
+
elif platform.system() == 'Darwin': # Darwin is the system name for macOS
|
|
197
|
+
return '-T fusion'
|
|
198
|
+
else:
|
|
199
|
+
raise Exception("Unsupported operating system")
|
|
200
|
+
|
|
201
|
+
# Start the virtual machine
|
|
202
|
+
def start_vm(vm_path, max_retries=20):
|
|
203
|
+
command = f'vmrun {get_vmrun_type()} start "{vm_path}" nogui'
|
|
204
|
+
for attempt in range(max_retries):
|
|
205
|
+
result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8")
|
|
206
|
+
if result.returncode == 0:
|
|
207
|
+
logger.info("Virtual machine started.")
|
|
208
|
+
return True
|
|
209
|
+
else:
|
|
210
|
+
if "Error" in result.stderr:
|
|
211
|
+
logger.error(f"Attempt {attempt + 1} failed with specific error: {result.stderr}")
|
|
212
|
+
else:
|
|
213
|
+
logger.error(f"Attempt {attempt + 1} failed: {result.stderr}")
|
|
214
|
+
|
|
215
|
+
if attempt == max_retries - 1:
|
|
216
|
+
logger.error("Maximum retry attempts reached, failed to start the virtual machine.")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
if not start_vm(vm_path):
|
|
220
|
+
raise ValueError("Error encountered during installation, please rerun the code for retrying.")
|
|
221
|
+
|
|
222
|
+
def get_vm_ip(vm_path, max_retries=20):
|
|
223
|
+
command = f'vmrun {get_vmrun_type()} getGuestIPAddress "{vm_path}" -wait'
|
|
224
|
+
for attempt in range(max_retries):
|
|
225
|
+
result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8")
|
|
226
|
+
if result.returncode == 0:
|
|
227
|
+
return result.stdout.strip()
|
|
228
|
+
else:
|
|
229
|
+
if "Error" in result.stderr:
|
|
230
|
+
logger.error(f"Attempt {attempt + 1} failed with specific error: {result.stderr}")
|
|
231
|
+
else:
|
|
232
|
+
logger.error(f"Attempt {attempt + 1} failed: {result.stderr}")
|
|
233
|
+
|
|
234
|
+
if attempt == max_retries - 1:
|
|
235
|
+
logger.error("Maximum retry attempts reached, failed to get the IP of virtual machine.")
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
vm_ip = get_vm_ip(vm_path)
|
|
239
|
+
if not vm_ip:
|
|
240
|
+
raise ValueError("Error encountered during installation, please rerun the code for retrying.")
|
|
241
|
+
|
|
242
|
+
# Function used to check whether the virtual machine is ready
|
|
243
|
+
def download_screenshot(ip):
|
|
244
|
+
url = f"http://{ip}:5000/screenshot"
|
|
245
|
+
try:
|
|
246
|
+
# max trey times 1, max timeout 1
|
|
247
|
+
response = requests.get(url, timeout=(10, 10))
|
|
248
|
+
if response.status_code == 200:
|
|
249
|
+
return True
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.error(f"Error: {e}")
|
|
252
|
+
logger.error(f"Type: {type(e).__name__}")
|
|
253
|
+
logger.error(f"Error detail: {str(e)}")
|
|
254
|
+
sleep(RETRY_INTERVAL)
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
# Try downloading the screenshot until successful
|
|
258
|
+
while not download_screenshot(vm_ip):
|
|
259
|
+
# Try to get the IP again in case it has changed
|
|
260
|
+
vm_ip = get_vm_ip(vm_path)
|
|
261
|
+
logger.info("Check whether the virtual machine is ready...")
|
|
262
|
+
|
|
263
|
+
logger.info("Virtual machine is ready. Start to make a snapshot on the virtual machine. It would take a while...")
|
|
264
|
+
|
|
265
|
+
def create_vm_snapshot(vm_path, max_retries=20):
|
|
266
|
+
command = f'vmrun {get_vmrun_type()} snapshot "{vm_path}" "init_state"'
|
|
267
|
+
for attempt in range(max_retries):
|
|
268
|
+
result = subprocess.run(command, shell=True, text=True, capture_output=True, encoding="utf-8")
|
|
269
|
+
if result.returncode == 0:
|
|
270
|
+
logger.info("Snapshot created.")
|
|
271
|
+
return True
|
|
272
|
+
else:
|
|
273
|
+
if "Error" in result.stderr:
|
|
274
|
+
logger.error(f"Attempt {attempt + 1} failed with specific error: {result.stderr}")
|
|
275
|
+
else:
|
|
276
|
+
logger.error(f"Attempt {attempt + 1} failed: {result.stderr}")
|
|
277
|
+
|
|
278
|
+
if attempt == max_retries - 1:
|
|
279
|
+
logger.error("Maximum retry attempts reached, failed to create snapshot.")
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
# Create a snapshot of the virtual machine
|
|
283
|
+
if create_vm_snapshot(vm_path, max_retries=MAX_RETRY_TIMES):
|
|
284
|
+
return vm_path
|
|
285
|
+
else:
|
|
286
|
+
raise ValueError("Error encountered during installation, please rerun the code for retrying.")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class VMwareVMManager(VMManager):
|
|
290
|
+
def __init__(self, registry_path=REGISTRY_PATH):
|
|
291
|
+
self.registry_path = registry_path
|
|
292
|
+
self.lock = FileLock(LOCK_FILE_NAME, timeout=60)
|
|
293
|
+
self.initialize_registry()
|
|
294
|
+
|
|
295
|
+
def initialize_registry(self):
|
|
296
|
+
with self.lock: # Locking during initialization
|
|
297
|
+
if not os.path.exists(self.registry_path):
|
|
298
|
+
with open(self.registry_path, 'w') as file:
|
|
299
|
+
file.write('')
|
|
300
|
+
|
|
301
|
+
def add_vm(self, vm_path, lock_needed=True):
|
|
302
|
+
if lock_needed:
|
|
303
|
+
with self.lock:
|
|
304
|
+
self._add_vm(vm_path)
|
|
305
|
+
else:
|
|
306
|
+
self._add_vm(vm_path)
|
|
307
|
+
|
|
308
|
+
def _add_vm(self, vm_path, region=None):
|
|
309
|
+
assert region in [None, 'local'], "For VMware provider, the region should be neither None or 'local'."
|
|
310
|
+
with self.lock:
|
|
311
|
+
with open(self.registry_path, 'r') as file:
|
|
312
|
+
lines = file.readlines()
|
|
313
|
+
new_lines = lines + [f'{vm_path}|free\n']
|
|
314
|
+
with open(self.registry_path, 'w') as file:
|
|
315
|
+
file.writelines(new_lines)
|
|
316
|
+
|
|
317
|
+
def occupy_vm(self, vm_path, pid, lock_needed=True):
|
|
318
|
+
if lock_needed:
|
|
319
|
+
with self.lock:
|
|
320
|
+
self._occupy_vm(vm_path, pid)
|
|
321
|
+
else:
|
|
322
|
+
self._occupy_vm(vm_path, pid)
|
|
323
|
+
|
|
324
|
+
def _occupy_vm(self, vm_path, pid, region=None):
|
|
325
|
+
assert region in [None, 'local'], "For VMware provider, the region should be neither None or 'local'."
|
|
326
|
+
with self.lock:
|
|
327
|
+
new_lines = []
|
|
328
|
+
with open(self.registry_path, 'r') as file:
|
|
329
|
+
lines = file.readlines()
|
|
330
|
+
for line in lines:
|
|
331
|
+
registered_vm_path, _ = line.strip().split('|')
|
|
332
|
+
if registered_vm_path == vm_path:
|
|
333
|
+
new_lines.append(f'{registered_vm_path}|{pid}\n')
|
|
334
|
+
else:
|
|
335
|
+
new_lines.append(line)
|
|
336
|
+
with open(self.registry_path, 'w') as file:
|
|
337
|
+
file.writelines(new_lines)
|
|
338
|
+
|
|
339
|
+
def delete_vm(self, vm_path, lock_needed=True):
|
|
340
|
+
if lock_needed:
|
|
341
|
+
with self.lock:
|
|
342
|
+
self._delete_vm(vm_path)
|
|
343
|
+
else:
|
|
344
|
+
self._delete_vm(vm_path)
|
|
345
|
+
|
|
346
|
+
def _delete_vm(self, vm_path):
|
|
347
|
+
raise NotImplementedError
|
|
348
|
+
|
|
349
|
+
def check_and_clean(self, vms_dir, lock_needed=True):
|
|
350
|
+
if lock_needed:
|
|
351
|
+
with self.lock:
|
|
352
|
+
self._check_and_clean(vms_dir)
|
|
353
|
+
else:
|
|
354
|
+
self._check_and_clean(vms_dir)
|
|
355
|
+
|
|
356
|
+
def _check_and_clean(self, vms_dir):
|
|
357
|
+
with self.lock: # Lock when cleaning up the registry and vms_dir
|
|
358
|
+
# Check and clean on the running vms, detect the released ones and mark then as 'free'
|
|
359
|
+
active_pids = {p.pid for p in psutil.process_iter()}
|
|
360
|
+
new_lines = []
|
|
361
|
+
vm_paths = []
|
|
362
|
+
|
|
363
|
+
with open(self.registry_path, 'r') as file:
|
|
364
|
+
lines = file.readlines()
|
|
365
|
+
for line in lines:
|
|
366
|
+
vm_path, pid_str = line.strip().split('|')
|
|
367
|
+
if not os.path.exists(vm_path):
|
|
368
|
+
logger.info(f"VM {vm_path} not found, releasing it.")
|
|
369
|
+
new_lines.append(f'{vm_path}|free\n')
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
vm_paths.append(vm_path)
|
|
373
|
+
if pid_str == "free":
|
|
374
|
+
new_lines.append(line)
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
if int(pid_str) in active_pids:
|
|
378
|
+
new_lines.append(line)
|
|
379
|
+
else:
|
|
380
|
+
new_lines.append(f'{vm_path}|free\n')
|
|
381
|
+
with open(self.registry_path, 'w') as file:
|
|
382
|
+
file.writelines(new_lines)
|
|
383
|
+
|
|
384
|
+
# Check and clean on the files inside vms_dir, delete the unregistered ones
|
|
385
|
+
os.makedirs(vms_dir, exist_ok=True)
|
|
386
|
+
vm_names = os.listdir(vms_dir)
|
|
387
|
+
for vm_name in vm_names:
|
|
388
|
+
# skip the downloaded .zip file
|
|
389
|
+
if ".zip" in vm_name:
|
|
390
|
+
continue
|
|
391
|
+
# Skip the .DS_Store file on macOS
|
|
392
|
+
if vm_name == ".DS_Store":
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
flag = True
|
|
396
|
+
for vm_path in vm_paths:
|
|
397
|
+
if vm_name + ".vmx" in vm_path:
|
|
398
|
+
flag = False
|
|
399
|
+
if flag:
|
|
400
|
+
shutil.rmtree(os.path.join(vms_dir, vm_name))
|
|
401
|
+
|
|
402
|
+
def list_free_vms(self, lock_needed=True):
|
|
403
|
+
if lock_needed:
|
|
404
|
+
with self.lock:
|
|
405
|
+
return self._list_free_vms()
|
|
406
|
+
else:
|
|
407
|
+
return self._list_free_vms()
|
|
408
|
+
|
|
409
|
+
def _list_free_vms(self):
|
|
410
|
+
with self.lock: # Lock when reading the registry
|
|
411
|
+
free_vms = []
|
|
412
|
+
with open(self.registry_path, 'r') as file:
|
|
413
|
+
lines = file.readlines()
|
|
414
|
+
for line in lines:
|
|
415
|
+
vm_path, pid_str = line.strip().split('|')
|
|
416
|
+
if pid_str == "free":
|
|
417
|
+
free_vms.append((vm_path, pid_str))
|
|
418
|
+
return free_vms
|
|
419
|
+
|
|
420
|
+
def get_vm_path(self, os_type, region=None, screen_size=(1920, 1080), **kwargs):
|
|
421
|
+
# Note: screen_size parameter is ignored for VMware provider
|
|
422
|
+
# but kept for interface consistency with other providers
|
|
423
|
+
with self.lock:
|
|
424
|
+
if not VMwareVMManager.checked_and_cleaned:
|
|
425
|
+
VMwareVMManager.checked_and_cleaned = True
|
|
426
|
+
self._check_and_clean(vms_dir=VMS_DIR)
|
|
427
|
+
|
|
428
|
+
allocation_needed = False
|
|
429
|
+
with self.lock:
|
|
430
|
+
free_vms_paths = self._list_free_vms()
|
|
431
|
+
if len(free_vms_paths) == 0:
|
|
432
|
+
# No free virtual machine available, generate a new one
|
|
433
|
+
allocation_needed = True
|
|
434
|
+
else:
|
|
435
|
+
# Choose the first free virtual machine
|
|
436
|
+
chosen_vm_path = free_vms_paths[0][0]
|
|
437
|
+
self._occupy_vm(chosen_vm_path, os.getpid())
|
|
438
|
+
return chosen_vm_path
|
|
439
|
+
|
|
440
|
+
if allocation_needed:
|
|
441
|
+
logger.info("No free virtual machine available. Generating a new one, which would take a while...☕")
|
|
442
|
+
new_vm_name = generate_new_vm_name(vms_dir=VMS_DIR, os_type=os_type)
|
|
443
|
+
|
|
444
|
+
original_vm_name = None
|
|
445
|
+
if os_type == "Ubuntu":
|
|
446
|
+
original_vm_name = "Ubuntu"
|
|
447
|
+
elif os_type == "Windows":
|
|
448
|
+
original_vm_name = "Windows 10 x64"
|
|
449
|
+
|
|
450
|
+
new_vm_path = _install_vm(new_vm_name, vms_dir=VMS_DIR,
|
|
451
|
+
downloaded_file_name=DOWNLOADED_FILE_NAME, original_vm_name=original_vm_name, os_type=os_type)
|
|
452
|
+
with self.lock:
|
|
453
|
+
self._add_vm(new_vm_path)
|
|
454
|
+
self._occupy_vm(new_vm_path, os.getpid())
|
|
455
|
+
return new_vm_path
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from desktop_env.providers.base import Provider
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("desktopenv.providers.vmware.VMwareProvider")
|
|
10
|
+
logger.setLevel(logging.INFO)
|
|
11
|
+
|
|
12
|
+
WAIT_TIME = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_vmrun_type(return_list=False):
|
|
16
|
+
if platform.system() == 'Windows' or platform.system() == 'Linux':
|
|
17
|
+
if return_list:
|
|
18
|
+
return ['-T', 'ws']
|
|
19
|
+
else:
|
|
20
|
+
return '-T ws'
|
|
21
|
+
elif platform.system() == 'Darwin': # Darwin is the system name for macOS
|
|
22
|
+
if return_list:
|
|
23
|
+
return ['-T', 'fusion']
|
|
24
|
+
else:
|
|
25
|
+
return '-T fusion'
|
|
26
|
+
else:
|
|
27
|
+
raise Exception("Unsupported operating system")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class VMwareProvider(Provider):
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _execute_command(command: list, return_output=False):
|
|
33
|
+
process = subprocess.Popen(
|
|
34
|
+
command,
|
|
35
|
+
stdout=subprocess.PIPE,
|
|
36
|
+
stderr=subprocess.PIPE,
|
|
37
|
+
text=True,
|
|
38
|
+
encoding="utf-8"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if return_output:
|
|
42
|
+
output = process.communicate()[0].strip()
|
|
43
|
+
return output
|
|
44
|
+
else:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def start_emulator(self, path_to_vm: str, headless: bool, os_type: str):
|
|
48
|
+
print("Starting VMware VM...")
|
|
49
|
+
logger.info("Starting VMware VM...")
|
|
50
|
+
|
|
51
|
+
while True:
|
|
52
|
+
try:
|
|
53
|
+
output = subprocess.check_output(f"vmrun {get_vmrun_type()} list", shell=True, stderr=subprocess.STDOUT)
|
|
54
|
+
output = output.decode()
|
|
55
|
+
output = output.splitlines()
|
|
56
|
+
normalized_path_to_vm = os.path.abspath(os.path.normpath(path_to_vm))
|
|
57
|
+
|
|
58
|
+
if any(os.path.abspath(os.path.normpath(line)) == normalized_path_to_vm for line in output):
|
|
59
|
+
logger.info("VM is running.")
|
|
60
|
+
break
|
|
61
|
+
else:
|
|
62
|
+
logger.info("Starting VM...")
|
|
63
|
+
_command = ["vmrun"] + get_vmrun_type(return_list=True) + ["start", path_to_vm]
|
|
64
|
+
if headless:
|
|
65
|
+
_command.append("nogui")
|
|
66
|
+
VMwareProvider._execute_command(_command)
|
|
67
|
+
time.sleep(WAIT_TIME)
|
|
68
|
+
|
|
69
|
+
except subprocess.CalledProcessError as e:
|
|
70
|
+
logger.error(f"Error executing command: {e.output.decode().strip()}")
|
|
71
|
+
|
|
72
|
+
def get_ip_address(self, path_to_vm: str) -> str:
|
|
73
|
+
logger.info("Getting VMware VM IP address...")
|
|
74
|
+
while True:
|
|
75
|
+
try:
|
|
76
|
+
output = VMwareProvider._execute_command(
|
|
77
|
+
["vmrun"] + get_vmrun_type(return_list=True) + ["getGuestIPAddress", path_to_vm, "-wait"],
|
|
78
|
+
return_output=True
|
|
79
|
+
)
|
|
80
|
+
logger.info(f"VMware VM IP address: {output}")
|
|
81
|
+
return output
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(e)
|
|
84
|
+
time.sleep(WAIT_TIME)
|
|
85
|
+
logger.info("Retrying to get VMware VM IP address...")
|
|
86
|
+
|
|
87
|
+
def save_state(self, path_to_vm: str, snapshot_name: str):
|
|
88
|
+
logger.info("Saving VMware VM state...")
|
|
89
|
+
VMwareProvider._execute_command(
|
|
90
|
+
["vmrun"] + get_vmrun_type(return_list=True) + ["snapshot", path_to_vm, snapshot_name])
|
|
91
|
+
time.sleep(WAIT_TIME) # Wait for the VM to save
|
|
92
|
+
|
|
93
|
+
def revert_to_snapshot(self, path_to_vm: str, snapshot_name: str):
|
|
94
|
+
logger.info(f"Reverting VMware VM to snapshot: {snapshot_name}...")
|
|
95
|
+
VMwareProvider._execute_command(
|
|
96
|
+
["vmrun"] + get_vmrun_type(return_list=True) + ["revertToSnapshot", path_to_vm, snapshot_name])
|
|
97
|
+
time.sleep(WAIT_TIME) # Wait for the VM to revert
|
|
98
|
+
return path_to_vm
|
|
99
|
+
|
|
100
|
+
def stop_emulator(self, path_to_vm: str, region=None, *args, **kwargs):
|
|
101
|
+
# Note: region parameter is ignored for VMware provider
|
|
102
|
+
# but kept for interface consistency with other providers
|
|
103
|
+
logger.info("Stopping VMware VM...")
|
|
104
|
+
VMwareProvider._execute_command(["vmrun"] + get_vmrun_type(return_list=True) + ["stop", path_to_vm])
|
|
105
|
+
time.sleep(WAIT_TIME) # Wait for the VM to stop
|
gui_agents/__init__.py
ADDED
|
File without changes
|