Skip to content

Commit 2d535ef

Browse files
committed
v2.0.0 Port feature
1 parent cae71ab commit 2d535ef

9 files changed

Lines changed: 1029 additions & 388 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<!-- HTML_BLOCK: no change to url; output entire as it is... -->
2323
![License](https://img.shields.io/badge/license-MIT-orange.svg)
2424
![Python](https://img.shields.io/badge/python-3.10%2B-pink)
25-
![Version](https://img.shields.io/badge/version-1.4.0-green)
25+
![Version](https://img.shields.io/badge/version-2.0.0-green)
2626
![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-blue)
2727
![cuda 12.x](https://img.shields.io/badge/CUDA-12.x-0f9d58?logo=nvidia)
2828

monitor/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = "1.4.0"
1+
__version__ = "2.0.0"
22
__author__ = "DataBoySu"
33
__license__ = "MIT"

monitor/api/server.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import asyncio
88
import threading
99

10+
import psutil
1011
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
1112
from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse
1213
from fastapi.staticfiles import StaticFiles
@@ -259,7 +260,6 @@ async def _vram_cap_watcher():
259260
# collect processes and terminate those on this GPU and in watchlist
260261
try:
261262
proc_list = collector.collect_processes()
262-
import psutil
263263
for proc in proc_list:
264264
try:
265265
pid = int(proc.get('pid'))
@@ -527,6 +527,7 @@ async def get_status():
527527

528528
return {
529529
'status': 'healthy' if not alerts else 'warning',
530+
'is_admin': getattr(app.state, 'is_admin', False),
530531
'metrics': metrics,
531532
'alerts': alerts,
532533
'benchmark_status': bench_status if 'bench_status' in locals() else None,
@@ -598,6 +599,78 @@ async def update_processes_watchlist(payload: Dict[str, Any]):
598599
599600
JSON: { "pid": 1234, "action": "add"|"remove" }
600601
"""
602+
if not getattr(app.state, 'is_admin', False):
603+
return {"status": "error", "message": "Admin privileges required"}
604+
605+
pid = payload.get('pid')
606+
action = payload.get('action')
607+
if not pid:
608+
return {"status": "error", "message": "PID required"}
609+
610+
wl = set(getattr(app.state, 'vram_watchlist', []) or [])
611+
if action == 'add':
612+
wl.add(int(pid))
613+
elif action == 'remove':
614+
wl.discard(int(pid))
615+
616+
app.state.vram_watchlist = list(wl)
617+
_save_vram_watchlist(list(wl))
618+
return {'status': 'success', 'watchlist': list(wl)}
619+
620+
@app.get("/api/ports")
621+
async def get_ports():
622+
sc = SystemCollector()
623+
return {'ports': sc.collect_ports()}
624+
625+
@app.post("/api/processes/terminate")
626+
async def terminate_process(payload: Dict[str, Any]):
627+
"""Terminate a process by PID (Admin only)."""
628+
if not getattr(app.state, 'is_admin', False):
629+
return {"status": "error", "message": "Admin privileges required"}
630+
631+
pid = payload.get('pid')
632+
action = payload.get('action', 'kill') # 'free' (soft) or 'kill' (full)
633+
if not pid:
634+
return {"status": "error", "message": "PID required"}
635+
636+
try:
637+
p = psutil.Process(int(pid))
638+
639+
if action == 'free':
640+
# "Free Port" - softer approach, targeting specifically the process holding it
641+
# We'll just use terminate() which is a SIGTERM (graceful)
642+
p.terminate()
643+
try:
644+
p.wait(timeout=2)
645+
return {"status": "success", "message": f"Port freed: Process {pid} terminated gracefully."}
646+
except psutil.TimeoutExpired:
647+
# If it doesn't die gracefully, we don't force it in 'free' mode unless requested?
648+
# The user said "decouple the process from the port",
649+
# usually that means killing it. I'll stick to a softer terminate.
650+
return {"status": "warning", "message": f"Process {pid} received terminate signal but is still active."}
651+
else:
652+
# "Terminate" - full kill including tree if possible
653+
try:
654+
parent = p.parent()
655+
# If it's a child process, maybe kill parent too if it's a worker?
656+
# Actually, usually users want to kill THE app.
657+
p.kill() # SIGKILL
658+
if parent and parent.pid > 0:
659+
# Optional: also kill parent if requested?
660+
# The user said "preferably specific child of process holding the port" for free,
661+
# and "terminate should kill the process itself".
662+
pass
663+
return {"status": "success", "message": f"Terminated PID {pid}."}
664+
except Exception as e:
665+
p.kill()
666+
return {"status": "success", "message": f"Killed PID {pid}."}
667+
668+
except psutil.NoSuchProcess:
669+
return {"status": "error", "message": f"Process {pid} not found."}
670+
except psutil.AccessDenied:
671+
return {"status": "error", "message": f"Access denied for PID {pid}. System process?"}
672+
except Exception as e:
673+
return {"status": "error", "message": str(e)}
601674
try:
602675
pid = int(payload.get('pid'))
603676
except Exception:

0 commit comments

Comments
 (0)