mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 20:31:21 +00:00
* Update GitHub repo references from feedback* to feedBack* * rename: slopsmith -> feedBack, byron -> got-feedBack Renames across the entire codebase: - slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack - byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack - /home/byron/ -> /opt/got-feedBack/ - byron@ougsoft.com -> hi@got-feedBack.org - github.com/byrongamatos/ -> github.com/got-feedback/ - com.byron. -> com.got-feedback. - SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback - Protocol/storage strings migrated with read-old/write-new pattern - window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias Refs: #rename-slopsmith * rename: complete regen against current main + fix backward-compat alias Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of current main (3 commits had landed since the branch: #572/#554/#574), resolving the four content conflicts in favour of main's newer content (autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge). Completion fixes on top of the mechanical rename: - Re-apply rename to post-branch content the original rename never saw: window.slopsmith(.Tour) consumers in lessons.js / notifications.js / onboarding-tour.js, and the matching JS + python tests (autoplay_exit, progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env vars now match server.py (which reads FEEDBACK_SYNC_STARTUP / FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real path again. - Restore the window.slopsmith backward-compat alias dropped during conflict resolution, and move the bus aliases to AFTER the _feedBackExisting merge block so they reference the fully-assembled object (also fixes the loop_api.test.js API-surface regex, which the original PR latently broke). - Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR) and gitignore it. - Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped source labels. Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669 passed / 1683 collected with 0 import errors; zero residual slopsmith/byron except the two intentional window.slopsmith aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rename: implement advertised backward-compat + prune dead community plugins Address gaps where PR #537's "Backward compatibility" section was advertised but not implemented, and clean up the community plugin list. Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured): - New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI / SLOPSMITH_PLUGINS_DIR / etc. deployments keep working. - Fix the rename collapsing plugins/__init__.py and minigames/routes.py from `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost). Storage (app.js update-channel): - Read feedBack-update-channel, fall back to legacy slopsmith-update-channel, and clear the legacy key on write — so a user's update-channel preference survives the rename instead of resetting to "stable". Community plugin list (README): the rename rewrote third-party repo URLs we don't own. Probed every one; their owners never renamed, so: - Restore the 13 live community plugins to their real slopsmith-* names. - Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle). - Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory). Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors, rename-sensitive + startup suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
242 lines
7.2 KiB
Python
242 lines
7.2 KiB
Python
"""Backend hardware probe for diagnostic bundles.
|
|
|
|
Produces a `system.hardware.v1`-shaped dict — see
|
|
docs/diagnostics-bundle-spec.md.
|
|
|
|
All probes are best-effort and never raise. Missing tools, missing
|
|
permissions, container masking — every case yields a structured note in
|
|
the output rather than a 500 on the export endpoint.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from env_compat import getenv_compat
|
|
|
|
SCHEMA = "system.hardware.v1"
|
|
|
|
|
|
def _safe_run(cmd: list[str], timeout: float = 2.0) -> tuple[int, str, str]:
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
check=False,
|
|
)
|
|
return proc.returncode, proc.stdout or "", proc.stderr or ""
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
return 127, "", ""
|
|
|
|
|
|
def detect_runtime() -> dict:
|
|
"""Cheap runtime-kind detection (env var + cgroup checks, no subprocess).
|
|
|
|
Exported as a public function so callers that don't need the full
|
|
hardware probe can still obtain the runtime kind without paying for
|
|
nvidia-smi / psutil CPU probes.
|
|
"""
|
|
out: dict = {"kind": "bare", "in_docker": False, "in_kubernetes": False}
|
|
env_runtime = (getenv_compat("FEEDBACK_RUNTIME", "") or "").strip().lower()
|
|
if env_runtime in ("electron", "docker", "bare"):
|
|
out["kind"] = env_runtime
|
|
if Path("/.dockerenv").exists():
|
|
out["in_docker"] = True
|
|
if out["kind"] == "bare":
|
|
out["kind"] = "docker"
|
|
cgroup = Path("/proc/1/cgroup")
|
|
if cgroup.exists():
|
|
try:
|
|
txt = cgroup.read_text(errors="ignore")
|
|
if "docker" in txt or "containerd" in txt or "kubepods" in txt:
|
|
out["in_docker"] = True
|
|
if out["kind"] == "bare":
|
|
out["kind"] = "docker"
|
|
except OSError:
|
|
pass
|
|
if os.environ.get("KUBERNETES_SERVICE_HOST"):
|
|
out["in_kubernetes"] = True
|
|
if out["kind"] == "bare":
|
|
try:
|
|
import psutil # type: ignore
|
|
|
|
parent = psutil.Process(os.getppid()).name().lower()
|
|
if "electron" in parent or "feedBack" in parent:
|
|
out["kind"] = "electron"
|
|
except Exception:
|
|
pass
|
|
return out
|
|
|
|
|
|
def _probe_os() -> dict:
|
|
return {
|
|
"system": platform.system(),
|
|
"release": platform.release(),
|
|
"version": platform.version(),
|
|
"machine": platform.machine(),
|
|
}
|
|
|
|
|
|
def _probe_cpu(notes: list[str]) -> dict:
|
|
out: dict = {
|
|
"brand": None,
|
|
"arch": platform.machine(),
|
|
"cores_logical": os.cpu_count(),
|
|
"cores_physical": None,
|
|
"freq_mhz_current": None,
|
|
"freq_mhz_max": None,
|
|
}
|
|
try:
|
|
import psutil # type: ignore
|
|
|
|
out["cores_physical"] = psutil.cpu_count(logical=False)
|
|
freq = psutil.cpu_freq()
|
|
if freq:
|
|
out["freq_mhz_current"] = round(freq.current) if freq.current else None
|
|
out["freq_mhz_max"] = round(freq.max) if freq.max else None
|
|
except Exception as e:
|
|
notes.append(f"psutil cpu probe failed: {e}")
|
|
try:
|
|
import cpuinfo # type: ignore
|
|
|
|
info = cpuinfo.get_cpu_info() or {}
|
|
out["brand"] = info.get("brand_raw") or info.get("brand") or None
|
|
except Exception as e:
|
|
notes.append(f"py-cpuinfo probe failed: {e}")
|
|
# Fallback: platform.processor() is reliable on Windows + some Linux,
|
|
# useless on macOS Apple Silicon (returns 'arm').
|
|
proc = platform.processor()
|
|
if proc and proc.lower() not in ("arm", "i386"):
|
|
out["brand"] = proc
|
|
return out
|
|
|
|
|
|
def _probe_memory(notes: list[str]) -> dict:
|
|
out: dict = {"total_bytes": None, "available_bytes": None}
|
|
try:
|
|
import psutil # type: ignore
|
|
|
|
vm = psutil.virtual_memory()
|
|
out["total_bytes"] = int(vm.total)
|
|
out["available_bytes"] = int(vm.available)
|
|
except Exception as e:
|
|
notes.append(f"psutil memory probe failed: {e}")
|
|
return out
|
|
|
|
|
|
def _probe_gpu_nvidia() -> list[dict]:
|
|
rc, stdout, _ = _safe_run(
|
|
[
|
|
"nvidia-smi",
|
|
"--query-gpu=name,driver_version,memory.total",
|
|
"--format=csv,noheader,nounits",
|
|
]
|
|
)
|
|
if rc != 0 or not stdout.strip():
|
|
return []
|
|
gpus: list[dict] = []
|
|
for line in stdout.strip().splitlines():
|
|
parts = [p.strip() for p in line.split(",")]
|
|
if len(parts) < 3:
|
|
continue
|
|
try:
|
|
mem_mb = int(parts[2])
|
|
except ValueError:
|
|
mem_mb = None
|
|
gpus.append({
|
|
"source": "nvidia-smi",
|
|
"name": parts[0],
|
|
"driver": parts[1],
|
|
"memory_total_mb": mem_mb,
|
|
})
|
|
return gpus
|
|
|
|
|
|
def _probe_gpu_rocm() -> list[dict]:
|
|
rc, stdout, _ = _safe_run(
|
|
["rocm-smi", "--showproductname", "--showdriverversion", "--json"]
|
|
)
|
|
if rc != 0 or not stdout.strip():
|
|
return []
|
|
try:
|
|
data = json.loads(stdout)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
gpus: list[dict] = []
|
|
for card_id, card in (data or {}).items():
|
|
if not isinstance(card, dict):
|
|
continue
|
|
gpu: dict = {
|
|
"source": "rocm-smi",
|
|
"id": card_id,
|
|
"name": card.get("Card series") or card.get("Card model") or "AMD GPU",
|
|
}
|
|
driver = card.get("Driver version") or card.get("driver_version")
|
|
if driver:
|
|
gpu["driver"] = driver
|
|
gpus.append(gpu)
|
|
return gpus
|
|
|
|
|
|
def _probe_gpu_macos() -> list[dict]:
|
|
if platform.system() != "Darwin":
|
|
return []
|
|
rc, stdout, _ = _safe_run(
|
|
["system_profiler", "SPDisplaysDataType", "-json"], timeout=4.0
|
|
)
|
|
if rc != 0 or not stdout.strip():
|
|
return []
|
|
try:
|
|
data = json.loads(stdout)
|
|
except json.JSONDecodeError:
|
|
return []
|
|
gpus: list[dict] = []
|
|
for card in data.get("SPDisplaysDataType", []) or []:
|
|
gpus.append({
|
|
"source": "system_profiler",
|
|
"name": card.get("sppci_model") or card.get("_name") or "GPU",
|
|
"vendor": card.get("spdisplays_vendor"),
|
|
"metal_support": card.get("spdisplays_metalfamily"),
|
|
})
|
|
return gpus
|
|
|
|
|
|
def _probe_gpus(notes: list[str]) -> list[dict]:
|
|
gpus: list[dict] = []
|
|
gpus.extend(_probe_gpu_nvidia())
|
|
gpus.extend(_probe_gpu_rocm())
|
|
gpus.extend(_probe_gpu_macos())
|
|
if not gpus:
|
|
notes.append(
|
|
"no GPU probes succeeded — nvidia-smi/rocm-smi/system_profiler absent or denied"
|
|
)
|
|
return gpus
|
|
|
|
|
|
def collect() -> dict:
|
|
"""Build a `system.hardware.v1` dict. Never raises."""
|
|
notes: list[str] = []
|
|
runtime = detect_runtime()
|
|
cpu = _probe_cpu(notes)
|
|
memory = _probe_memory(notes)
|
|
gpu = _probe_gpus(notes)
|
|
if runtime["in_docker"]:
|
|
notes.append(
|
|
"container masks host CPU/RAM — values reflect container limits, not host"
|
|
)
|
|
return {
|
|
"schema": SCHEMA,
|
|
"runtime": runtime,
|
|
"os": _probe_os(),
|
|
"cpu": cpu,
|
|
"memory": memory,
|
|
"gpu": gpu,
|
|
"notes": notes,
|
|
}
|