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,283 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import importlib.util
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import re
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_json_keybindings(actual: str, expected: str, **options) -> float:
|
|
10
|
+
"""
|
|
11
|
+
Args:
|
|
12
|
+
actual (str): path to result text file
|
|
13
|
+
expected (str): expected dict{}
|
|
14
|
+
|
|
15
|
+
Return:
|
|
16
|
+
float: the score
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def direct_load_json(fp):
|
|
20
|
+
try:
|
|
21
|
+
with open(fp, 'r') as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
return data
|
|
24
|
+
except:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def skip_first_line_load_json(fp):
|
|
28
|
+
try:
|
|
29
|
+
with open(fp, 'r') as f:
|
|
30
|
+
f.readline()
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
return data
|
|
33
|
+
except:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
for func in [direct_load_json, skip_first_line_load_json]:
|
|
37
|
+
data = func(actual)
|
|
38
|
+
if data is not None and type(data) == list:
|
|
39
|
+
break
|
|
40
|
+
else:
|
|
41
|
+
return 0.0
|
|
42
|
+
expected = expected['expected']
|
|
43
|
+
if expected in data:
|
|
44
|
+
return 1.0
|
|
45
|
+
else:
|
|
46
|
+
return 0.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_json_settings(actual: str, expected: str, **options) -> float:
|
|
50
|
+
"""
|
|
51
|
+
Args:
|
|
52
|
+
actual (str): path to result text file
|
|
53
|
+
expected (dict): expected dict{}, containing key "expect"
|
|
54
|
+
|
|
55
|
+
Return:
|
|
56
|
+
float: the score
|
|
57
|
+
"""
|
|
58
|
+
if not actual:
|
|
59
|
+
return 0.
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with open(actual, 'r') as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return 0.0
|
|
66
|
+
|
|
67
|
+
expect = expected['expected']
|
|
68
|
+
|
|
69
|
+
# Check if all expected key-value pairs are in the actual data
|
|
70
|
+
for key, value in expect.items():
|
|
71
|
+
if key not in data or data[key] != value:
|
|
72
|
+
return 0.0
|
|
73
|
+
|
|
74
|
+
return 1.0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compare_text_file(actual: str, expected: str, **options) -> float:
|
|
78
|
+
"""
|
|
79
|
+
Args:
|
|
80
|
+
actual (str): path to result text file
|
|
81
|
+
expected (str): path to gold text file
|
|
82
|
+
|
|
83
|
+
Return:
|
|
84
|
+
float: the score
|
|
85
|
+
"""
|
|
86
|
+
if not actual:
|
|
87
|
+
return 0.
|
|
88
|
+
|
|
89
|
+
with open(actual) as f1:
|
|
90
|
+
actual_text = f1.read()
|
|
91
|
+
with open(expected) as f2:
|
|
92
|
+
expected_text = f2.read()
|
|
93
|
+
|
|
94
|
+
ignore_blanks = options.get('ignore_blanks', False)
|
|
95
|
+
if ignore_blanks:
|
|
96
|
+
actual_text = re.sub(r'[\t\n]', ' ', actual_text).strip()
|
|
97
|
+
actual_text = re.sub(r'\s+', ' ', actual_text)
|
|
98
|
+
expected_text = re.sub(r'[\t\n]', ' ', expected_text).strip()
|
|
99
|
+
expected_text = re.sub(r'\s+', ' ', expected_text)
|
|
100
|
+
|
|
101
|
+
ignore_case = options.get('ignore_case', False)
|
|
102
|
+
if ignore_case:
|
|
103
|
+
actual_text = actual_text.lower()
|
|
104
|
+
expected_text = expected_text.lower()
|
|
105
|
+
|
|
106
|
+
if actual_text == expected_text:
|
|
107
|
+
return 1.0
|
|
108
|
+
return 0.0
|
|
109
|
+
|
|
110
|
+
import zipfile
|
|
111
|
+
from difflib import SequenceMatcher
|
|
112
|
+
import PyPDF2
|
|
113
|
+
|
|
114
|
+
def compare_pdf_content(content1, content2, text_similarity_threshold):
|
|
115
|
+
def extract_text_from_pdf(content):
|
|
116
|
+
with open("temp.pdf", "wb") as temp_pdf:
|
|
117
|
+
temp_pdf.write(content)
|
|
118
|
+
with open("temp.pdf", "rb") as temp_pdf:
|
|
119
|
+
pdf_reader = PyPDF2.PdfReader(temp_pdf)
|
|
120
|
+
text = ''
|
|
121
|
+
for page_num in range(len(pdf_reader.pages)):
|
|
122
|
+
page = pdf_reader.pages[page_num]
|
|
123
|
+
text += page.extract_text()
|
|
124
|
+
return text
|
|
125
|
+
|
|
126
|
+
text1 = extract_text_from_pdf(content1)
|
|
127
|
+
text2 = extract_text_from_pdf(content2)
|
|
128
|
+
|
|
129
|
+
similarity_ratio = SequenceMatcher(None, text1, text2).ratio()
|
|
130
|
+
|
|
131
|
+
return similarity_ratio >= text_similarity_threshold
|
|
132
|
+
|
|
133
|
+
def compare_zip_files(actual: str, expected: str, **options) -> float:
|
|
134
|
+
"""
|
|
135
|
+
Args:
|
|
136
|
+
actual (str): path to result zip file
|
|
137
|
+
expected (str): path to gold zip file
|
|
138
|
+
|
|
139
|
+
Return:
|
|
140
|
+
float: the score
|
|
141
|
+
"""
|
|
142
|
+
if not actual:
|
|
143
|
+
return 0.
|
|
144
|
+
|
|
145
|
+
with zipfile.ZipFile(actual, 'r') as zip_file1, zipfile.ZipFile(expected, 'r') as zip_file2:
|
|
146
|
+
file_list1 = set(zip_file1.namelist())
|
|
147
|
+
file_list2 = set(zip_file2.namelist())
|
|
148
|
+
|
|
149
|
+
if file_list1 != file_list2:
|
|
150
|
+
return 0.0
|
|
151
|
+
|
|
152
|
+
for file_name in file_list1:
|
|
153
|
+
content1 = zip_file1.read(file_name)
|
|
154
|
+
content2 = zip_file2.read(file_name)
|
|
155
|
+
|
|
156
|
+
if file_name.lower().endswith('.pdf'):
|
|
157
|
+
if compare_pdf_content(content1, content2, 0.95):
|
|
158
|
+
continue
|
|
159
|
+
else:
|
|
160
|
+
return 0.0
|
|
161
|
+
elif content1 != content2:
|
|
162
|
+
return 0.0
|
|
163
|
+
return 1.0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def compare_config(actual: str, rules: Dict, **options) -> float:
|
|
167
|
+
if not actual:
|
|
168
|
+
return 0.
|
|
169
|
+
|
|
170
|
+
with open(actual) as f1:
|
|
171
|
+
actual_text = f1.read()
|
|
172
|
+
|
|
173
|
+
if actual_text == rules['expected']:
|
|
174
|
+
return 1.0
|
|
175
|
+
return 0.0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def compare_answer(actual: str, rules: Dict, **options) -> float:
|
|
179
|
+
"""
|
|
180
|
+
Args:
|
|
181
|
+
actual (str): result string
|
|
182
|
+
expected (str): gold string
|
|
183
|
+
|
|
184
|
+
Return:
|
|
185
|
+
float: the score
|
|
186
|
+
"""
|
|
187
|
+
if not actual:
|
|
188
|
+
return 0.
|
|
189
|
+
|
|
190
|
+
if actual == rules['expected']:
|
|
191
|
+
return 1.0
|
|
192
|
+
|
|
193
|
+
# TODO: can use text embedding to get non-zero return
|
|
194
|
+
return 0.0
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def is_extension_installed(actual: str, rules: Dict, **options):
|
|
198
|
+
if rules['type'] == 'contain':
|
|
199
|
+
if rules['expected'] in actual:
|
|
200
|
+
return 1.0
|
|
201
|
+
return 0.0
|
|
202
|
+
elif rules['type'] == 'not_contain':
|
|
203
|
+
if rules['expected'] not in actual:
|
|
204
|
+
return 1.0
|
|
205
|
+
return 0.0
|
|
206
|
+
else:
|
|
207
|
+
raise NotImplementedError
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def check_python_file_by_test_suite(actual_files, test_file, **options) -> float:
|
|
211
|
+
"""Check the python file by running the test suite in the given test file."""
|
|
212
|
+
|
|
213
|
+
test_function_name = options.get('test_function_name', 'test')
|
|
214
|
+
# Create a unique module name, it can be arbitrary but must be unique in the current runtime environment
|
|
215
|
+
module_name = 'dynamic_module'
|
|
216
|
+
|
|
217
|
+
# Load the module from the given file path
|
|
218
|
+
spec = importlib.util.spec_from_file_location(module_name, test_file)
|
|
219
|
+
module = importlib.util.module_from_spec(spec)
|
|
220
|
+
sys.modules[module_name] = module # Add the loaded module to sys.modules
|
|
221
|
+
spec.loader.exec_module(module) # Execute the module to make its content available
|
|
222
|
+
|
|
223
|
+
# Retrieve the function by name from the loaded module and execute it
|
|
224
|
+
test_function = getattr(module, test_function_name)
|
|
225
|
+
try:
|
|
226
|
+
if test_function():
|
|
227
|
+
return 1.0
|
|
228
|
+
else:
|
|
229
|
+
return 0.0
|
|
230
|
+
except Exception as e:
|
|
231
|
+
return 0.0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def check_python_file_by_gold_file(actual_files, gold_file: str, **options) -> float:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def check_html_background_image(src_path: str, rule: Dict = None) -> float:
|
|
239
|
+
"""
|
|
240
|
+
Check if the background image is correctly set.
|
|
241
|
+
multi-app:bb7db4c2-30b5-4be7-8dd7-b8c4ec7d3108
|
|
242
|
+
"""
|
|
243
|
+
if not src_path:
|
|
244
|
+
return 0.0
|
|
245
|
+
|
|
246
|
+
from bs4 import BeautifulSoup
|
|
247
|
+
with open(src_path, 'r') as f:
|
|
248
|
+
html_content = f.read()
|
|
249
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
250
|
+
styles = soup.find_all('style')
|
|
251
|
+
for style in styles:
|
|
252
|
+
if f'background-image: url(\'{rule["value"]}\')' in style.text:
|
|
253
|
+
return 1.0
|
|
254
|
+
return 0.0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def compare_result_files(src_path, tgt_path):
|
|
258
|
+
"""
|
|
259
|
+
Compare whether the content of two files are the same.
|
|
260
|
+
multi-app:7f35355e-02a6-45b5-b140-f0be698bcf85
|
|
261
|
+
"""
|
|
262
|
+
if not src_path or not tgt_path:
|
|
263
|
+
return 0.0
|
|
264
|
+
|
|
265
|
+
with open(src_path, 'r') as f:
|
|
266
|
+
src_content = f.read().strip()
|
|
267
|
+
with open(tgt_path, 'r') as f:
|
|
268
|
+
tgt_content = f.read().strip()
|
|
269
|
+
try:
|
|
270
|
+
# Compare the content as numbers
|
|
271
|
+
tgt_content_num = float(tgt_content)
|
|
272
|
+
if tgt_content in src_content:
|
|
273
|
+
# If the content of tgt is in src, return 1.0 since output src might be
|
|
274
|
+
# a superset(language description+number) of tgt
|
|
275
|
+
return 1.0
|
|
276
|
+
src_content_num = float(src_content)
|
|
277
|
+
if abs(src_content_num - tgt_content_num) < 1e-4:
|
|
278
|
+
return 1.0
|
|
279
|
+
return 0.0
|
|
280
|
+
except:
|
|
281
|
+
if src_content == tgt_content:
|
|
282
|
+
return 1.0
|
|
283
|
+
return 0.0
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from desktop_env.providers.base import VMManager, Provider
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_vm_manager_and_provider(provider_name: str, region: str, use_proxy: bool = False):
|
|
5
|
+
"""
|
|
6
|
+
Factory function to get the Virtual Machine Manager and Provider instances based on the provided provider name.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
provider_name (str): The name of the provider (e.g., "aws", "vmware", etc.)
|
|
10
|
+
region (str): The region for the provider
|
|
11
|
+
use_proxy (bool): Whether to use proxy-enabled providers (currently only supported for AWS)
|
|
12
|
+
"""
|
|
13
|
+
provider_name = provider_name.lower().strip()
|
|
14
|
+
if provider_name == "vmware":
|
|
15
|
+
from desktop_env.providers.vmware.manager import VMwareVMManager
|
|
16
|
+
from desktop_env.providers.vmware.provider import VMwareProvider
|
|
17
|
+
return VMwareVMManager(), VMwareProvider(region)
|
|
18
|
+
elif provider_name == "virtualbox":
|
|
19
|
+
from desktop_env.providers.virtualbox.manager import VirtualBoxVMManager
|
|
20
|
+
from desktop_env.providers.virtualbox.provider import VirtualBoxProvider
|
|
21
|
+
return VirtualBoxVMManager(), VirtualBoxProvider(region)
|
|
22
|
+
elif provider_name in ["aws", "amazon web services"]:
|
|
23
|
+
from desktop_env.providers.aws.manager import AWSVMManager
|
|
24
|
+
from desktop_env.providers.aws.provider import AWSProvider
|
|
25
|
+
return AWSVMManager(), AWSProvider(region)
|
|
26
|
+
elif provider_name == "azure":
|
|
27
|
+
from desktop_env.providers.azure.manager import AzureVMManager
|
|
28
|
+
from desktop_env.providers.azure.provider import AzureProvider
|
|
29
|
+
return AzureVMManager(), AzureProvider(region)
|
|
30
|
+
elif provider_name == "docker":
|
|
31
|
+
from desktop_env.providers.docker.manager import DockerVMManager
|
|
32
|
+
from desktop_env.providers.docker.provider import DockerProvider
|
|
33
|
+
return DockerVMManager(), DockerProvider(region)
|
|
34
|
+
else:
|
|
35
|
+
raise NotImplementedError(f"{provider_name} not implemented!")
|
|
File without changes
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from filelock import FileLock
|
|
3
|
+
import boto3
|
|
4
|
+
import psutil
|
|
5
|
+
import logging
|
|
6
|
+
import dotenv
|
|
7
|
+
import signal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
INSTANCE_TYPE = "t3.xlarge"
|
|
11
|
+
|
|
12
|
+
# Load environment variables from .env file
|
|
13
|
+
dotenv.load_dotenv()
|
|
14
|
+
|
|
15
|
+
# Ensure the AWS region is set in the environment
|
|
16
|
+
if not os.getenv('AWS_REGION'):
|
|
17
|
+
raise EnvironmentError("AWS_REGION must be set in the environment variables.")
|
|
18
|
+
|
|
19
|
+
# Ensure the AWS subnet and security group IDs are set in the environment
|
|
20
|
+
if not os.getenv('AWS_SUBNET_ID') or not os.getenv('AWS_SECURITY_GROUP_ID'):
|
|
21
|
+
raise EnvironmentError("AWS_SUBNET_ID and AWS_SECURITY_GROUP_ID must be set in the environment variables.")
|
|
22
|
+
|
|
23
|
+
from desktop_env.providers.base import VMManager
|
|
24
|
+
|
|
25
|
+
# Import proxy-related modules only when needed
|
|
26
|
+
try:
|
|
27
|
+
from desktop_env.providers.aws.proxy_pool import get_global_proxy_pool, init_proxy_pool
|
|
28
|
+
PROXY_SUPPORT_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
PROXY_SUPPORT_AVAILABLE = False
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("desktopenv.providers.aws.AWSVMManager")
|
|
33
|
+
logger.setLevel(logging.INFO)
|
|
34
|
+
|
|
35
|
+
DEFAULT_REGION = "us-east-1"
|
|
36
|
+
# todo: Add doc for the configuration of image, security group and network interface
|
|
37
|
+
# todo: public the AMI images
|
|
38
|
+
IMAGE_ID_MAP = {
|
|
39
|
+
"us-east-1": {
|
|
40
|
+
(1920, 1080): "ami-09138bff939f82bd8"
|
|
41
|
+
},
|
|
42
|
+
"ap-east-1": {
|
|
43
|
+
(1920, 1080): "ami-0c092a5b8be4116f5"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _allocate_vm(region=DEFAULT_REGION, screen_size=(1920, 1080)):
|
|
49
|
+
|
|
50
|
+
if region not in IMAGE_ID_MAP:
|
|
51
|
+
raise ValueError(f"Region {region} is not supported. Supported regions are: {list(IMAGE_ID_MAP.keys())}")
|
|
52
|
+
if screen_size not in IMAGE_ID_MAP[region]:
|
|
53
|
+
raise ValueError(f"Screen size {screen_size} not supported for region {region}. Supported: {list(IMAGE_ID_MAP[region].keys())}")
|
|
54
|
+
ami_id = IMAGE_ID_MAP[region][screen_size]
|
|
55
|
+
|
|
56
|
+
ec2_client = boto3.client('ec2', region_name=region)
|
|
57
|
+
instance_id = None
|
|
58
|
+
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
|
59
|
+
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
|
|
60
|
+
|
|
61
|
+
def signal_handler(sig, frame):
|
|
62
|
+
if instance_id:
|
|
63
|
+
signal_name = "SIGINT" if sig == signal.SIGINT else "SIGTERM"
|
|
64
|
+
logger.warning(f"Received {signal_name} signal, terminating instance {instance_id}...")
|
|
65
|
+
try:
|
|
66
|
+
ec2_client.terminate_instances(InstanceIds=[instance_id])
|
|
67
|
+
logger.info(f"Successfully terminated instance {instance_id} after {signal_name}.")
|
|
68
|
+
except Exception as cleanup_error:
|
|
69
|
+
logger.error(f"Failed to terminate instance {instance_id} after {signal_name}: {str(cleanup_error)}")
|
|
70
|
+
|
|
71
|
+
# Restore original signal handlers
|
|
72
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
|
73
|
+
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
|
74
|
+
|
|
75
|
+
# Raise appropriate exception based on signal type
|
|
76
|
+
if sig == signal.SIGINT:
|
|
77
|
+
raise KeyboardInterrupt
|
|
78
|
+
else:
|
|
79
|
+
# For SIGTERM, exit gracefully
|
|
80
|
+
import sys
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Set up signal handlers for both SIGINT and SIGTERM
|
|
85
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
86
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
87
|
+
|
|
88
|
+
if not os.getenv('AWS_SECURITY_GROUP_ID'):
|
|
89
|
+
raise ValueError("AWS_SECURITY_GROUP_ID is not set in the environment variables.")
|
|
90
|
+
if not os.getenv('AWS_SUBNET_ID'):
|
|
91
|
+
raise ValueError("AWS_SUBNET_ID is not set in the environment variables.")
|
|
92
|
+
|
|
93
|
+
run_instances_params = {
|
|
94
|
+
"MaxCount": 1,
|
|
95
|
+
"MinCount": 1,
|
|
96
|
+
"ImageId": ami_id,
|
|
97
|
+
"InstanceType": INSTANCE_TYPE,
|
|
98
|
+
"EbsOptimized": True,
|
|
99
|
+
"NetworkInterfaces": [
|
|
100
|
+
{
|
|
101
|
+
"SubnetId": os.getenv('AWS_SUBNET_ID'),
|
|
102
|
+
"AssociatePublicIpAddress": True,
|
|
103
|
+
"DeviceIndex": 0,
|
|
104
|
+
"Groups": [
|
|
105
|
+
os.getenv('AWS_SECURITY_GROUP_ID')
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"BlockDeviceMappings": [
|
|
110
|
+
{
|
|
111
|
+
"DeviceName": "/dev/sda1",
|
|
112
|
+
"Ebs": {
|
|
113
|
+
# "VolumeInitializationRate": 300
|
|
114
|
+
"VolumeSize": 30, # Size in GB
|
|
115
|
+
"VolumeType": "gp3", # General Purpose SSD
|
|
116
|
+
"Throughput": 1000,
|
|
117
|
+
"Iops": 4000 # Adjust IOPS as needed
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
response = ec2_client.run_instances(**run_instances_params)
|
|
124
|
+
instance_id = response['Instances'][0]['InstanceId']
|
|
125
|
+
|
|
126
|
+
waiter = ec2_client.get_waiter('instance_running')
|
|
127
|
+
logger.info(f"Waiting for instance {instance_id} to be running...")
|
|
128
|
+
waiter.wait(InstanceIds=[instance_id])
|
|
129
|
+
logger.info(f"Instance {instance_id} is ready.")
|
|
130
|
+
|
|
131
|
+
# 获取并显示VNC访问地址
|
|
132
|
+
try:
|
|
133
|
+
instance_details = ec2_client.describe_instances(InstanceIds=[instance_id])
|
|
134
|
+
instance = instance_details['Reservations'][0]['Instances'][0]
|
|
135
|
+
public_ip = instance.get('PublicIpAddress', '')
|
|
136
|
+
if public_ip:
|
|
137
|
+
vnc_url = f"http://{public_ip}:5910/vnc.html"
|
|
138
|
+
logger.info("="*80)
|
|
139
|
+
logger.info(f"🖥️ VNC Web Access URL: {vnc_url}")
|
|
140
|
+
logger.info(f"📡 Public IP: {public_ip}")
|
|
141
|
+
logger.info(f"🆔 Instance ID: {instance_id}")
|
|
142
|
+
logger.info("="*80)
|
|
143
|
+
print(f"\n🌐 VNC访问地址: {vnc_url}")
|
|
144
|
+
print(f"📍 请在浏览器中打开上述地址进行远程桌面访问\n")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning(f"Failed to get VNC address for instance {instance_id}: {e}")
|
|
147
|
+
except KeyboardInterrupt:
|
|
148
|
+
logger.warning("VM allocation interrupted by user (SIGINT).")
|
|
149
|
+
if instance_id:
|
|
150
|
+
logger.info(f"Terminating instance {instance_id} due to interruption.")
|
|
151
|
+
ec2_client.terminate_instances(InstanceIds=[instance_id])
|
|
152
|
+
raise
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"Failed to allocate VM: {e}", exc_info=True)
|
|
155
|
+
if instance_id:
|
|
156
|
+
logger.info(f"Terminating instance {instance_id} due to an error.")
|
|
157
|
+
ec2_client.terminate_instances(InstanceIds=[instance_id])
|
|
158
|
+
raise
|
|
159
|
+
finally:
|
|
160
|
+
# Restore original signal handlers
|
|
161
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
|
162
|
+
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
|
163
|
+
|
|
164
|
+
return instance_id
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _allocate_vm_with_proxy(region=DEFAULT_REGION, proxy_config_file=None, screen_size=(1920, 1080)):
|
|
168
|
+
"""Allocate a VM with proxy configuration"""
|
|
169
|
+
if not PROXY_SUPPORT_AVAILABLE:
|
|
170
|
+
logger.warning("Proxy support not available, falling back to regular VM allocation")
|
|
171
|
+
return _allocate_vm(region, screen_size=screen_size)
|
|
172
|
+
|
|
173
|
+
from desktop_env.providers.aws.provider_with_proxy import AWSProviderWithProxy
|
|
174
|
+
|
|
175
|
+
# Initialize proxy pool if needed
|
|
176
|
+
if proxy_config_file:
|
|
177
|
+
init_proxy_pool(proxy_config_file)
|
|
178
|
+
|
|
179
|
+
# Get current proxy
|
|
180
|
+
proxy_pool = get_global_proxy_pool()
|
|
181
|
+
current_proxy = proxy_pool.get_next_proxy()
|
|
182
|
+
|
|
183
|
+
if current_proxy:
|
|
184
|
+
logger.info(f"Allocating VM with proxy: {current_proxy.host}:{current_proxy.port}")
|
|
185
|
+
|
|
186
|
+
# Create provider instance
|
|
187
|
+
provider = AWSProviderWithProxy(region=region, proxy_config_file=proxy_config_file)
|
|
188
|
+
|
|
189
|
+
# Create new instance
|
|
190
|
+
instance_id = provider.create_instance_with_proxy(
|
|
191
|
+
image_id=IMAGE_ID_MAP[region],
|
|
192
|
+
instance_type=INSTANCE_TYPE,
|
|
193
|
+
security_groups=[os.getenv('AWS_SECURITY_GROUP_ID')],
|
|
194
|
+
subnet_id=os.getenv('AWS_SUBNET_ID')
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
ec2_client = boto3.client('ec2', region_name=region)
|
|
199
|
+
instance_details = ec2_client.describe_instances(InstanceIds=[instance_id])
|
|
200
|
+
instance = instance_details['Reservations'][0]['Instances'][0]
|
|
201
|
+
public_ip = instance.get('PublicIpAddress', '')
|
|
202
|
+
if public_ip:
|
|
203
|
+
vnc_url = f"http://{public_ip}:5910/vnc.html"
|
|
204
|
+
logger.info("="*80)
|
|
205
|
+
logger.info(f"🖥️ VNC Web Access URL: {vnc_url}")
|
|
206
|
+
logger.info(f"📡 Public IP: {public_ip}")
|
|
207
|
+
logger.info(f"🆔 Instance ID: {instance_id}")
|
|
208
|
+
if current_proxy:
|
|
209
|
+
logger.info(f"🌐 Proxy: {current_proxy.host}:{current_proxy.port}")
|
|
210
|
+
logger.info("="*80)
|
|
211
|
+
print(f"\n🌐 VNC Web Access URL: {vnc_url}")
|
|
212
|
+
if current_proxy:
|
|
213
|
+
print(f"🔄 Current Proxy: {current_proxy.host}:{current_proxy.port}")
|
|
214
|
+
print(f"📍 Please open the above address in the browser for remote desktop access\n")
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.warning(f"Failed to get VNC address for proxy instance {instance_id}: {e}")
|
|
217
|
+
|
|
218
|
+
return instance_id
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AWSVMManager(VMManager):
|
|
222
|
+
"""
|
|
223
|
+
AWS VM Manager for managing virtual machines on AWS.
|
|
224
|
+
|
|
225
|
+
AWS does not need to maintain a registry of VMs, as it can dynamically allocate and deallocate VMs.
|
|
226
|
+
This class supports both regular VM allocation and proxy-enabled VM allocation.
|
|
227
|
+
"""
|
|
228
|
+
def __init__(self, proxy_config_file=None, **kwargs):
|
|
229
|
+
self.proxy_config_file = proxy_config_file
|
|
230
|
+
# self.lock = FileLock(".aws_lck", timeout=60)
|
|
231
|
+
self.initialize_registry()
|
|
232
|
+
|
|
233
|
+
# Initialize proxy pool if proxy configuration is provided
|
|
234
|
+
if proxy_config_file and PROXY_SUPPORT_AVAILABLE:
|
|
235
|
+
init_proxy_pool(proxy_config_file)
|
|
236
|
+
logger.info(f"Proxy pool initialized with config: {proxy_config_file}")
|
|
237
|
+
|
|
238
|
+
def initialize_registry(self, **kwargs):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
def add_vm(self, vm_path, region=DEFAULT_REGION, lock_needed=True, **kwargs):
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
def _add_vm(self, vm_path, region=DEFAULT_REGION):
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
def delete_vm(self, vm_path, region=DEFAULT_REGION, lock_needed=True, **kwargs):
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
def _delete_vm(self, vm_path, region=DEFAULT_REGION):
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
def occupy_vm(self, vm_path, pid, region=DEFAULT_REGION, lock_needed=True, **kwargs):
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
def _occupy_vm(self, vm_path, pid, region=DEFAULT_REGION):
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
def check_and_clean(self, lock_needed=True, **kwargs):
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
def _check_and_clean(self):
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
def list_free_vms(self, region=DEFAULT_REGION, lock_needed=True, **kwargs):
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
def _list_free_vms(self, region=DEFAULT_REGION):
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
def get_vm_path(self, region=DEFAULT_REGION, screen_size=(1920, 1080), **kwargs):
|
|
272
|
+
if self.proxy_config_file:
|
|
273
|
+
logger.info("Allocating a new VM with proxy configuration in region: {}".format(region))
|
|
274
|
+
new_vm_path = _allocate_vm_with_proxy(region, self.proxy_config_file, screen_size=screen_size)
|
|
275
|
+
else:
|
|
276
|
+
logger.info("Allocating a new VM in region: {}".format(region))
|
|
277
|
+
new_vm_path = _allocate_vm(region, screen_size=screen_size)
|
|
278
|
+
return new_vm_path
|