Files
pyTorrent/pytorrent/services/rtorrent/diagnostics.py
2026-05-19 13:43:37 +00:00

119 lines
4.8 KiB
Python

from __future__ import annotations
from .client import *
import shlex
def scgi_diagnostics(profile: dict) -> dict:
c = client_for(profile)
started = time.perf_counter()
body = dumps((), methodname="system.client_version", allow_none=True).encode("utf-8")
headers = {
"CONTENT_LENGTH": str(len(body)),
"SCGI": "1",
"REQUEST_METHOD": "POST",
"REQUEST_URI": c.path,
"SCRIPT_NAME": c.path,
"SERVER_PROTOCOL": "HTTP/1.1",
"CONTENT_TYPE": "text/xml",
}
header_blob = b"".join(k.encode() + b"\0" + v.encode() + b"\0" for k, v in headers.items())
payload = str(len(header_blob)).encode("ascii") + b":" + header_blob + b"," + body
metrics = {
"url": profile.get("scgi_url"),
"host": c.host,
"port": c.port,
"path": c.path,
"timeout_seconds": c.timeout,
"request_bytes": len(payload),
}
connect_started = time.perf_counter()
with socket.create_connection((c.host, c.port), timeout=c.timeout) as sock:
sock.settimeout(c.timeout)
metrics["connect_ms"] = round((time.perf_counter() - connect_started) * 1000, 2)
send_started = time.perf_counter()
sock.sendall(payload)
metrics["send_ms"] = round((time.perf_counter() - send_started) * 1000, 2)
chunks: list[bytes] = []
first_byte_at = None
while True:
chunk = sock.recv(65536)
if chunk and first_byte_at is None:
first_byte_at = time.perf_counter()
if not chunk:
break
chunks.append(chunk)
response = b"".join(chunks)
metrics["response_bytes"] = len(response)
metrics["first_byte_ms"] = round(((first_byte_at or time.perf_counter()) - started) * 1000, 2)
metrics["total_ms"] = round((time.perf_counter() - started) * 1000, 2)
if not response:
raise ConnectionError("Empty response from rTorrent SCGI")
xml_response = response
if b"\r\n\r\n" in xml_response:
xml_response = xml_response.split(b"\r\n\r\n", 1)[1]
elif b"\n\n" in xml_response:
xml_response = xml_response.split(b"\n\n", 1)[1]
result, _ = loads(xml_response)
metrics["xml_bytes"] = len(xml_response)
metrics["client_version"] = str(result[0]) if result else ""
metrics["ok"] = True
return metrics
def profile_diagnostics(profile: dict) -> dict:
"""Lightweight per-profile diagnostics for save/test UI."""
started = time.perf_counter()
result = {"profile_id": profile.get("id"), "ok": False, "checks": {}}
try:
c = client_for(profile)
version = str(c.call("system.client_version") or "")
library = ""
try:
library = str(c.call("system.library_version") or "")
except Exception:
library = ""
paths = {}
for key, method in (("default_directory", "directory.default"), ("cwd", "system.cwd")):
try:
paths[key] = str(c.call(method) or "")
except Exception as exc:
paths[key] = {"error": str(exc)}
write_permissions = {}
free_disk = {}
base = paths.get("default_directory") if isinstance(paths.get("default_directory"), str) else ""
if base:
try:
out = _rt_execute(c, "execute.capture", "sh", "-lc", f"test -w {shlex.quote(base)} && printf writable || printf readonly")
write_permissions[base] = str(out or "").strip() or "unknown"
except Exception as exc:
write_permissions[base] = f"error: {exc}"
try:
out = _rt_execute(c, "execute.capture", "sh", "-lc", f"df -Pk {shlex.quote(base)} | tail -1 | awk '{{print $4}}'")
kb = int(str(out or "0").strip() or 0)
free_disk[base] = {"free_bytes": kb * 1024, "free_h": human_size(kb * 1024)}
except Exception as exc:
free_disk[base] = {"error": str(exc)}
result.update({
"ok": True,
"status": "online",
"version": version,
"library_version": library,
"base_paths": paths,
"write_permissions": write_permissions,
"free_disk": free_disk,
"response_time_ms": round((time.perf_counter() - started) * 1000, 2),
})
except Exception as exc:
result.update({"ok": False, "status": "error", "error": str(exc), "response_time_ms": round((time.perf_counter() - started) * 1000, 2)})
if result.get("ok") and result.get("response_time_ms", 0) > 1500:
result["status"] = "slow"
return result
# Note: Keep split module exports compatible with the previous single rtorrent.py module.
__all__ = [
name for name in globals()
if not name.startswith("__") and name not in {"annotations"}
]