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
Files changed (1) hide show
  1. 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
  # ---------------------------------------------------------------------------