Spaces:
Sleeping
Sleeping
Cyber Catalyst Team commited on
Commit ·
4571bf6
1
Parent(s): ebf8c61
Expose /api/backup/download and implement system watchdog to clean up hung Claude Code processes
Browse files- backend.py +71 -0
backend.py
CHANGED
|
@@ -1200,6 +1200,31 @@ async def get_models_status():
|
|
| 1200 |
return status_list
|
| 1201 |
|
| 1202 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
@app.get("/health")
|
| 1204 |
async def health():
|
| 1205 |
return {
|
|
@@ -1213,6 +1238,52 @@ async def health():
|
|
| 1213 |
}
|
| 1214 |
|
| 1215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1216 |
# ---------------------------------------------------------------------------
|
| 1217 |
# Entrypoint
|
| 1218 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1200 |
return status_list
|
| 1201 |
|
| 1202 |
|
| 1203 |
+
import shutil
|
| 1204 |
+
import threading
|
| 1205 |
+
import signal
|
| 1206 |
+
from fastapi.responses import FileResponse
|
| 1207 |
+
|
| 1208 |
+
@app.get("/api/backup/download")
|
| 1209 |
+
async def download_backup(authorization: str = Header(None)):
|
| 1210 |
+
auth(authorization)
|
| 1211 |
+
archive_base = "/tmp/workspace_backup_download"
|
| 1212 |
+
archive_zip = archive_base + ".zip"
|
| 1213 |
+
if os.path.exists(archive_zip):
|
| 1214 |
+
try:
|
| 1215 |
+
os.unlink(archive_zip)
|
| 1216 |
+
except Exception:
|
| 1217 |
+
pass
|
| 1218 |
+
|
| 1219 |
+
try:
|
| 1220 |
+
shutil.make_archive(archive_base, 'zip', WORKSPACE_DIR)
|
| 1221 |
+
if not os.path.exists(archive_zip):
|
| 1222 |
+
raise HTTPException(status_code=500, detail="Failed to create zip archive")
|
| 1223 |
+
return FileResponse(archive_zip, media_type="application/zip", filename="workspace_backup.zip")
|
| 1224 |
+
except Exception as e:
|
| 1225 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1226 |
+
|
| 1227 |
+
|
| 1228 |
@app.get("/health")
|
| 1229 |
async def health():
|
| 1230 |
return {
|
|
|
|
| 1238 |
}
|
| 1239 |
|
| 1240 |
|
| 1241 |
+
# ---------------------------------------------------------------------------
|
| 1242 |
+
# Watchdog Daemon for Claude Code Subprocesses
|
| 1243 |
+
# ---------------------------------------------------------------------------
|
| 1244 |
+
|
| 1245 |
+
def run_watchdog():
|
| 1246 |
+
log_activity("System Watchdog Daemon started")
|
| 1247 |
+
while True:
|
| 1248 |
+
try:
|
| 1249 |
+
import psutil
|
| 1250 |
+
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'create_time']):
|
| 1251 |
+
try:
|
| 1252 |
+
cmd = " ".join(proc.info['cmdline'] or [])
|
| 1253 |
+
if "claude-code" in cmd.lower() or "anthropic" in cmd.lower():
|
| 1254 |
+
elapsed = time.time() - proc.info['create_time']
|
| 1255 |
+
if elapsed > 600: # 10 minutes limit
|
| 1256 |
+
log_activity(f"[Watchdog SIGKILL] Reaping hung Claude Code process PID {proc.info['pid']} (Active for {elapsed:.1f}s)")
|
| 1257 |
+
proc.kill()
|
| 1258 |
+
except Exception:
|
| 1259 |
+
continue
|
| 1260 |
+
except ImportError:
|
| 1261 |
+
# Fallback zero-dependency shell parser
|
| 1262 |
+
try:
|
| 1263 |
+
out = subprocess.check_output("ps -o pid,etime,args | grep -E 'claude-code|anthropic' | grep -v grep", shell=True, text=True)
|
| 1264 |
+
for line in out.strip().split("\n"):
|
| 1265 |
+
parts = line.strip().split(None, 2)
|
| 1266 |
+
if len(parts) >= 2:
|
| 1267 |
+
pid = int(parts[0])
|
| 1268 |
+
etime = parts[1]
|
| 1269 |
+
# Check if running > 10 mins (format dd-hh:mm:ss or mm:ss)
|
| 1270 |
+
is_stale = "-" in etime or len(etime.split(":")) > 2 or (len(etime.split(":")) == 2 and int(etime.split(":")[0]) > 10)
|
| 1271 |
+
if is_stale:
|
| 1272 |
+
log_activity(f"[Watchdog SIGKILL Fallback] Reaping hung process PID {pid} (etime: {etime})")
|
| 1273 |
+
os.kill(pid, signal.SIGKILL)
|
| 1274 |
+
except Exception:
|
| 1275 |
+
pass
|
| 1276 |
+
except Exception as e:
|
| 1277 |
+
log_activity(f"[Watchdog Error] {e}")
|
| 1278 |
+
time.sleep(60)
|
| 1279 |
+
|
| 1280 |
+
|
| 1281 |
+
@app.on_event("startup")
|
| 1282 |
+
async def startup_event():
|
| 1283 |
+
# Start the watchdog thread on startup
|
| 1284 |
+
threading.Thread(target=run_watchdog, daemon=True).start()
|
| 1285 |
+
|
| 1286 |
+
|
| 1287 |
# ---------------------------------------------------------------------------
|
| 1288 |
# Entrypoint
|
| 1289 |
# ---------------------------------------------------------------------------
|