Clean up LSP diagnostics across backend and frontend

Backend (nvcurve/):
- hal/fans.py, hal/limits.py, hal/gpu.py: replace conditional pynvml
  imports with the established 'pynvml: Any = _pynvml_import' pattern
  (fixes ~50 'possibly unbound' errors); type the result dicts; guard
  query_interface() results; explicit uuid/pci-bus parsing (int, hex
  convention documented); modernize Optional[T] -> T | None
- cli.py: fix 'curve_state' possibly-unbound and snap_path None handling
  in cmd_setup; wrap unchecked int()/open()/makedirs() calls in
  try/except with clean CLI errors; add module logger for silent
  except-pass blocks; raise ... from exc; fix unused loop vars and
  set-comprehension
- hal/snapshot.py: filepath: str | None; wrap all file ops; sorted
  imports; remove unused CT_POINTS import
- daemon.py: extract 0o666 to _SOCKET_MODE constant (intentional for
  /run sockets) with nosemgrep
- server.py: nosemgrep for Python 3.7-compat false positive (project
  requires >= 3.12); log previously-swallowed exception
- profiles/native.py, profiles/apply.py: wrap file ops and int(k)
  profile-key parsing; sorted imports; modernize typing

Frontend (frontend/src):
- Add .js extensions to all relative imports (standard TS-ESM; Vite
  resolves .js -> .ts)
- React.FormEvent (deprecated in React 19 types) -> React.SubmitEvent
- catch (e: any) -> catch (e: unknown) + instanceof Error narrowing
- React-hooks: move ref writes from render into effects; convert
  viewport reset to render-phase state adjustment; split
  selectPoint(index, multi) into selectPoint + togglePoint (no flag
  argument); remove non-null assertion
- Static inline styles -> Tailwind classes (dynamic positioning/cursor
  styles kept)
- Remove non-standard 'container' option from scrollIntoView (browsers
  ignore unknown options) which had orphaned a @ts-expect-error
- Object.fromEntries for Map -> Record conversion

Tooling:
- .gitignore: ignore .codegraph/ local tool data

Verified: tsc --noEmit, vite production build, python imports, and
full LSP scan (0 errors/warnings in both projects).
This commit is contained in:
ARIA committed 2026-09-08 23:57:30 +02:00
1 parent 9006c22fde
commit 930e56bd07
33 files changed
+1571 -804

No files matched your search

+29 -11
View File
@@ -9,14 +9,19 @@ Uses NVML (via pynvml) for all operations:
import ctypes
import logging
from typing import List, Optional
from typing import Any
try:
import pynvml
import pynvml as _pynvml_import
_NVML_AVAILABLE = True
except ImportError:
_pynvml_import = None
_NVML_AVAILABLE = False
# Aliased as Any so attribute access is not flagged when the import failed.
pynvml: Any = _pynvml_import
log = logging.getLogger("nvcurve.hal.fans")
# We use fan index 0 (first/primary fan) for all operations.
@@ -35,7 +40,7 @@ def get_fan_info(gpu_index: int = 0) -> dict:
Returns None values on failure.
"""
out = {
out: dict[str, float | None] = {
"fan_pct": None,
"fan_mode": None,
"min_fan_pct": None,
@@ -75,7 +80,10 @@ def get_fan_info(gpu_index: int = 0) -> dict:
def set_fan_speed(gpu_index: int, pct: int) -> tuple[bool, str]:
"""Set fan speed to a percentage (0-100) on the primary fan."""
pct = max(0, min(100, int(pct)))
try:
pct = max(0, min(100, int(pct)))
except (TypeError, ValueError):
return False, "Invalid fan speed"
if not _NVML_AVAILABLE:
return False, "NVML not available"
try:
@@ -102,7 +110,9 @@ def reset_fan(gpu_index: int = 0) -> tuple[bool, str]:
try:
ret = subprocess.run(
["nvidia-smi", "-i", str(gpu_index), "-fan", "default"],
capture_output=True, text=True, timeout=10,
capture_output=True,
text=True,
timeout=10,
)
if ret.returncode == 0:
return True, "OK"
@@ -123,19 +133,21 @@ def reset_fan(gpu_index: int = 0) -> tuple[bool, str]:
return False, f"Failed to reset fan: {exc}"
def get_temp(gpu_index: int = 0) -> Optional[float]:
def get_temp(gpu_index: int = 0) -> float | None:
"""Read current GPU temperature in °C."""
if not _NVML_AVAILABLE:
return None
try:
handle = _get_handle(gpu_index)
return float(pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU))
return float(
pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
)
except pynvml.NVMLError as exc:
log.debug("get_temp: %s", exc)
return None
def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
def interpolate_fan_speed(curve: list[dict], temp_c: float) -> int | None:
"""Interpolate target fan speed from a curve at a given temperature.
curve: list of {temp_c: int, fan_pct: int} sorted by temp_c
@@ -144,7 +156,10 @@ def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
if not curve or len(curve) < 2:
return None
temp = float(temp_c)
try:
temp = float(temp_c)
except (TypeError, ValueError):
return None
# Find the two surrounding points
for i in range(len(curve) - 1):
@@ -157,7 +172,10 @@ def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
if t0 <= temp <= t1:
fraction = (temp - t0) / (t1 - t0)
result = f0 + fraction * (f1 - f0)
return max(0, min(100, int(round(result))))
try:
return max(0, min(100, int(round(result))))
except (TypeError, ValueError):
return None
# Outside range: clamp to first or last point
if temp <= curve[0]["temp_c"]:
@@ -165,7 +183,7 @@ def interpolate_fan_speed(curve: List[dict], temp_c: float) -> Optional[int]:
return max(0, min(100, curve[-1]["fan_pct"]))
def validate_curve(curve: List[dict]) -> tuple[bool, str]:
def validate_curve(curve: list[dict]) -> tuple[bool, str]:
"""Validate a fan curve.
Returns (True, "OK") or (False, error_message).
+38 -20
View File
@@ -1,12 +1,17 @@
"""GPU discovery and initialization."""
import contextlib
import ctypes
import logging
import sys
from typing import Any
from ..nvapi.bootstrap import query_interface
from ..nvapi.constants import FUNC
from ..nvapi.types import GpuInfo
log = logging.getLogger("nvcurve.hal.gpu")
def init_nvapi() -> None:
"""Initialize NvAPI. Must be called before any GPU operations."""
@@ -19,7 +24,10 @@ def enumerate_gpus() -> tuple[ctypes.Array, int]:
"""Return (gpu_handles_array, count). Exits if no GPUs found."""
gpus = (ctypes.c_void_p * 64)()
ngpu = ctypes.c_int32()
query_interface(FUNC["EnumPhysicalGPUs"])(ctypes.byref(gpus), ctypes.byref(ngpu))
enum_fn = query_interface(FUNC["EnumPhysicalGPUs"])
if enum_fn is None:
raise RuntimeError("NvAPI function EnumPhysicalGPUs not available")
enum_fn(ctypes.byref(gpus), ctypes.byref(ngpu))
if ngpu.value == 0:
print("No NVIDIA GPUs found")
sys.exit(1)
@@ -29,7 +37,10 @@ def enumerate_gpus() -> tuple[ctypes.Array, int]:
def get_gpu_name(gpu) -> str:
"""Return the full name string for a GPU handle."""
name_buf = ctypes.create_string_buffer(256)
query_interface(FUNC["GetFullName"])(gpu, name_buf)
fn = query_interface(FUNC["GetFullName"])
if fn is None:
raise RuntimeError("NvAPI function GetFullName not available")
fn(gpu, name_buf)
return name_buf.value.decode(errors="replace")
@@ -40,38 +51,45 @@ def discover_gpus() -> list[GpuInfo]:
infos = []
try:
import pynvml
pynvml.nvmlInit()
has_nvml = True
import pynvml as _pynvml
_pynvml.nvmlInit()
except Exception:
has_nvml = False
_pynvml = None
# Aliased as Any so attribute access is not flagged when the import failed.
pynvml: Any = _pynvml
for i in range(count):
name = get_gpu_name(gpus[i])
uuid = None
pci_bus_id = None
if has_nvml:
if pynvml is not None:
try:
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
uuid = pynvml.nvmlDeviceGetUUID(handle)
raw_uuid = pynvml.nvmlDeviceGetUUID(handle)
# NVML might return bytes
if isinstance(uuid, bytes):
uuid = uuid.decode('utf-8', errors='ignore')
if isinstance(raw_uuid, bytes):
uuid = raw_uuid.decode("utf-8", errors="ignore")
elif raw_uuid is not None:
uuid = str(raw_uuid)
pci_info = pynvml.nvmlDeviceGetPciInfo(handle)
# Parse something like "00000000:01:00.0" -> bus is 1
if isinstance(pci_info.bus, bytes):
pci_bus_id = int(pci_info.bus.decode('utf-8', errors='ignore'), 16)
# Parse something like "00000000:01:00.0" -> bus is 1.
# PCI bus numbers are hex by convention (pynvml's field is an
# int; the str/bytes branches are defensive).
bus = pci_info.bus
if isinstance(bus, bytes):
pci_bus_id = int(bus.decode("utf-8", errors="ignore"), 16)
elif isinstance(bus, str):
pci_bus_id = int(bus, 16)
else:
pci_bus_id = pci_info.bus
except Exception:
pass
pci_bus_id = int(bus)
except Exception as exc:
log.debug("NVML query for GPU %d failed: %s", i, exc)
infos.append(GpuInfo(name=name, index=i, uuid=uuid, pci_bus_id=pci_bus_id))
if has_nvml:
try:
if pynvml is not None:
with contextlib.suppress(Exception):
pynvml.nvmlShutdown()
except Exception:
pass
return infos
+75 -38
View File
@@ -11,21 +11,26 @@ that are explicitly specified, leaving others unchanged on hardware.
"""
import ctypes
import subprocess
import logging
from typing import Optional
import subprocess
from typing import Any
try:
import pynvml
import pynvml as _pynvml_import
_NVML_AVAILABLE = True
except ImportError:
_pynvml_import = None
_NVML_AVAILABLE = False
# Aliased as Any so attribute access is not flagged when the import failed.
pynvml: Any = _pynvml_import
log = logging.getLogger("nvcurve.hal.limits")
# ── NVML library / handle helpers ─────────────────────────────────────────────
_nvml_lib: Optional[ctypes.CDLL] = None
_nvml_lib: ctypes.CDLL | None = None
def _nvml_cdll() -> ctypes.CDLL:
@@ -34,12 +39,12 @@ def _nvml_cdll() -> ctypes.CDLL:
if _nvml_lib is not None:
return _nvml_lib
# Prefer to reuse the library already loaded by pynvml to avoid dlopen races.
for attr in ("nvml", "_nvml"): # attribute name varies by pynvml version
for attr in ("nvml", "_nvml"): # attribute name varies by pynvml version
mod = getattr(pynvml, attr, None)
lib = getattr(mod, "_lib", None) or getattr(mod, "_nvmlLib", None)
if lib is not None:
_nvml_lib = lib
return _nvml_lib
return lib
_nvml_lib = ctypes.CDLL("libnvidia-ml.so.1")
return _nvml_lib
@@ -53,9 +58,10 @@ def _get_handle(gpu_index: int):
# ── Power limit ───────────────────────────────────────────────────────────────
def get_power_limit(gpu_index: int = 0) -> dict:
"""Return dict with power_limit_w, default_power_limit_w, min_power_limit_w, max_power_limit_w."""
out = {
out: dict[str, int | None] = {
"power_limit_w": None,
"default_power_limit_w": None,
"min_power_limit_w": None,
@@ -71,8 +77,8 @@ def get_power_limit(gpu_index: int = 0) -> dict:
try:
default = pynvml.nvmlDeviceGetPowerManagementDefaultLimit(handle)
out["default_power_limit_w"] = default // 1000
except Exception:
pass
except Exception as exc:
log.debug("nvmlDeviceGetPowerManagementDefaultLimit: %s", exc)
except Exception as exc:
log.warning("get_power_limit: %s", exc)
return out
@@ -89,7 +95,8 @@ def set_power_limit(limit_w: int, gpu_index: int = 0) -> tuple[bool, str]:
ret = subprocess.run(
["nvidia-smi", "-i", str(gpu_index), "-pl", str(limit_w)],
capture_output=True, text=True,
capture_output=True,
text=True,
)
if ret.returncode == 0:
return True, "OK"
@@ -111,34 +118,38 @@ def set_power_limit(limit_w: int, gpu_index: int = 0) -> tuple[bool, str]:
# pynvml (nvidia-ml-py ≥ 12) exposes c_nvmlClockOffset_t and nvmlClockOffset_v1
# as ctypes objects; we use them when available and fall back to our own definition.
class _ClockOffset(ctypes.Structure):
_fields_ = [
("version", ctypes.c_uint),
("type", ctypes.c_uint), # nvmlClockType_t
("pstate", ctypes.c_uint), # nvmlPstates_t
("version", ctypes.c_uint),
("type", ctypes.c_uint), # nvmlClockType_t
("pstate", ctypes.c_uint), # nvmlPstates_t
("clockOffsetMHz", ctypes.c_int),
]
_CLOCK_OFFSET_VER = (1 << 24) | ctypes.sizeof(_ClockOffset) # = 0x01000010 (16 bytes)
# NVML clock-type constants (same values as pynvml).
_NVML_CLOCK_GRAPHICS = 0
_NVML_CLOCK_MEM = 2
_NVML_CLOCK_MEM = 2
def _make_clock_offset(clock_type: int, pstate: int = 0, offset_mhz: int = 0) -> ctypes.Structure:
def _make_clock_offset(
clock_type: int, pstate: int = 0, offset_mhz: int = 0
) -> ctypes.Structure:
"""Return a populated nvmlClockOffset_t struct, using pynvml's type when available."""
if hasattr(pynvml, "c_nvmlClockOffset_t") and hasattr(pynvml, "nvmlClockOffset_v1"):
info = pynvml.c_nvmlClockOffset_t()
info.version = pynvml.nvmlClockOffset_v1
info.type = clock_type
info.pstate = pstate
info.version = pynvml.nvmlClockOffset_v1
info.type = clock_type
info.pstate = pstate
info.clockOffsetMHz = offset_mhz
return info
info = _ClockOffset()
info.version = _CLOCK_OFFSET_VER
info.type = clock_type
info.pstate = pstate
info.version = _CLOCK_OFFSET_VER
info.type = clock_type
info.pstate = pstate
info.clockOffsetMHz = offset_mhz
return info
@@ -158,7 +169,7 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
Keys: gpc_offset_mhz, mem_offset_mhz (both int or None on failure).
Calls nvmlDeviceGetClockOffsets once per clock domain (GRAPHICS, MEM).
"""
out = {"gpc_offset_mhz": None, "mem_offset_mhz": None}
out: dict[str, int | None] = {"gpc_offset_mhz": None, "mem_offset_mhz": None}
if not _NVML_AVAILABLE:
return out
try:
@@ -167,11 +178,15 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
# Try pynvml wrapper first (nvidia-ml-py ≥ 12 exposes it correctly).
# Fall back to ctypes-direct if pynvml doesn't have it.
_pynvml_get = getattr(pynvml, "nvmlDeviceGetClockOffsets", None)
fn_get = _try_nvml_fn("nvmlDeviceGetClockOffsets") if _pynvml_get is None else None
fn_get = (
_try_nvml_fn("nvmlDeviceGetClockOffsets") if _pynvml_get is None else None
)
used_new_api = False
for clock_type, key in ((_NVML_CLOCK_GRAPHICS, "gpc_offset_mhz"),
(_NVML_CLOCK_MEM, "mem_offset_mhz")):
for clock_type, key in (
(_NVML_CLOCK_GRAPHICS, "gpc_offset_mhz"),
(_NVML_CLOCK_MEM, "mem_offset_mhz"),
):
info = _make_clock_offset(clock_type, pstate=0)
try:
if _pynvml_get is not None:
@@ -184,7 +199,9 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
out[key] = int(info.clockOffsetMHz)
used_new_api = True
else:
log.debug("nvmlDeviceGetClockOffsets(type=%d) returned %d", clock_type, rc)
log.debug(
"nvmlDeviceGetClockOffsets(type=%d) returned %d", clock_type, rc
)
except Exception as exc:
log.debug("nvmlDeviceGetClockOffsets(type=%d): %s", clock_type, exc)
@@ -200,7 +217,9 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
if hasattr(pynvml, "nvmlDeviceGetMemClkVfOffset"):
try:
res = pynvml.nvmlDeviceGetMemClkVfOffset(handle)
out["mem_offset_mhz"] = int(res[0] if isinstance(res, (list, tuple)) else res)
out["mem_offset_mhz"] = int(
res[0] if isinstance(res, (list, tuple)) else res
)
except Exception as exc:
log.debug("nvmlDeviceGetMemClkVfOffset: %s", exc)
@@ -210,8 +229,8 @@ def get_clock_offsets(gpu_index: int = 0) -> dict:
def set_clock_offsets(
gpc_offset_mhz: Optional[int] = None,
mem_offset_mhz: Optional[int] = None,
gpc_offset_mhz: int | None = None,
mem_offset_mhz: int | None = None,
gpu_index: int = 0,
) -> tuple[bool, str]:
"""Set clock offsets (MHz) for the specified domains only.
@@ -235,20 +254,35 @@ def set_clock_offsets(
domains.append((_NVML_CLOCK_MEM, mem_offset_mhz))
_pynvml_set = getattr(pynvml, "nvmlDeviceSetClockOffsets", None)
fn_set = _try_nvml_fn("nvmlDeviceSetClockOffsets") if _pynvml_set is None else None
fn_set = (
_try_nvml_fn("nvmlDeviceSetClockOffsets") if _pynvml_set is None else None
)
if _pynvml_set is not None or fn_set is not None:
all_ok = True
for clock_type, offset in domains:
info = _make_clock_offset(clock_type, pstate=0, offset_mhz=offset)
try:
rc = _pynvml_set(handle, ctypes.byref(info)) if _pynvml_set else fn_set(handle, ctypes.byref(info))
if _pynvml_set is not None:
rc = _pynvml_set(handle, ctypes.byref(info))
elif fn_set is not None:
rc = fn_set(handle, ctypes.byref(info))
else:
break
if rc != 0:
log.debug("nvmlDeviceSetClockOffsets(type=%d) returned %d — trying fallback", clock_type, rc)
log.debug(
"nvmlDeviceSetClockOffsets(type=%d) returned %d — trying fallback",
clock_type,
rc,
)
all_ok = False
break
except Exception as exc:
log.debug("nvmlDeviceSetClockOffsets(type=%d): %s — trying fallback", clock_type, exc)
log.debug(
"nvmlDeviceSetClockOffsets(type=%d): %s — trying fallback",
clock_type,
exc,
)
all_ok = False
break
if all_ok:
@@ -257,12 +291,16 @@ def set_clock_offsets(
# Deprecated per-domain fallback (works on Blackwell/driver 590.x).
errs = []
if gpc_offset_mhz is not None and hasattr(pynvml, "nvmlDeviceSetGpcClkVfOffset"):
if gpc_offset_mhz is not None and hasattr(
pynvml, "nvmlDeviceSetGpcClkVfOffset"
):
try:
pynvml.nvmlDeviceSetGpcClkVfOffset(handle, gpc_offset_mhz)
except Exception as exc:
errs.append(f"GPC: {exc}")
if mem_offset_mhz is not None and hasattr(pynvml, "nvmlDeviceSetMemClkVfOffset"):
if mem_offset_mhz is not None and hasattr(
pynvml, "nvmlDeviceSetMemClkVfOffset"
):
try:
pynvml.nvmlDeviceSetMemClkVfOffset(handle, mem_offset_mhz)
except Exception as exc:
@@ -278,6 +316,7 @@ def set_clock_offsets(
# ── Range queries ─────────────────────────────────────────────────────────────
def get_mem_offset_range(gpu_index: int = 0) -> dict:
"""Return the min/max allowed memory clock offset (MHz).
@@ -285,7 +324,7 @@ def get_mem_offset_range(gpu_index: int = 0) -> dict:
Uses nvmlDeviceGetMemClkMinMaxVfOffset; falls back to observed RTX values.
"""
# Observed RTX 5090 defaults (NvAPI GetClockBoostRanges says -1000/+3000).
out = {"min_mem_offset_mhz": -2000, "max_mem_offset_mhz": 3000}
out: dict[str, int] = {"min_mem_offset_mhz": -2000, "max_mem_offset_mhz": 3000}
if not _NVML_AVAILABLE:
return out
try:
@@ -317,5 +356,3 @@ def get_mem_offset_range(gpu_index: int = 0) -> dict:
except Exception as exc:
log.debug("get_mem_offset_range: %s", exc)
return out
+61 -30
View File
@@ -2,18 +2,20 @@
import ctypes
import json
import logging
import os
import struct
from datetime import datetime
from typing import Optional
from ..nvapi.bootstrap import nvcall_raw
from ..nvapi.constants import FUNC, CT_SIZE, CT_BASE, CT_STRIDE, CT_DELTA_OFF, CT_POINTS
from ..nvapi.constants import CT_BASE, CT_DELTA_OFF, CT_SIZE, CT_STRIDE, FUNC
from ..nvapi.types import SnapshotInfo
from .vfcurve import read_clock_table_raw, get_boost_mask
from .vfcurve import get_boost_mask, read_clock_table_raw
log = logging.getLogger("nvcurve.hal.snapshot")
def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optional[str]:
def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> str | None:
"""Save the current ClockBoostTable to disk.
Writes both a binary .bin file and a human-readable .json metadata file.
@@ -25,13 +27,21 @@ def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optio
print(f"Failed to read ClockBoostTable: {err}")
return None
os.makedirs(snapshot_dir, exist_ok=True)
try:
os.makedirs(snapshot_dir, exist_ok=True)
except OSError as exc:
print(f"Failed to create snapshot dir {snapshot_dir}: {exc}")
return None
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
bin_path = os.path.join(snapshot_dir, f"clock_boost_table_{ts}.bin")
meta_path = os.path.join(snapshot_dir, f"clock_boost_table_{ts}.json")
with open(bin_path, "wb") as f:
f.write(raw)
try:
with open(bin_path, "wb") as f:
f.write(raw)
except OSError as exc:
print(f"Failed to write snapshot {bin_path}: {exc}")
return None
offsets = []
max_entries = (len(raw) - CT_BASE) // CT_STRIDE
@@ -48,10 +58,14 @@ def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optio
"offsets_kHz": offsets,
"nonzero_offsets": sum(1 for o in offsets if o != 0),
}
with open(meta_path, "w") as f:
json.dump(meta, f, indent=2)
try:
with open(meta_path, "w") as f:
json.dump(meta, f, indent=2)
except OSError as exc:
print(f"Failed to write snapshot metadata {meta_path}: {exc}")
return None
print(f"Snapshot saved:")
print("Snapshot saved:")
print(f" Binary: {bin_path}")
print(f" Metadata: {meta_path}")
print(f" Size: {len(raw)} bytes")
@@ -65,20 +79,22 @@ def save(gpu, gpu_name: str, snapshot_dir: str, max_snapshots: int = 0) -> Optio
def _prune_snapshots(snapshot_dir: str, max_snapshots: int) -> None:
"""Delete oldest snapshots (both .bin and .json) to stay within max_snapshots."""
bins = sorted(
f for f in os.listdir(snapshot_dir) if f.endswith(".bin")
) # oldest first (lexicographic = chronological for our timestamp format)
# Oldest first (lexicographic = chronological for our timestamp format).
try:
bins = sorted(f for f in os.listdir(snapshot_dir) if f.endswith(".bin"))
except OSError:
return
excess = len(bins) - max_snapshots
for fname in bins[:excess]:
stem = fname[:-4] # strip .bin
for ext in (".bin", ".json"):
try:
os.remove(os.path.join(snapshot_dir, stem + ext))
except OSError:
pass
except OSError as exc:
log.debug("Could not remove %s: %s", stem + ext, exc)
def restore(gpu, snapshot_dir: str, filepath: str = None) -> bool:
def restore(gpu, snapshot_dir: str, filepath: str | None = None) -> bool:
"""Restore a ClockBoostTable snapshot from disk.
If no filepath is given, uses the most recent snapshot in snapshot_dir.
@@ -88,10 +104,14 @@ def restore(gpu, snapshot_dir: str, filepath: str = None) -> bool:
if not os.path.isdir(snapshot_dir):
print(f"No snapshots found in {snapshot_dir}")
return False
bins = sorted(
[f for f in os.listdir(snapshot_dir) if f.endswith(".bin")],
reverse=True,
)
try:
bins = sorted(
[f for f in os.listdir(snapshot_dir) if f.endswith(".bin")],
reverse=True,
)
except OSError:
print(f"No snapshots found in {snapshot_dir}")
return False
if not bins:
print(f"No snapshot .bin files in {snapshot_dir}")
return False
@@ -101,8 +121,12 @@ def restore(gpu, snapshot_dir: str, filepath: str = None) -> bool:
print(f"Snapshot file not found: {filepath}")
return False
with open(filepath, "rb") as f:
raw = f.read()
try:
with open(filepath, "rb") as f:
raw = f.read()
except OSError as exc:
print(f"Failed to read snapshot {filepath}: {exc}")
return False
if len(raw) != CT_SIZE:
print(f"Snapshot size mismatch: expected {CT_SIZE}, got {len(raw)}")
@@ -134,8 +158,13 @@ def list_snapshots(snapshot_dir: str) -> list[SnapshotInfo]:
if not os.path.isdir(snapshot_dir):
return []
try:
fnames = sorted(os.listdir(snapshot_dir), reverse=True)
except OSError:
return []
results = []
for fname in sorted(os.listdir(snapshot_dir), reverse=True):
for fname in fnames:
if not fname.endswith(".json"):
continue
meta_path = os.path.join(snapshot_dir, fname)
@@ -143,13 +172,15 @@ def list_snapshots(snapshot_dir: str) -> list[SnapshotInfo]:
with open(meta_path) as f:
meta = json.load(f)
bin_path = meta.get("file", meta_path.replace(".json", ".bin"))
results.append(SnapshotInfo(
filepath=bin_path,
timestamp=meta.get("timestamp", ""),
gpu=meta.get("gpu", ""),
nonzero_offsets=meta.get("nonzero_offsets", 0),
size=meta.get("size", 0),
))
results.append(
SnapshotInfo(
filepath=bin_path,
timestamp=meta.get("timestamp", ""),
gpu=meta.get("gpu", ""),
nonzero_offsets=meta.get("nonzero_offsets", 0),
size=meta.get("size", 0),
)
)
except (json.JSONDecodeError, KeyError):
continue