benchlab-pytools 3.0.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.
- benchlab/__init__.py +14 -0
- benchlab/__main__.py +21 -0
- benchlab/config/README.md +325 -0
- benchlab/config/__init__.py +9 -0
- benchlab/config/config_client.py +631 -0
- benchlab/config/config_manager.py +495 -0
- benchlab/config/config_tool.py +358 -0
- benchlab/config/diff.py +163 -0
- benchlab/config/examples/complete_config.json +56 -0
- benchlab/config/examples/rgb_lighting.json +24 -0
- benchlab/config/examples/simple_fan.json +32 -0
- benchlab/config/schema.py +138 -0
- benchlab/core/README.md +154 -0
- benchlab/core/__init__.py +78 -0
- benchlab/core/config.py +145 -0
- benchlab/core/datasource.py +1209 -0
- benchlab/core/datasource_manager.py +418 -0
- benchlab/core/device_registry.py +206 -0
- benchlab/core/discovery.py +92 -0
- benchlab/core/infrastructure.py +309 -0
- benchlab/core/process_manager.py +356 -0
- benchlab/core/retry.py +85 -0
- benchlab/core/shared_serial.py +235 -0
- benchlab/core/statistics.py +224 -0
- benchlab/csv_log/README.md +206 -0
- benchlab/csv_log/__init__.py +5 -0
- benchlab/csv_log/csv_logger_enhanced.py +416 -0
- benchlab/csv_log/message_batcher.py +361 -0
- benchlab/csv_log/requirements.txt +3 -0
- benchlab/csv_log/smart_retry.py +466 -0
- benchlab/graph/README.md +117 -0
- benchlab/graph/__init__.py +5 -0
- benchlab/graph/app.py +353 -0
- benchlab/graph/requirements.txt +2 -0
- benchlab/graph/runner.py +42 -0
- benchlab/graph/ui.py +288 -0
- benchlab/hwinfo/README.md +135 -0
- benchlab/hwinfo/__init__.py +5 -0
- benchlab/hwinfo/hwinfo_export.py +386 -0
- benchlab/hwinfo/requirements.txt +1 -0
- benchlab/launcher.py +387 -0
- benchlab/link/.env.EXAMPLE +10 -0
- benchlab/link/README.md +172 -0
- benchlab/link/__init__.py +1 -0
- benchlab/link/link_main.py +479 -0
- benchlab/main.py +462 -0
- benchlab/menu.py +729 -0
- benchlab/menu_classic.py +410 -0
- benchlab/mqtt/README.md +201 -0
- benchlab/mqtt/__init__.py +5 -0
- benchlab/mqtt/mqtt.config_template +2 -0
- benchlab/mqtt/mqtt_publisher.py +540 -0
- benchlab/mqtt/requirements.txt +3 -0
- benchlab/prefs.py +67 -0
- benchlab/restapi/.env.example +17 -0
- benchlab/restapi/__init__.py +5 -0
- benchlab/restapi/favicon.ico +0 -0
- benchlab/restapi/readme.md +202 -0
- benchlab/restapi/requirements.txt +5 -0
- benchlab/restapi/telemetry_api.py +808 -0
- benchlab/sources.py +515 -0
- benchlab/tools.py +147 -0
- benchlab/tui/README.md +266 -0
- benchlab/tui/__init__.py +5 -0
- benchlab/tui/config.py +477 -0
- benchlab/tui/requirements.txt +2 -0
- benchlab/tui/tui_core.py +1298 -0
- benchlab/tui/tui_main.py +418 -0
- benchlab/vu/README.md +64 -0
- benchlab/vu/VU-Server/NOTICE.md +13 -0
- benchlab/vu/VU-Server/README.md +80 -0
- benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
- benchlab/vu/VU-Server/__init__.py +0 -0
- benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
- benchlab/vu/VU-Server/community_applications.md +13 -0
- benchlab/vu/VU-Server/config.yaml +9 -0
- benchlab/vu/VU-Server/database.py +256 -0
- benchlab/vu/VU-Server/dial_driver.py +534 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
- benchlab/vu/VU-Server/dials/__init__.py +0 -0
- benchlab/vu/VU-Server/dials/base_logger.py +100 -0
- benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
- benchlab/vu/VU-Server/installer/install.nsi +148 -0
- benchlab/vu/VU-Server/make_version.py +40 -0
- benchlab/vu/VU-Server/pylintrc +628 -0
- benchlab/vu/VU-Server/requirements.txt +9 -0
- benchlab/vu/VU-Server/serial_driver.py +244 -0
- benchlab/vu/VU-Server/server.py +670 -0
- benchlab/vu/VU-Server/server_config.py +255 -0
- benchlab/vu/VU-Server/server_dial_handler.py +334 -0
- benchlab/vu/VU-Server/vu_notifications.py +24 -0
- benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
- benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
- benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
- benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
- benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
- benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
- benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
- benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
- benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
- benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
- benchlab/vu/VU-Server/www/favicon.ico +0 -0
- benchlab/vu/VU-Server/www/favicon.png +0 -0
- benchlab/vu/VU-Server/www/index.html +47 -0
- benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
- benchlab/vu/VU-Server/www/views/dial.html +286 -0
- benchlab/vu/VU-Server/www/views/footer.html +27 -0
- benchlab/vu/VU-Server/www/views/header.html +86 -0
- benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
- benchlab/vu/VU-Server/www/views/start.html +124 -0
- benchlab/vu/__init__.py +5 -0
- benchlab/vu/assets/Barlow-Bold.ttf +0 -0
- benchlab/vu/assets/bl_dial_144x200.png +0 -0
- benchlab/vu/assets/bl_logo_144x200.png +0 -0
- benchlab/vu/devices.py +166 -0
- benchlab/vu/requirements.txt +24 -0
- benchlab/vu/sensors.py +18 -0
- benchlab/vu/vu_dial.config_template +90 -0
- benchlab/vu/vu_logo_gen.py +97 -0
- benchlab/vu/vu_server.config_template +5 -0
- benchlab/vu/vu_server_config.py +40 -0
- benchlab/vu/vu_server_manager.py +192 -0
- benchlab/vu/vu_tui.py +919 -0
- benchlab/vu/vu_updater.py +540 -0
- benchlab/wigidash/README.md +182 -0
- benchlab/wigidash/__init__.py +5 -0
- benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
- benchlab/wigidash/assets/Inter.ttf +0 -0
- benchlab/wigidash/assets/benchlab.png +0 -0
- benchlab/wigidash/benchlab_fleet.py +150 -0
- benchlab/wigidash/benchlab_graph.py +517 -0
- benchlab/wigidash/benchlab_overview.py +426 -0
- benchlab/wigidash/benchlab_telemetry.py +105 -0
- benchlab/wigidash/benchlab_ui.py +190 -0
- benchlab/wigidash/benchlab_utils.py +182 -0
- benchlab/wigidash/requirements.txt +9 -0
- benchlab/wigidash/wigidash_device.py +541 -0
- benchlab/wigidash/wigidash_manager.py +342 -0
- benchlab/wigidash/wigidash_session.py +355 -0
- benchlab/wigidash/wigidash_usb.py +247 -0
- benchlab/wigidash/wigidash_widget.py +88 -0
- benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
- benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
- benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
- benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
- benchlab_pytools-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# benchlab/vu/vu_server_config.py
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
config_path = Path(__file__).parent / "vu_server.config"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def update_vu_config(dial_uid, dial_name, device, sensor):
|
|
10
|
+
# Load existing config
|
|
11
|
+
if config_path.exists():
|
|
12
|
+
with open(config_path, "r") as f:
|
|
13
|
+
config = json.load(f)
|
|
14
|
+
else:
|
|
15
|
+
config = {
|
|
16
|
+
"vu_server_url": "http://localhost:5340",
|
|
17
|
+
"api_key": "",
|
|
18
|
+
"mappings": [],
|
|
19
|
+
"update_interval_sec": 1}
|
|
20
|
+
|
|
21
|
+
# Remove any mapping with the same dial_uid
|
|
22
|
+
existing = config.get("mappings", [])
|
|
23
|
+
filtered = [m for m in existing if m.get("dial_uid") != dial_uid]
|
|
24
|
+
|
|
25
|
+
# Add new mapping
|
|
26
|
+
filtered.append({
|
|
27
|
+
"dial_uid": dial_uid,
|
|
28
|
+
"dial_name": dial_name,
|
|
29
|
+
"benchlab_uid": device["uid"],
|
|
30
|
+
"benchlab_port": device["port"],
|
|
31
|
+
"sensor": sensor
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
# Update config
|
|
35
|
+
config["mappings"] = filtered
|
|
36
|
+
|
|
37
|
+
# Write back to file
|
|
38
|
+
with open(config_path, "w") as f:
|
|
39
|
+
json.dump(config, f, indent=2)
|
|
40
|
+
print("[INFO] vu_server.config updated.")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import requests
|
|
5
|
+
import yaml
|
|
6
|
+
import json
|
|
7
|
+
import signal
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
import platform
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
# -----------------------------------------------------------------------------
|
|
15
|
+
# Platform setup
|
|
16
|
+
# -----------------------------------------------------------------------------
|
|
17
|
+
IS_WINDOWS = platform.system() == "Windows"
|
|
18
|
+
PYTHON_CMD = sys.executable
|
|
19
|
+
CREATIONFLAGS = subprocess.CREATE_NEW_PROCESS_GROUP if IS_WINDOWS else 0
|
|
20
|
+
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
# Paths
|
|
23
|
+
# -----------------------------------------------------------------------------
|
|
24
|
+
BASE_DIR = Path(__file__).parent
|
|
25
|
+
VU_SERVER_DIR = BASE_DIR / "VU-Server"
|
|
26
|
+
VU_SERVER_CONFIG = BASE_DIR / "vu_server.config"
|
|
27
|
+
SERVER_YAML_CONFIG = VU_SERVER_DIR / "config.yaml"
|
|
28
|
+
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
# Logging
|
|
31
|
+
# -----------------------------------------------------------------------------
|
|
32
|
+
logger = logging.getLogger("vu_server_manager")
|
|
33
|
+
|
|
34
|
+
# -----------------------------------------------------------------------------
|
|
35
|
+
# Helpers
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_vu_server(server_url: str, api_key: str = "") -> bool:
|
|
40
|
+
"""Return True if the VU server responds successfully.
|
|
41
|
+
|
|
42
|
+
The server reads the API key as a `key` query parameter (see
|
|
43
|
+
server.py's BaseHandler.is_valid_api_key), not an HTTP header — match
|
|
44
|
+
that here so a correct api_key is actually validated by the server
|
|
45
|
+
instead of always being rejected and falling through to the 403
|
|
46
|
+
"still running" case below.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
# "localhost" resolves to both an IPv4 and IPv6 address on Windows,
|
|
50
|
+
# and requests/urllib3 tries each in turn, each getting its own
|
|
51
|
+
# full timeout budget — a "nothing listening" check with
|
|
52
|
+
# timeout=1 can silently take ~2s instead of 1s. Force IPv4
|
|
53
|
+
# loopback for the common "localhost" case so the timeout below
|
|
54
|
+
# is actually honored; leave any other configured hostname alone.
|
|
55
|
+
probe_url = server_url.replace("://localhost", "://127.0.0.1", 1)
|
|
56
|
+
params = {"key": api_key} if api_key else {}
|
|
57
|
+
r = requests.get(
|
|
58
|
+
f"{probe_url}/api/v0/dial/list",
|
|
59
|
+
params=params,
|
|
60
|
+
timeout=1)
|
|
61
|
+
# 403 (missing/invalid key) still means a real VU server answered —
|
|
62
|
+
# that's enough to know we shouldn't auto-start a second one.
|
|
63
|
+
return r.status_code in (200, 403)
|
|
64
|
+
except requests.RequestException:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def forward_logs(proc: subprocess.Popen):
|
|
69
|
+
"""Forward VU server stdout to the main logger."""
|
|
70
|
+
if not proc.stdout:
|
|
71
|
+
logger.warning("No stdout to forward from VU server.")
|
|
72
|
+
return
|
|
73
|
+
for line in proc.stdout:
|
|
74
|
+
if line := line.rstrip():
|
|
75
|
+
logger.info(f"[VU SERVER] {line}")
|
|
76
|
+
|
|
77
|
+
# -----------------------------------------------------------------------------
|
|
78
|
+
# Server startup
|
|
79
|
+
# -----------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def start_vu_server() -> subprocess.Popen | None:
|
|
83
|
+
"""
|
|
84
|
+
Ensure a VU server is running.
|
|
85
|
+
Returns a subprocess.Popen handle if a new server was started, or
|
|
86
|
+
None if already running.
|
|
87
|
+
"""
|
|
88
|
+
logger.info("Checking for existing VU server...")
|
|
89
|
+
|
|
90
|
+
# Load previous JSON config
|
|
91
|
+
server_cfg = {}
|
|
92
|
+
if VU_SERVER_CONFIG.exists():
|
|
93
|
+
try:
|
|
94
|
+
server_cfg = json.loads(VU_SERVER_CONFIG.read_text())
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.warning(f"Failed to read {VU_SERVER_CONFIG}: {e}")
|
|
97
|
+
|
|
98
|
+
url = server_cfg.get("vu_server_url", "http://localhost:5340")
|
|
99
|
+
api_key = server_cfg.get("api_key", "")
|
|
100
|
+
|
|
101
|
+
if check_vu_server(url, api_key):
|
|
102
|
+
logger.info(f"VU server already running at {url}")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
logger.info("No VU server found — starting one...")
|
|
106
|
+
|
|
107
|
+
# Load YAML config
|
|
108
|
+
try:
|
|
109
|
+
cfg = yaml.safe_load(SERVER_YAML_CONFIG.read_text())
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(f"Failed to read {SERVER_YAML_CONFIG}: {e}")
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
host = cfg.get("server", {}).get("hostname", "localhost")
|
|
115
|
+
port = cfg.get("server", {}).get("port", 5340)
|
|
116
|
+
master_key = cfg.get("server", {}).get("master_key", "")
|
|
117
|
+
|
|
118
|
+
new_cfg = {
|
|
119
|
+
"vu_server_url": f"http://{host}:{port}",
|
|
120
|
+
"api_key": master_key,
|
|
121
|
+
"logo_file": str(
|
|
122
|
+
server_cfg.get(
|
|
123
|
+
"logo_file",
|
|
124
|
+
"assets/bl_logo_144x200.png"))}
|
|
125
|
+
|
|
126
|
+
# Ensure VU-Server directory exists
|
|
127
|
+
if not VU_SERVER_DIR.exists():
|
|
128
|
+
logger.error(f"Missing server directory: {VU_SERVER_DIR}")
|
|
129
|
+
raise FileNotFoundError(VU_SERVER_DIR)
|
|
130
|
+
|
|
131
|
+
# Launch server
|
|
132
|
+
proc = subprocess.Popen(
|
|
133
|
+
[PYTHON_CMD, "-u", str(VU_SERVER_DIR / "server.py")],
|
|
134
|
+
cwd=str(VU_SERVER_DIR),
|
|
135
|
+
stdout=subprocess.PIPE,
|
|
136
|
+
stderr=subprocess.STDOUT,
|
|
137
|
+
bufsize=1,
|
|
138
|
+
text=True,
|
|
139
|
+
creationflags=CREATIONFLAGS,
|
|
140
|
+
preexec_fn=os.setsid if not IS_WINDOWS else None,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
threading.Thread(target=forward_logs, args=(proc,), daemon=True).start()
|
|
144
|
+
|
|
145
|
+
# Wait for server to become ready
|
|
146
|
+
for _ in range(10):
|
|
147
|
+
time.sleep(1)
|
|
148
|
+
if check_vu_server(new_cfg["vu_server_url"], new_cfg["api_key"]):
|
|
149
|
+
logger.info(
|
|
150
|
+
f"VU server is now running at {
|
|
151
|
+
new_cfg['vu_server_url']}")
|
|
152
|
+
break
|
|
153
|
+
else:
|
|
154
|
+
logger.error("Failed to verify VU server after startup.")
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
# Only persist the new config once the server is confirmed up, so a
|
|
158
|
+
# failed launch doesn't leave vu_server.config pointing at nothing.
|
|
159
|
+
try:
|
|
160
|
+
VU_SERVER_CONFIG.write_text(json.dumps(new_cfg, indent=2))
|
|
161
|
+
logger.info(
|
|
162
|
+
f"Updated {VU_SERVER_CONFIG} with {
|
|
163
|
+
new_cfg['vu_server_url']}")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning(f"Failed to write {VU_SERVER_CONFIG}: {e}")
|
|
166
|
+
|
|
167
|
+
return proc
|
|
168
|
+
|
|
169
|
+
# -----------------------------------------------------------------------------
|
|
170
|
+
# Shutdown
|
|
171
|
+
# -----------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def terminate_vu_server(proc: subprocess.Popen | None):
|
|
175
|
+
if not proc or proc.poll() is not None:
|
|
176
|
+
return
|
|
177
|
+
logger.info("Terminating auto-started VU server...")
|
|
178
|
+
try:
|
|
179
|
+
if IS_WINDOWS:
|
|
180
|
+
proc.send_signal(signal.CTRL_BREAK_EVENT)
|
|
181
|
+
else:
|
|
182
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGINT)
|
|
183
|
+
proc.wait(timeout=5)
|
|
184
|
+
except subprocess.TimeoutExpired:
|
|
185
|
+
logger.warning("VU server did not exit gracefully — force killing.")
|
|
186
|
+
try:
|
|
187
|
+
proc.kill()
|
|
188
|
+
proc.wait(timeout=5)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.warning(f"Failed to force-kill VU server: {e}")
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.warning(f"Failed to terminate VU server cleanly: {e}")
|