diff --git a/app.py b/app.py index b6d6e86..84663d0 100644 --- a/app.py +++ b/app.py @@ -88,6 +88,7 @@ raid_refresh_lock = threading.Lock() datastore_cache = {"stored_at": 0.0, "snapshot": None, "raid": None, "raid_at": 0.0} raid_log_state = None graphics_log_state = None +graphics_process_log_state = None hardware_log_once = set() LANG_NAMES = {"de":"Deutsch", "deu":"Deutsch", "ger":"Deutsch", "en":"Englisch", "eng":"Englisch", "ja":"Japanisch", "jpn":"Japanisch", "ko":"Koreanisch", "kor":"Koreanisch", "fr":"Französisch", "fra":"Französisch", "fre":"Französisch", "es":"Spanisch", "spa":"Spanisch", "it":"Italienisch", "ita":"Italienisch", "ru":"Russisch", "rus":"Russisch", "zh":"Chinesisch", "zho":"Chinesisch", "chi":"Chinesisch", "und":"Unbekannt", "":"Unbekannt"} @@ -357,7 +358,7 @@ def io_pressure(): def graphics_snapshot(): """Read NVIDIA GPU details with short, non-blocking subprocess timeouts.""" - global graphics_log_state + global graphics_log_state, graphics_process_log_state query = "index,name,uuid,serial,pci.bus_id,driver_version,pstate,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used,memory.free,power.draw,power.limit,fan.speed,clocks.current.graphics,clocks.current.memory,clocks.max.graphics,clocks.max.memory,compute_mode" try: result = subprocess.run([NVIDIA_SMI_BIN, f"--query-gpu={query}", "--format=csv,noheader,nounits"], capture_output=True, text=True, timeout=5, check=False) @@ -388,15 +389,49 @@ def graphics_snapshot(): try: gpu[key] = round(float(gpu[key]), 1) except (TypeError, ValueError): gpu[key] = None gpus.append(gpu) - processes = [] + processes_by_pid = {} try: proc = subprocess.run([NVIDIA_SMI_BIN, "--query-compute-apps=pid,process_name,used_gpu_memory", "--format=csv,noheader,nounits"], capture_output=True, text=True, timeout=5, check=False) - for line in proc.stdout.splitlines(): - values = [value.strip() for value in line.split(",")] - if len(values) >= 3: - processes.append({"pid": values[0], "name": values[1], "memory": values[2]}) + if proc.returncode == 0: + for line in proc.stdout.splitlines(): + values = [value.strip() for value in line.split(",")] + if len(values) >= 3 and values[0].isdigit(): + processes_by_pid[values[0]] = {"pid": values[0], "name": values[1], "memory": values[2], "type": "compute"} except (OSError, subprocess.SubprocessError): pass + pmon_error = "" + try: + # Compute-apps misses NVENC/NVDEC. pmon also reports video/graphics + # processes such as Jellyfin while a hardware transcode is running. + pmon = subprocess.run([NVIDIA_SMI_BIN, "pmon", "-c", "1", "-s", "um"], capture_output=True, text=True, timeout=5, check=False) + if pmon.returncode == 0: + for line in pmon.stdout.splitlines(): + values = line.split() + if not values or values[0].startswith("#") or len(values) < 8 or not values[1].isdigit(): + continue + pid, process_type = values[1], values[2] + command = " ".join(values[7:]) or "GPU-Prozess" + processes_by_pid[pid] = { + "pid": pid, + "name": command, + "memory": f"SM {values[3]}% · ENC {values[5]}% · DEC {values[6]}%", + "type": process_type, + "sm": values[3], "enc": values[5], "dec": values[6], + } + else: + pmon_error = " ".join((pmon.stderr or "pmon ohne Ausgabe").split())[:180] + except (OSError, subprocess.SubprocessError) as exc: + pmon_error = error_summary(exc) + processes = list(processes_by_pid.values()) + process_signature = tuple((item["pid"], item["name"], item.get("type")) for item in processes) + if process_signature != graphics_process_log_state: + if processes: + log.info("GPU-Prozesse erkannt: %s", ", ".join(f"{item['name']} (PID {item['pid']})" for item in processes)) + elif pmon_error: + log.warning("GPU-Prozessabfrage fehlgeschlagen: %s", pmon_error) + else: + log.info("Keine GPU-Prozesse erkannt") + graphics_process_log_state = process_signature signature = ("ok", tuple((gpu.get("index"), gpu.get("name"), gpu.get("temperature.gpu"), gpu.get("utilization.gpu")) for gpu in gpus)) if signature != graphics_log_state: log.info("GPU-Status verfügbar: %s NVIDIA-GPU(s) via %s", len(gpus), NVIDIA_SMI_BIN) diff --git a/templates/index.html b/templates/index.html index c2e8758..2d63ac8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -20,6 +20,12 @@ + + @@ -60,7 +66,7 @@ if ('serviceWorker' in navigator) { .settings-modal .setting-help{display:block;margin:-3px 0 12px;color:var(--muted);font-size:11px}.settings-modal .setting-select{width:100%;height:38px}.pwa-install{display:none} @media(max-width:700px){body{font-size:12px;padding-bottom:env(safe-area-inset-bottom)}header{height:auto;min-height:62px;padding:9px 12px;gap:9px;align-items:center}.brand-mark,.brand-mark svg{width:30px;height:30px}.brand-name{font-size:14px}.head-title{font-size:12px}.head-sub{font-size:10px}.actions{gap:4px}.actions button{height:34px;padding:0 8px}.settings-button{font-size:15px}.actions #rescan{font-size:0}.actions #rescan::after{content:'↻';font-size:16px}main{padding:10px 10px 24px}.nav{margin-bottom:10px;padding:4px;overflow-x:auto;scrollbar-width:none}.nav::-webkit-scrollbar{display:none}.nav button{flex:0 0 auto;white-space:nowrap;padding:8px 10px;font-size:11px}.toolbar{grid-template-columns:1fr;gap:7px;padding:8px}.toolbar input,.toolbar select{width:100%;height:38px}.stats{gap:6px}.stat{padding:6px 8px}.tablewrap{overflow-x:auto;border-radius:8px}table{min-width:860px}.dashboard-hero{margin-bottom:10px;padding:18px;border-radius:11px}.dashboard-hero h1{font-size:21px}.dashboard-hero p{font-size:11px;line-height:1.4}.overview-grid{grid-template-columns:1fr 1fr;gap:7px}.overview-card{min-height:84px;padding:12px 10px;gap:8px}.overview-icon{width:32px;height:32px;font-size:18px}.overview-card strong{font-size:18px}.overview-card span:not(.overview-icon){font-size:9px}.overview-card small{font-size:9px}.dashboard-columns{gap:8px}.system-panel,.service-panel{padding:14px;border-radius:10px}.system-meters{gap:12px}.panel-heading{margin-bottom:13px}.panel-heading h2{font-size:14px}.panel-heading p{font-size:10px}.disk-row{grid-template-columns:82px 1fr auto;gap:7px}.disk-values{font-size:10px}.download-hero{padding:18px;margin-bottom:10px}.download-hero h2{font-size:19px}.download-hero p{font-size:11px}.download-stats{grid-template-columns:1fr 1fr;gap:7px}.download-card{min-height:78px;padding:10px;gap:8px}.card-icon{width:30px;height:30px;font-size:17px}.card-label{font-size:9px}.download-card strong{font-size:17px}.download-card small{font-size:9px}.settings-modal{max-height:calc(100dvh - 24px);overflow:auto;padding:18px}.settings-head{margin-bottom:15px}.settings-modal label{margin-top:13px}} @media(max-width:390px){.brand-name{display:none}.head-sub{display:none}.overview-grid{grid-template-columns:1fr}.dashboard-hero{padding:15px}.download-stats{grid-template-columns:1fr}} - +
MEDIA
Media Max
Audio- und Untertitelspuren aus den Mediendateien