myagent-ai 1.47.25 → 1.47.27
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.
- package/aiskills/browser_stealth.py +269 -9
- package/aiskills/chromedev_mcp.py +47 -19
- package/core/vnc_manager.py +1326 -137
- package/main.py +19 -1
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/start.js +5 -1
- package/tray_manager.py +20 -2
- package/web/ui/admin/admin-sites.js +6 -15
- package/web/ui/chat/chat_container.html +2 -2
|
@@ -34,6 +34,7 @@ import json
|
|
|
34
34
|
import os
|
|
35
35
|
import shutil
|
|
36
36
|
import subprocess
|
|
37
|
+
import sys
|
|
37
38
|
import threading
|
|
38
39
|
import time
|
|
39
40
|
from pathlib import Path
|
|
@@ -880,12 +881,29 @@ class StealthBrowser:
|
|
|
880
881
|
capture_output=True, text=True, timeout=5,
|
|
881
882
|
)
|
|
882
883
|
if result.returncode == 0 and result.stdout.strip():
|
|
883
|
-
|
|
884
|
+
pid = result.stdout.strip().split()[0]
|
|
885
|
+
logger.info(f"检测到已有 Firefox 运行 (PID: {pid}),复用")
|
|
886
|
+
|
|
887
|
+
# [v1.47.26] 修正 profile 目录:已有 Firefox 可能用的是 VNC manager 启动的
|
|
888
|
+
# 默认 profile (~/.mozilla/firefox/default),而不是 browser_profile_manager 路径
|
|
889
|
+
# 读取 Firefox 的命令行参数来确定实际使用的 profile
|
|
890
|
+
actual_profile = self._detect_firefox_profile(pid)
|
|
891
|
+
if actual_profile:
|
|
892
|
+
logger.info(f"检测到已有 Firefox 使用的 profile: {actual_profile}")
|
|
893
|
+
self._firefox_profile_dir = actual_profile
|
|
894
|
+
else:
|
|
895
|
+
# 从 profiles.ini 推断默认 profile
|
|
896
|
+
default_profile = self._get_firefox_default_profile()
|
|
897
|
+
if default_profile:
|
|
898
|
+
logger.info(f"使用 Firefox 默认 profile: {default_profile}")
|
|
899
|
+
self._firefox_profile_dir = default_profile
|
|
900
|
+
|
|
884
901
|
self._started = True
|
|
885
902
|
return SkillResult(
|
|
886
903
|
success=True,
|
|
887
|
-
message=f"Firefox 已在 VNC 中运行 (Profile: {self.profile_name})",
|
|
888
|
-
data={"profile": self.profile_name, "mode": "firefox_vnc", "reused": True
|
|
904
|
+
message=f"Firefox 已在 VNC 中运行 (Profile: {self.profile_name}, 实际目录: {self._firefox_profile_dir})",
|
|
905
|
+
data={"profile": self.profile_name, "mode": "firefox_vnc", "reused": True,
|
|
906
|
+
"firefox_profile_dir": self._firefox_profile_dir},
|
|
889
907
|
)
|
|
890
908
|
except Exception:
|
|
891
909
|
pass
|
|
@@ -2097,10 +2115,27 @@ class StealthBrowser:
|
|
|
2097
2115
|
)
|
|
2098
2116
|
|
|
2099
2117
|
def _firefox_screenshot(self, save_path: str = "") -> SkillResult:
|
|
2100
|
-
"""Firefox+VNC
|
|
2118
|
+
"""Firefox+VNC 模式下截图。
|
|
2119
|
+
|
|
2120
|
+
截图方法优先级(纯 Python 优先,无需外部工具):
|
|
2121
|
+
1. python-xlib + Pillow — 纯 Python X11 协议截图,proot 下最可靠
|
|
2122
|
+
2. ImageMagick import — 外部命令,需安装 imagemagick
|
|
2123
|
+
3. scrot — 轻量截图工具
|
|
2124
|
+
4. xwd + convert — 需要 xwd + imagemagick
|
|
2125
|
+
5. xwd + ffmpeg — 需要 xwd + ffmpeg
|
|
2126
|
+
6. xwd + Pillow — 需要 xwd,Pillow 转换
|
|
2127
|
+
"""
|
|
2101
2128
|
display = os.environ.get("DISPLAY", ":99")
|
|
2102
2129
|
env = {**os.environ, "DISPLAY": display}
|
|
2103
2130
|
|
|
2131
|
+
# [v1.47.26] 检测 python-xlib 是否可用(用于截图方法和错误报告)
|
|
2132
|
+
_xlib_available = False
|
|
2133
|
+
try:
|
|
2134
|
+
from Xlib import display as _xd # noqa: F401
|
|
2135
|
+
_xlib_available = True
|
|
2136
|
+
except ImportError:
|
|
2137
|
+
pass
|
|
2138
|
+
|
|
2104
2139
|
try:
|
|
2105
2140
|
if not save_path:
|
|
2106
2141
|
from core.browser_profile import get_browser_profile_manager
|
|
@@ -2109,7 +2144,40 @@ class StealthBrowser:
|
|
|
2109
2144
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
2110
2145
|
save_path = str(save_dir / f"firefox_{timestamp}.png")
|
|
2111
2146
|
|
|
2112
|
-
# 方法1:
|
|
2147
|
+
# 方法1: python-xlib + Pillow — 纯 Python X11 协议截图(无需外部工具)
|
|
2148
|
+
# [v1.47.26] 这是 proot 环境下最可靠的方法,不依赖 import/scrot/xwd 等外部命令
|
|
2149
|
+
try:
|
|
2150
|
+
from Xlib import display as xdisplay
|
|
2151
|
+
from PIL import Image as PILImage
|
|
2152
|
+
|
|
2153
|
+
disp = xdisplay.Display(os.environ.get("DISPLAY", ":99"))
|
|
2154
|
+
screen = disp.screen()
|
|
2155
|
+
root = screen.root
|
|
2156
|
+
geom = root.get_geometry()
|
|
2157
|
+
width = geom.width
|
|
2158
|
+
height = geom.height
|
|
2159
|
+
|
|
2160
|
+
# XGetImage 获取原始像素数据
|
|
2161
|
+
raw = root.get_image(0, 0, width, height, xdisplay.X.ZPixmap, 0xffffffff)
|
|
2162
|
+
# raw.data 是 bytes,格式为 BGRX(每像素4字节)
|
|
2163
|
+
img = PILImage.frombytes("RGB", (width, height), raw.data, "raw", "BGRX")
|
|
2164
|
+
img.save(save_path, "PNG")
|
|
2165
|
+
|
|
2166
|
+
if os.path.isfile(save_path):
|
|
2167
|
+
logger.info(f"Firefox 截图已保存 (python-xlib): {save_path}")
|
|
2168
|
+
return SkillResult(
|
|
2169
|
+
success=True,
|
|
2170
|
+
message="Firefox+VNC 截图已保存 (python-xlib)",
|
|
2171
|
+
data={"path": save_path},
|
|
2172
|
+
)
|
|
2173
|
+
else:
|
|
2174
|
+
logger.warning("[_firefox_screenshot] python-xlib 截图保存失败")
|
|
2175
|
+
except ImportError:
|
|
2176
|
+
logger.debug("[_firefox_screenshot] python-xlib 未安装,跳过纯 Python 截图")
|
|
2177
|
+
except Exception as xlib_err:
|
|
2178
|
+
logger.warning(f"[_firefox_screenshot] python-xlib 截图失败: {xlib_err}")
|
|
2179
|
+
|
|
2180
|
+
# 方法2: 使用 ImageMagick import 截取整个屏幕
|
|
2113
2181
|
import_cmd = shutil.which("import")
|
|
2114
2182
|
if import_cmd:
|
|
2115
2183
|
result = subprocess.run(
|
|
@@ -2240,20 +2308,180 @@ class StealthBrowser:
|
|
|
2240
2308
|
pass
|
|
2241
2309
|
|
|
2242
2310
|
_available = []
|
|
2311
|
+
_available.append("python-xlib" if _xlib_available else "python-xlib❌")
|
|
2243
2312
|
if import_cmd: _available.append("import")
|
|
2244
2313
|
if scrot_cmd: _available.append("scrot")
|
|
2245
2314
|
if xwd_cmd and convert_cmd: _available.append("xwd+convert")
|
|
2246
2315
|
if xwd_cmd and ffmpeg_cmd: _available.append("xwd+ffmpeg")
|
|
2247
|
-
_tried = ", ".join(_available)
|
|
2316
|
+
_tried = ", ".join(_available)
|
|
2317
|
+
|
|
2318
|
+
# [v1.47.26] 自动安装 python-xlib(如果未安装)
|
|
2319
|
+
if not _xlib_available:
|
|
2320
|
+
logger.info("[_firefox_screenshot] 尝试自动安装 python-xlib ...")
|
|
2321
|
+
try:
|
|
2322
|
+
result = subprocess.run(
|
|
2323
|
+
[sys.executable, "-m", "pip", "install", "python-xlib", "-q"],
|
|
2324
|
+
capture_output=True, timeout=60,
|
|
2325
|
+
)
|
|
2326
|
+
if result.returncode == 0:
|
|
2327
|
+
logger.info("[_firefox_screenshot] python-xlib 安装成功,重试截图")
|
|
2328
|
+
try:
|
|
2329
|
+
from Xlib import display as xdisplay
|
|
2330
|
+
from PIL import Image as PILImage
|
|
2331
|
+
disp = xdisplay.Display(os.environ.get("DISPLAY", ":99"))
|
|
2332
|
+
screen = disp.screen()
|
|
2333
|
+
root = screen.root
|
|
2334
|
+
geom = root.get_geometry()
|
|
2335
|
+
raw = root.get_image(0, 0, geom.width, geom.height, xdisplay.X.ZPixmap, 0xffffffff)
|
|
2336
|
+
img = PILImage.frombytes("RGB", (geom.width, geom.height), raw.data, "raw", "BGRX")
|
|
2337
|
+
img.save(save_path, "PNG")
|
|
2338
|
+
if os.path.isfile(save_path):
|
|
2339
|
+
logger.info(f"Firefox 截图已保存 (python-xlib 安装后): {save_path}")
|
|
2340
|
+
return SkillResult(
|
|
2341
|
+
success=True,
|
|
2342
|
+
message="Firefox+VNC 截图已保存 (python-xlib)",
|
|
2343
|
+
data={"path": save_path},
|
|
2344
|
+
)
|
|
2345
|
+
except Exception as retry_err:
|
|
2346
|
+
logger.warning(f"[_firefox_screenshot] python-xlib 安装后截图仍失败: {retry_err}")
|
|
2347
|
+
else:
|
|
2348
|
+
logger.warning(f"[_firefox_screenshot] python-xlib 安装失败: {result.stderr.decode(errors='ignore')[:200]}")
|
|
2349
|
+
except Exception as pip_err:
|
|
2350
|
+
logger.warning(f"[_firefox_screenshot] pip install python-xlib 异常: {pip_err}")
|
|
2248
2351
|
|
|
2249
2352
|
return SkillResult(
|
|
2250
2353
|
success=False,
|
|
2251
2354
|
error=f"Firefox+VNC 截图失败 (尝试过: {_tried}) "
|
|
2252
|
-
f"— 需要
|
|
2355
|
+
f"— 需要 python-xlib (pip install python-xlib) 或 ImageMagick/scrot",
|
|
2253
2356
|
)
|
|
2254
2357
|
except Exception as e:
|
|
2255
2358
|
return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
|
|
2256
2359
|
|
|
2360
|
+
@staticmethod
|
|
2361
|
+
def _detect_firefox_profile(pid: str) -> Optional[str]:
|
|
2362
|
+
"""[v1.47.26] 从运行中 Firefox 进程的命令行参数检测其 profile 目录。
|
|
2363
|
+
|
|
2364
|
+
读取 /proc/<pid>/cmdline 获取 Firefox 启动参数,查找 --profile 或
|
|
2365
|
+
从 --user-data-dir 推断 profile 位置。
|
|
2366
|
+
|
|
2367
|
+
Args:
|
|
2368
|
+
pid: Firefox 进程 PID
|
|
2369
|
+
|
|
2370
|
+
Returns:
|
|
2371
|
+
Firefox profile 目录路径,检测失败返回 None
|
|
2372
|
+
"""
|
|
2373
|
+
try:
|
|
2374
|
+
cmdline_path = f"/proc/{pid}/cmdline"
|
|
2375
|
+
if not os.path.isfile(cmdline_path):
|
|
2376
|
+
return None
|
|
2377
|
+
|
|
2378
|
+
with open(cmdline_path, "rb") as f:
|
|
2379
|
+
cmdline_data = f.read()
|
|
2380
|
+
|
|
2381
|
+
# /proc/pid/cmdline 用 \0 分隔参数
|
|
2382
|
+
args = cmdline_data.decode("utf-8", errors="replace").split("\0")
|
|
2383
|
+
args = [a for a in args if a.strip()]
|
|
2384
|
+
|
|
2385
|
+
# 查找 --profile 参数
|
|
2386
|
+
for i, arg in enumerate(args):
|
|
2387
|
+
if arg == "--profile" and i + 1 < len(args):
|
|
2388
|
+
profile_dir = args[i + 1]
|
|
2389
|
+
if os.path.isdir(profile_dir):
|
|
2390
|
+
logger.info(f"[_detect_firefox_profile] 从 cmdline 找到 --profile: {profile_dir}")
|
|
2391
|
+
return profile_dir
|
|
2392
|
+
elif arg.startswith("--profile="):
|
|
2393
|
+
profile_dir = arg.split("=", 1)[1]
|
|
2394
|
+
if os.path.isdir(profile_dir):
|
|
2395
|
+
logger.info(f"[_detect_firefox_profile] 从 cmdline 找到 --profile=: {profile_dir}")
|
|
2396
|
+
return profile_dir
|
|
2397
|
+
|
|
2398
|
+
# 没有 --profile 参数 → Firefox 使用 profiles.ini 中的默认 profile
|
|
2399
|
+
return None
|
|
2400
|
+
except Exception as e:
|
|
2401
|
+
logger.debug(f"[_detect_firefox_profile] 检测失败: {e}")
|
|
2402
|
+
return None
|
|
2403
|
+
|
|
2404
|
+
@staticmethod
|
|
2405
|
+
def _get_firefox_default_profile() -> Optional[str]:
|
|
2406
|
+
"""[v1.47.26] 从 profiles.ini 获取 Firefox 默认 profile 目录路径。
|
|
2407
|
+
|
|
2408
|
+
Firefox 的 profiles.ini 格式:
|
|
2409
|
+
[Profile0]
|
|
2410
|
+
Name=default
|
|
2411
|
+
IsRelative=1
|
|
2412
|
+
Path=default
|
|
2413
|
+
Default=1
|
|
2414
|
+
|
|
2415
|
+
[General]
|
|
2416
|
+
StartWithLastProfile=1
|
|
2417
|
+
|
|
2418
|
+
如果 profiles.ini 不存在或解析失败,尝试直接检测
|
|
2419
|
+
~/.mozilla/firefox/default 目录。
|
|
2420
|
+
|
|
2421
|
+
Returns:
|
|
2422
|
+
Firefox 默认 profile 目录的绝对路径,失败返回 None
|
|
2423
|
+
"""
|
|
2424
|
+
ff_base = os.path.expanduser("~/.mozilla/firefox")
|
|
2425
|
+
profiles_ini = os.path.join(ff_base, "profiles.ini")
|
|
2426
|
+
|
|
2427
|
+
# 方法1: 解析 profiles.ini
|
|
2428
|
+
if os.path.isfile(profiles_ini):
|
|
2429
|
+
try:
|
|
2430
|
+
import configparser
|
|
2431
|
+
config = configparser.ConfigParser()
|
|
2432
|
+
config.read(profiles_ini)
|
|
2433
|
+
|
|
2434
|
+
# 查找标记为 Default=1 的 profile
|
|
2435
|
+
for section in config.sections():
|
|
2436
|
+
if section.startswith("Profile"):
|
|
2437
|
+
is_default = config.get(section, "Default", fallback="")
|
|
2438
|
+
is_relative = config.get(section, "IsRelative", fallback="1")
|
|
2439
|
+
path = config.get(section, "Path", fallback="")
|
|
2440
|
+
|
|
2441
|
+
if is_default == "1" and path:
|
|
2442
|
+
if is_relative == "1":
|
|
2443
|
+
full_path = os.path.join(ff_base, path)
|
|
2444
|
+
else:
|
|
2445
|
+
full_path = path
|
|
2446
|
+
if os.path.isdir(full_path):
|
|
2447
|
+
logger.info(f"[_get_firefox_default_profile] profiles.ini Default profile: {full_path}")
|
|
2448
|
+
return full_path
|
|
2449
|
+
|
|
2450
|
+
# 没有 Default=1,取第一个 Profile section
|
|
2451
|
+
for section in config.sections():
|
|
2452
|
+
if section.startswith("Profile"):
|
|
2453
|
+
path = config.get(section, "Path", fallback="")
|
|
2454
|
+
is_relative = config.get(section, "IsRelative", fallback="1")
|
|
2455
|
+
if path:
|
|
2456
|
+
full_path = os.path.join(ff_base, path) if is_relative == "1" else path
|
|
2457
|
+
if os.path.isdir(full_path):
|
|
2458
|
+
logger.info(f"[_get_firefox_default_profile] profiles.ini 第一个 profile: {full_path}")
|
|
2459
|
+
return full_path
|
|
2460
|
+
except Exception as e:
|
|
2461
|
+
logger.debug(f"[_get_firefox_default_profile] profiles.ini 解析失败: {e}")
|
|
2462
|
+
|
|
2463
|
+
# 方法2: 直接检测 ~/.mozilla/firefox/default 目录
|
|
2464
|
+
default_dir = os.path.join(ff_base, "default")
|
|
2465
|
+
if os.path.isdir(default_dir):
|
|
2466
|
+
logger.info(f"[_get_firefox_default_profile] 直接找到 default 目录: {default_dir}")
|
|
2467
|
+
return default_dir
|
|
2468
|
+
|
|
2469
|
+
# 方法3: 扫描所有 Firefox profile 子目录
|
|
2470
|
+
if os.path.isdir(ff_base):
|
|
2471
|
+
try:
|
|
2472
|
+
for entry in sorted(os.listdir(ff_base)):
|
|
2473
|
+
entry_path = os.path.join(ff_base, entry)
|
|
2474
|
+
if not os.path.isdir(entry_path):
|
|
2475
|
+
continue
|
|
2476
|
+
# Firefox profile 目录特征:包含 prefs.js
|
|
2477
|
+
if os.path.isfile(os.path.join(entry_path, "prefs.js")):
|
|
2478
|
+
logger.info(f"[_get_firefox_default_profile] 扫描找到 profile: {entry_path}")
|
|
2479
|
+
return entry_path
|
|
2480
|
+
except Exception:
|
|
2481
|
+
pass
|
|
2482
|
+
|
|
2483
|
+
return None
|
|
2484
|
+
|
|
2257
2485
|
def _firefox_read_sessionstore(self, max_wait: float = 5.0) -> dict:
|
|
2258
2486
|
"""[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
|
|
2259
2487
|
|
|
@@ -2270,8 +2498,33 @@ class StealthBrowser:
|
|
|
2270
2498
|
|
|
2271
2499
|
# 搜索可能的 sessionstore 路径
|
|
2272
2500
|
search_dirs = []
|
|
2273
|
-
|
|
2501
|
+
|
|
2502
|
+
# [v1.47.26] 优先搜索 Firefox 实际使用的 profile 目录
|
|
2503
|
+
# 先尝试从运行中 Firefox 进程检测实际 profile
|
|
2504
|
+
actual_profile = None
|
|
2505
|
+
try:
|
|
2506
|
+
result = subprocess.run(
|
|
2507
|
+
["pgrep", "-f", "firefox"],
|
|
2508
|
+
capture_output=True, text=True, timeout=3,
|
|
2509
|
+
)
|
|
2510
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
2511
|
+
pid = result.stdout.strip().split()[0]
|
|
2512
|
+
actual_profile = self._detect_firefox_profile(pid)
|
|
2513
|
+
except Exception:
|
|
2514
|
+
pass
|
|
2515
|
+
|
|
2516
|
+
if actual_profile and os.path.isdir(actual_profile) and actual_profile not in search_dirs:
|
|
2517
|
+
search_dirs.append(actual_profile)
|
|
2518
|
+
logger.info(f"[_firefox_read_sessionstore] 搜索实际 Firefox profile: {actual_profile}")
|
|
2519
|
+
|
|
2520
|
+
# 也从 profiles.ini 获取默认 profile
|
|
2521
|
+
default_profile = self._get_firefox_default_profile()
|
|
2522
|
+
if default_profile and os.path.isdir(default_profile) and default_profile not in search_dirs:
|
|
2523
|
+
search_dirs.append(default_profile)
|
|
2524
|
+
|
|
2525
|
+
if self._firefox_profile_dir and os.path.isdir(self._firefox_profile_dir) and self._firefox_profile_dir not in search_dirs:
|
|
2274
2526
|
search_dirs.append(self._firefox_profile_dir)
|
|
2527
|
+
|
|
2275
2528
|
# 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
|
|
2276
2529
|
ff_base = os.path.expanduser("~/.mozilla/firefox")
|
|
2277
2530
|
for extra in [
|
|
@@ -2280,7 +2533,8 @@ class StealthBrowser:
|
|
|
2280
2533
|
]:
|
|
2281
2534
|
if os.path.isdir(extra) and extra not in search_dirs:
|
|
2282
2535
|
search_dirs.append(extra)
|
|
2283
|
-
|
|
2536
|
+
|
|
2537
|
+
# 搜索所有 Firefox profile 子目录(如 xxx.default-release)
|
|
2284
2538
|
try:
|
|
2285
2539
|
if os.path.isdir(ff_base):
|
|
2286
2540
|
for entry in os.listdir(ff_base):
|
|
@@ -2293,6 +2547,12 @@ class StealthBrowser:
|
|
|
2293
2547
|
except Exception:
|
|
2294
2548
|
pass
|
|
2295
2549
|
|
|
2550
|
+
# [v1.47.26] 详细记录搜索路径
|
|
2551
|
+
logger.info(
|
|
2552
|
+
f"[_firefox_read_sessionstore] 搜索 {len(search_dirs)} 个目录: "
|
|
2553
|
+
f"{[d.replace(os.path.expanduser('~'), '~') for d in search_dirs]}"
|
|
2554
|
+
)
|
|
2555
|
+
|
|
2296
2556
|
# [v1.47.24] 等待 Firefox 写入 sessionstore(刚导航完可能还没写入)
|
|
2297
2557
|
_start_time = time.time()
|
|
2298
2558
|
while True:
|
|
@@ -189,27 +189,55 @@ def _ensure_display() -> Optional[str]:
|
|
|
189
189
|
logger.info(f"VNC 远程桌面已在运行,复用显示: {display}")
|
|
190
190
|
return display
|
|
191
191
|
|
|
192
|
-
# VNC 未运行 → 尝试自动启动
|
|
192
|
+
# [v1.47.13] VNC 未运行 → 尝试自动启动
|
|
193
|
+
# 修复: 不能在 ThreadPoolExecutor + asyncio.run 中直接调用 vnc.start()!
|
|
194
|
+
# 因为 vnc.start() 内部用 asyncio.Lock(),跨 event loop 会导致死锁。
|
|
195
|
+
# 正确做法:在单独线程中用 asyncio.run 启动,然后轮询等待 VNC 就绪。
|
|
193
196
|
try:
|
|
194
197
|
import asyncio
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
198
|
+
import concurrent.futures
|
|
199
|
+
|
|
200
|
+
def _start_vnc_in_thread():
|
|
201
|
+
try:
|
|
202
|
+
return asyncio.run(vnc.start())
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return {"success": False, "message": str(e)}
|
|
205
|
+
|
|
206
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
207
|
+
future = pool.submit(_start_vnc_in_thread)
|
|
208
|
+
|
|
209
|
+
max_wait = 60
|
|
210
|
+
check_interval = 2
|
|
211
|
+
elapsed = 0
|
|
212
|
+
while elapsed < max_wait:
|
|
213
|
+
if vnc.is_running:
|
|
214
|
+
display = vnc.display
|
|
215
|
+
os.environ["DISPLAY"] = display
|
|
216
|
+
logger.info(f"VNC 远程桌面已启动,复用显示: {display}")
|
|
217
|
+
return display
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
result = future.result(timeout=0.1)
|
|
221
|
+
if result.get("success") or vnc.is_running:
|
|
222
|
+
display = vnc.display
|
|
223
|
+
os.environ["DISPLAY"] = display
|
|
224
|
+
logger.info(f"VNC 远程桌面已启动,显示: {display}")
|
|
225
|
+
return display
|
|
226
|
+
else:
|
|
227
|
+
logger.warning(f"VNC 启动失败: {result.get('message', '')}")
|
|
228
|
+
break
|
|
229
|
+
except concurrent.futures.TimeoutError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
import time
|
|
233
|
+
time.sleep(check_interval)
|
|
234
|
+
elapsed += check_interval
|
|
235
|
+
|
|
236
|
+
if vnc.is_running:
|
|
237
|
+
display = vnc.display
|
|
238
|
+
os.environ["DISPLAY"] = display
|
|
239
|
+
return display
|
|
240
|
+
|
|
213
241
|
except Exception as e:
|
|
214
242
|
logger.warning(f"VNC 自动启动异常: {e}")
|
|
215
243
|
|