Add a GPU process table to the Performance tab showing which processes use the selected GPU, with per-process VRAM, GPU utilization, CPU, host memory, and command. Includes a kill endpoint (SIGTERM/SIGKILL, with optional parent kill) behind a confirm dialog. - nvcurve/hal/processes.py: NVML + /proc process enumeration - server.py: GET /api/processes, POST /api/processes/kill (refuses to signal init/kthreadd) - frontend: ProcessList component, API client, types - tests: test_processes.py - docs: API reference for the new endpoints
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
import type {
|
|
CurveState,
|
|
GpuInfo,
|
|
MonitoringSample,
|
|
SnapshotInfo,
|
|
LimitsState,
|
|
ProfileData,
|
|
FanState,
|
|
FanPoint,
|
|
DashboardInfo,
|
|
ProcessListResponse,
|
|
} from "../types.js";
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
// Global handler invoked when any API call returns 401 (not logged in, or the
|
|
// 24-hour session expired). The app uses it to re-show the login screen.
|
|
let unauthorizedHandler: (() => void) | null = null;
|
|
export function onUnauthorized(handler: (() => void) | null): void {
|
|
unauthorizedHandler = handler;
|
|
}
|
|
|
|
function handleUnauthorized(res: Response): void {
|
|
if (res.status === 401 && unauthorizedHandler) {
|
|
unauthorizedHandler();
|
|
}
|
|
}
|
|
|
|
async function get<T>(path: string, gpuIndex?: number): Promise<T> {
|
|
const url =
|
|
gpuIndex !== undefined
|
|
? `/api${path}?gpu_index=${gpuIndex}`
|
|
: `/api${path}`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
handleUnauthorized(res);
|
|
const text = await res.text().catch(() => res.statusText);
|
|
throw new ApiError(res.status, `GET ${url}: ${res.status} — ${text}`);
|
|
}
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
async function post<T = void>(
|
|
path: string,
|
|
body?: unknown,
|
|
gpuIndex?: number,
|
|
): Promise<T> {
|
|
const url =
|
|
gpuIndex !== undefined
|
|
? `/api${path}?gpu_index=${gpuIndex}`
|
|
: `/api${path}`;
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: body != null ? { "Content-Type": "application/json" } : {},
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (!res.ok) {
|
|
handleUnauthorized(res);
|
|
const text = await res.text().catch(() => res.statusText);
|
|
throw new ApiError(res.status, `POST ${url}: ${res.status} — ${text}`);
|
|
}
|
|
// Some endpoints return no body (204)
|
|
const ct = res.headers.get("content-type") ?? "";
|
|
if (ct.includes("application/json")) return res.json() as Promise<T>;
|
|
// SAFETY: non-JSON responses (e.g. 204) carry no body; callers of these
|
|
// endpoints use T = void and ignore the result, so undefined is a valid T.
|
|
return undefined as unknown as T;
|
|
}
|
|
|
|
async function del<T = void>(path: string, gpuIndex?: number): Promise<T> {
|
|
const url =
|
|
gpuIndex !== undefined
|
|
? `/api${path}?gpu_index=${gpuIndex}`
|
|
: `/api${path}`;
|
|
const res = await fetch(url, { method: "DELETE" });
|
|
if (!res.ok) {
|
|
handleUnauthorized(res);
|
|
const text = await res.text().catch(() => res.statusText);
|
|
throw new ApiError(res.status, `DELETE ${url}: ${res.status} — ${text}`);
|
|
}
|
|
const ct = res.headers.get("content-type") ?? "";
|
|
if (ct.includes("application/json")) return res.json() as Promise<T>;
|
|
// SAFETY: non-JSON responses (e.g. 204) carry no body; callers of these
|
|
// endpoints use T = void and ignore the result, so undefined is a valid T.
|
|
return undefined as unknown as T;
|
|
}
|
|
|
|
export interface AuthStatus {
|
|
auth_required: boolean;
|
|
authenticated: boolean;
|
|
username: string | null;
|
|
expires_at: number | null;
|
|
}
|
|
|
|
export const api = {
|
|
/** Auth */
|
|
authStatus: () => get<AuthStatus>("/auth/status"),
|
|
login: (username: string, password: string) =>
|
|
post<{ ok: boolean; username: string; expires_at: number; token: string }>(
|
|
"/auth/login",
|
|
{ username, password },
|
|
),
|
|
logout: () => post("/auth/logout"),
|
|
|
|
gpus: () => get<GpuInfo[]>("/gpus"),
|
|
gpu: (gpuIndex: number) => get<GpuInfo>("/gpu", gpuIndex),
|
|
dashboard: (gpuIndex: number) => get<DashboardInfo>("/dashboard", gpuIndex),
|
|
curve: (gpuIndex: number) => get<CurveState>("/curve", gpuIndex),
|
|
ranges: (gpuIndex: number) =>
|
|
get<Record<string, { min_khz: number; max_khz: number }>>(
|
|
"/ranges",
|
|
gpuIndex,
|
|
),
|
|
voltage: (gpuIndex: number) =>
|
|
get<{ voltage_uv: number; voltage_mv: number }>("/voltage", gpuIndex),
|
|
monitor: (gpuIndex: number) => get<MonitoringSample>("/monitor", gpuIndex),
|
|
processes: (gpuIndex: number) =>
|
|
get<ProcessListResponse>("/processes", gpuIndex),
|
|
killProcess: (
|
|
pid: number,
|
|
signal: "TERM" | "KILL" = "TERM",
|
|
parent = false,
|
|
) =>
|
|
post<{ ok: boolean; pid: number; signal: string; parent: boolean }>(
|
|
"/processes/kill",
|
|
{ pid, signal, parent },
|
|
),
|
|
snapshots: (gpuIndex: number) => get<SnapshotInfo[]>("/snapshots", gpuIndex),
|
|
|
|
/** Write per-point frequency deltas. deltas: { pointIndex: deltaKhz } */
|
|
writeDeltas: (deltas: Record<number, number>, gpuIndex: number) =>
|
|
post<{ ok: boolean; freq_warnings?: string[] }>(
|
|
"/curve/write",
|
|
{ deltas },
|
|
gpuIndex,
|
|
),
|
|
|
|
/** Reset all frequency deltas to zero. */
|
|
resetCurve: (gpuIndex: number) => post("/curve/reset", undefined, gpuIndex),
|
|
|
|
/** Get performance limits mapping */
|
|
limits: (gpuIndex: number) => get<LimitsState>("/limits", gpuIndex),
|
|
|
|
/** Set performance limits */
|
|
updateLimits: (updates: Partial<LimitsState>, gpuIndex: number) =>
|
|
post("/limits", updates, gpuIndex),
|
|
|
|
/** Reset power limit and memory offset to hardware defaults */
|
|
resetLimits: (gpuIndex: number) => post("/limits/reset", undefined, gpuIndex),
|
|
|
|
/** Profile Management */
|
|
profiles: (gpuIndex: number) =>
|
|
get<{
|
|
profiles: ProfileData[];
|
|
active: string | null;
|
|
auto_load: string | null;
|
|
}>("/profiles", gpuIndex),
|
|
saveProfile: (name: string, gpuIndex: number) =>
|
|
post<{ ok: boolean; filepath: string }>("/profiles", { name }, gpuIndex),
|
|
applyProfile: (name: string, gpuIndex: number) =>
|
|
post(`/profiles/${encodeURIComponent(name)}/apply`, undefined, gpuIndex),
|
|
deleteProfile: (name: string) => del(`/profiles/${encodeURIComponent(name)}`),
|
|
renameProfile: (oldName: string, newName: string) =>
|
|
post(`/profiles/${encodeURIComponent(oldName)}/rename`, {
|
|
new_name: newName,
|
|
}),
|
|
|
|
/** Server config */
|
|
setAutoLoadProfile: (name: string | null, gpuIndex: number) =>
|
|
post<{ ok: boolean; auto_load_profile: string | null }>("/config", {
|
|
auto_load_profile: name,
|
|
gpu_index: gpuIndex,
|
|
}),
|
|
|
|
/** Fan control */
|
|
fans: (gpuIndex: number) => get<FanState>("/fans", gpuIndex),
|
|
updateFans: (curve: FanPoint[], gpuIndex: number, fans?: number[] | null) =>
|
|
post("/fans", { curve, fans: fans ?? null }, gpuIndex),
|
|
resetFans: (gpuIndex: number) => post("/fans/reset", undefined, gpuIndex),
|
|
setFanSpeed: (fanPct: number, gpuIndex: number, fan?: number | null) =>
|
|
post("/fans/speed", { fan_pct: fanPct, fan: fan ?? null }, gpuIndex),
|
|
};
|