feat: add Silo_Diag connection diagnostics command
Add a diagnostics command that sequentially tests: - DNS resolution of the server hostname - Health endpoint (/health) - Readiness endpoint (/ready) - Auth endpoint (/auth/me) with token - SSE event stream (/events) Results are printed to the FreeCAD console with PASS/FAIL status. Closes #3
This commit is contained in:
@@ -48,6 +48,7 @@ class SiloWorkbench(FreeCADGui.Workbench):
|
||||
"Silo_Settings",
|
||||
"Silo_Auth",
|
||||
"Silo_StartPanel",
|
||||
"Silo_Diag",
|
||||
]
|
||||
|
||||
self.appendMenu("Silo", self.menu_commands)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
@@ -3191,6 +3192,139 @@ class Silo_StartPanel:
|
||||
return True
|
||||
|
||||
|
||||
class _DiagWorker(QtCore.QThread):
|
||||
"""Background worker that runs connectivity diagnostics."""
|
||||
|
||||
result = QtCore.Signal(str, bool, str) # (test_name, passed, detail)
|
||||
finished_all = QtCore.Signal()
|
||||
_HTTP_TIMEOUT = 10
|
||||
_DNS_TIMEOUT = 5
|
||||
|
||||
def run(self):
|
||||
api_url = _get_api_url()
|
||||
base_url = api_url.rstrip("/")
|
||||
if base_url.endswith("/api"):
|
||||
base_url = base_url[:-4]
|
||||
self._test_dns(base_url)
|
||||
self._test_http("Health", f"{base_url}/health", auth=False)
|
||||
self._test_http("Ready", f"{base_url}/ready", auth=False)
|
||||
self._test_http("Auth", f"{api_url.rstrip('/')}/auth/me", auth=True)
|
||||
self._test_sse(api_url)
|
||||
self.finished_all.emit()
|
||||
|
||||
def _test_dns(self, base_url):
|
||||
parsed = urllib.parse.urlparse(base_url)
|
||||
hostname = parsed.hostname
|
||||
if not hostname:
|
||||
self.result.emit("DNS", False, "no hostname in URL")
|
||||
return
|
||||
try:
|
||||
addrs = socket.getaddrinfo(
|
||||
hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM
|
||||
)
|
||||
first_ip = addrs[0][4][0] if addrs else "?"
|
||||
self.result.emit("DNS", True, f"{hostname} -> {first_ip}")
|
||||
except socket.gaierror as e:
|
||||
self.result.emit("DNS", False, f"{hostname}: {e}")
|
||||
except Exception as e:
|
||||
self.result.emit("DNS", False, str(e))
|
||||
|
||||
def _test_http(self, name, url, auth=False):
|
||||
try:
|
||||
headers = {}
|
||||
if auth:
|
||||
headers.update(_get_auth_headers())
|
||||
req = urllib.request.Request(url, headers=headers, method="GET")
|
||||
resp = urllib.request.urlopen(
|
||||
req, context=_get_ssl_context(), timeout=self._HTTP_TIMEOUT
|
||||
)
|
||||
code = resp.getcode()
|
||||
body = resp.read(4096).decode("utf-8", errors="replace").strip()
|
||||
detail = f"HTTP {code}"
|
||||
try:
|
||||
data = json.loads(body)
|
||||
if isinstance(data, dict):
|
||||
parts = []
|
||||
for key in ("status", "username", "role"):
|
||||
if key in data:
|
||||
parts.append(f"{key}={data[key]}")
|
||||
if parts:
|
||||
detail += f" ({', '.join(parts)})"
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
if len(body) <= 80:
|
||||
detail += f" {body}"
|
||||
self.result.emit(name, code < 400, detail)
|
||||
except urllib.error.HTTPError as e:
|
||||
self.result.emit(name, False, f"HTTP {e.code} {e.reason}")
|
||||
except urllib.error.URLError as e:
|
||||
self.result.emit(name, False, str(e.reason))
|
||||
except Exception as e:
|
||||
self.result.emit(name, False, str(e))
|
||||
|
||||
def _test_sse(self, api_url):
|
||||
url = f"{api_url.rstrip('/')}/events"
|
||||
try:
|
||||
headers = {"Accept": "text/event-stream"}
|
||||
headers.update(_get_auth_headers())
|
||||
req = urllib.request.Request(url, headers=headers, method="GET")
|
||||
resp = urllib.request.urlopen(
|
||||
req, context=_get_ssl_context(), timeout=self._HTTP_TIMEOUT
|
||||
)
|
||||
content_type = resp.headers.get("Content-Type", "")
|
||||
code = resp.getcode()
|
||||
resp.close()
|
||||
if "text/event-stream" in content_type:
|
||||
self.result.emit("SSE", True, f"HTTP {code} event-stream")
|
||||
else:
|
||||
self.result.emit("SSE", True, f"HTTP {code} (type: {content_type})")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code in (404, 501):
|
||||
self.result.emit("SSE", False, f"HTTP {e.code} (not supported)")
|
||||
else:
|
||||
self.result.emit("SSE", False, f"HTTP {e.code} {e.reason}")
|
||||
except urllib.error.URLError as e:
|
||||
self.result.emit("SSE", False, str(e.reason))
|
||||
except Exception as e:
|
||||
self.result.emit("SSE", False, str(e))
|
||||
|
||||
|
||||
class Silo_Diag:
|
||||
"""Command to run connection diagnostics."""
|
||||
|
||||
_worker = None
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Diagnostics",
|
||||
"ToolTip": "Test DNS, health, readiness, auth, and SSE connectivity",
|
||||
"Pixmap": _icon("info"),
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
if self._worker is not None and self._worker.isRunning():
|
||||
FreeCAD.Console.PrintWarning("Silo: diagnostics already running\n")
|
||||
return
|
||||
api_url = _get_api_url()
|
||||
FreeCAD.Console.PrintMessage(f"--- Silo Diagnostics ({api_url}) ---\n")
|
||||
self._worker = _DiagWorker()
|
||||
self._worker.result.connect(self._on_result)
|
||||
self._worker.finished_all.connect(self._on_finished)
|
||||
self._worker.start()
|
||||
|
||||
def _on_result(self, name, passed, detail):
|
||||
if passed:
|
||||
FreeCAD.Console.PrintMessage(f" PASS {name}: {detail}\n")
|
||||
else:
|
||||
FreeCAD.Console.PrintError(f" FAIL {name}: {detail}\n")
|
||||
|
||||
def _on_finished(self):
|
||||
FreeCAD.Console.PrintMessage("--- Diagnostics complete ---\n")
|
||||
self._worker = None
|
||||
|
||||
def IsActive(self):
|
||||
return True
|
||||
|
||||
|
||||
# Register commands
|
||||
FreeCADGui.addCommand("Silo_Open", Silo_Open())
|
||||
FreeCADGui.addCommand("Silo_New", Silo_New())
|
||||
@@ -3207,3 +3341,4 @@ FreeCADGui.addCommand("Silo_Settings", Silo_Settings())
|
||||
|
||||
FreeCADGui.addCommand("Silo_Auth", Silo_Auth())
|
||||
FreeCADGui.addCommand("Silo_StartPanel", Silo_StartPanel())
|
||||
FreeCADGui.addCommand("Silo_Diag", Silo_Diag())
|
||||
|
||||
Reference in New Issue
Block a user