Files
olivv-cs-ComfyUI-FunPack/core/sysinfo.py
T
digital-garbage 739a720adc app, core: Settings matches v4 -- retractable rail, fixed frame, real About
Three things the unified Settings window was still missing next to v4's
own, all found by looking at it next to v4 rather than in isolation:

- The sidebar is a 52px icon rail now, expanding to a full list on
  hover/focus and overlaying the content instead of a splitPane's
  permanently-docked two-pane shape -- v4's own sidebar, not an
  approximation of it. Fixing this surfaced two real CSS bugs along the
  way: filterList's own min-width:220px silently defeated the collapse
  (clipping the search box to "Sear" instead of showing an icon), and
  .cx-filter-list's unset overflow-x (computed to auto once overflow-y
  was set) put a horizontal scrollbar under it. Centering the icon in
  the collapsed row needed the label's flex-basis AND the row's own gap
  to both go to zero -- an invisible label still claims its share of
  both otherwise, and the icon renders off-centre.
- The modal now has a real height (v4's own 660px) rather than sizing to
  whichever section is showing, so switching between a short section
  (About) and a long one (Node packs) doesn't visibly resize the whole
  window. That exposed a THIRD latent bug: .cx-modal-body never had
  flex:1 1 auto, invisible until a modal had a real height to fill --
  every prior modal shrink-wrapped its content, so head+body+foot always
  summed to exactly the card's height by accident. Fixed at the shared
  rule, not just for Settings.
- About now shows what v4's does: commit and dirty state, a codename
  when the checkout has one, and the machine's real hardware/software
  facts (chip, memory, GPU or MPS, disk, OS, ComfyUI/Python/Torch/
  attention-backend versions, hostname) -- a new core/sysinfo.py
  (ported from v4's own, individually guarded so a missing probe never
  takes the panel down) behind a new GET /funpack/api/system, injected
  into settings_window.js the same way every other network call here is.

Also: a nav row's tone (the coloured icon chip) is now validated against
its closed set the same way button.js already validates its own tone --
an unknown one used to render silently wrong instead of failing loudly.
2026-09-07 13:47:18 +03:00

188 lines
6.1 KiB
Python

"""Host facts for the About section -- the machine ComfyUI runs on, not the
browser's.
That distinction is the whole point on a rental: the frontend already knows
the laptop it is displayed on, and nobody cares. What matters is the box doing
the sampling -- which GPU, how much VRAM, which torch, how much disk is left
for outputs.
Everything here is best-effort and individually guarded: a missing probe
returns None and the panel renders a dash. Nothing in this module may raise,
because About is the one screen that should still work when the rest of the
install is broken.
"""
from __future__ import annotations
import os
import platform
import shutil
import subprocess
import sys
from . import config
def _bytes_gb(n):
"""Bytes -> GB as a float, using the 1024 base the OS's own About panels use."""
try:
return round(float(n) / (1024 ** 3), 2)
except Exception:
return None
def _cpu_name():
"""A human CPU model string. platform.processor() is famously empty on Linux and
just 'arm' on Apple silicon, so ask the OS directly first."""
try:
if sys.platform == "darwin":
out = subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"],
capture_output=True, text=True, timeout=3)
if out.returncode == 0 and out.stdout.strip():
return out.stdout.strip()
elif sys.platform.startswith("linux"):
cpuinfo_path = "/proc/cpuinfo"
if os.path.exists(cpuinfo_path):
with open(cpuinfo_path, errors="replace") as f:
for line in f:
if line.lower().startswith("model name"):
return line.split(":", 1)[1].strip()
except Exception:
pass
return platform.processor() or platform.machine() or None
def _cpu():
info = {"name": _cpu_name(), "arch": platform.machine() or None,
"cores": None, "threads": os.cpu_count()}
try:
import psutil # a ComfyUI requirement, so normally present
info["cores"] = psutil.cpu_count(logical=False)
info["threads"] = psutil.cpu_count(logical=True) or info["threads"]
except Exception:
pass
return info
def _memory():
try:
import psutil
vm = psutil.virtual_memory()
return {"total_gb": _bytes_gb(vm.total), "available_gb": _bytes_gb(vm.available)}
except Exception:
pass
try: # stdlib fallback (Linux/macOS)
total = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
return {"total_gb": _bytes_gb(total), "available_gb": None}
except Exception:
return {"total_gb": None, "available_gb": None}
def _gpus():
"""Every visible CUDA/ROCm device. Empty list on a CPU-only box (the local Mac)."""
out = []
try:
import torch
if not torch.cuda.is_available():
return out
for i in range(torch.cuda.device_count()):
entry = {"name": None, "vram_gb": None, "capability": None}
try:
entry["name"] = torch.cuda.get_device_name(i)
except Exception:
pass
try:
props = torch.cuda.get_device_properties(i)
entry["vram_gb"] = _bytes_gb(props.total_memory)
entry["capability"] = f"sm_{props.major}{props.minor}"
except Exception:
pass
out.append(entry)
except Exception:
pass
return out
def _mps():
"""Apple-silicon GPU, which reports through MPS rather than CUDA."""
try:
import torch
if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available():
return True
except Exception:
pass
return False
def _disk():
"""Free space where FunPack lives -- the volume that fills up with renders."""
try:
usage = shutil.disk_usage(str(config.ROOT))
return {"total_gb": _bytes_gb(usage.total), "free_gb": _bytes_gb(usage.free)}
except Exception:
return {"total_gb": None, "free_gb": None}
def _os_name():
try:
if sys.platform == "darwin":
return f"macOS {platform.mac_ver()[0]}".strip()
if sys.platform.startswith("win"):
return f"Windows {platform.release()}"
# Prefer the distro's own pretty name over the kernel version.
osr_path = "/etc/os-release"
if os.path.exists(osr_path):
with open(osr_path, errors="replace") as f:
for line in f:
if line.startswith("PRETTY_NAME="):
return line.split("=", 1)[1].strip().strip('"')
return f"{platform.system()} {platform.release()}"
except Exception:
return None
def _torch():
info = {"version": None, "cuda": None, "attention": None}
try:
import torch
info["version"] = torch.__version__
info["cuda"] = getattr(torch.version, "cuda", None)
except Exception:
pass
# Which fast-attention backend is actually importable. This is the line that
# answers "is sage actually installed on this rental?" without digging
# through the launch args.
for mod, label in (("sageattention", "SageAttention"), ("flash_attn", "FlashAttention"),
("xformers", "xformers")):
try:
m = __import__(mod)
info["attention"] = f"{label} {getattr(m, '__version__', '')}".strip()
break
except Exception:
continue
return info
def _comfy_version():
try:
import comfyui_version
return comfyui_version.__version__
except Exception:
return None
def collect():
"""The whole About payload. Static enough that the frontend caches it per load."""
gpus = _gpus()
return {
"host": platform.node() or None,
"os": _os_name(),
"cpu": _cpu(),
"memory": _memory(),
"gpus": gpus,
"mps": _mps() if not gpus else False,
"disk": _disk(),
"python": platform.python_version(),
"torch": _torch(),
"comfyui": _comfy_version(),
}