Skip to content

Commit 2c4c3e0

Browse files
fix(unified): harden process cleanup and startup robustness
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/b7840be5-04fc-4c1e-b680-e75ec7b1ca3f Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
1 parent 5d82403 commit 2c4c3e0

File tree

1 file changed

+44
-16
lines changed

1 file changed

+44
-16
lines changed

src/praisonai/praisonai/cli/commands/unified.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
import atexit
1212
import signal
1313
from pathlib import Path
14-
from typing import Set
14+
from types import FrameType
15+
from typing import Set, TextIO
1516

1617
import typer
1718

@@ -40,6 +41,8 @@
4041

4142
# Global process tracking for cleanup
4243
_ACTIVE_PROCESSES: Set[subprocess.Popen] = set()
44+
_PROCESS_LOG_HANDLES: dict[subprocess.Popen, TextIO] = {}
45+
_CLEANUP_HANDLERS_REGISTERED = False
4346

4447
def _cleanup_processes():
4548
"""Cleanup all spawned processes on exit."""
@@ -48,17 +51,36 @@ def _cleanup_processes():
4851
if proc.poll() is None: # Process still running
4952
proc.terminate()
5053
proc.wait(timeout=5)
51-
except:
54+
except Exception:
5255
try:
5356
proc.kill()
54-
except:
57+
except Exception:
58+
pass
59+
log_handle = _PROCESS_LOG_HANDLES.pop(proc, None)
60+
if log_handle:
61+
try:
62+
log_handle.close()
63+
except Exception:
5564
pass
5665
_ACTIVE_PROCESSES.discard(proc)
5766

58-
# Register cleanup handlers
59-
atexit.register(_cleanup_processes)
60-
signal.signal(signal.SIGTERM, lambda s, f: _cleanup_processes())
61-
signal.signal(signal.SIGINT, lambda s, f: _cleanup_processes())
67+
def _resolve_check_host(host: str) -> str:
68+
return "127.0.0.1" if host == "0.0.0.0" else host
69+
70+
71+
def _handle_shutdown_signal(signum: int, frame: FrameType | None):
72+
_cleanup_processes()
73+
sys.exit(0)
74+
75+
76+
def _register_cleanup_handlers():
77+
global _CLEANUP_HANDLERS_REGISTERED
78+
if _CLEANUP_HANDLERS_REGISTERED:
79+
return
80+
atexit.register(_cleanup_processes)
81+
signal.signal(signal.SIGTERM, _handle_shutdown_signal)
82+
signal.signal(signal.SIGINT, _handle_shutdown_signal)
83+
_CLEANUP_HANDLERS_REGISTERED = True
6284

6385

6486
def _generate_dashboard_html(host: str = "localhost") -> str:
@@ -410,6 +432,7 @@ def unified(
410432

411433
# Create FastAPI app
412434
fastapi_app = FastAPI(title="PraisonAI Unified Dashboard")
435+
_register_cleanup_handlers()
413436

414437
@fastapi_app.get("/", response_class=HTMLResponse)
415438
async def dashboard():
@@ -426,45 +449,50 @@ async def start_service(service: str):
426449
# Check if service is already running
427450
import socket
428451
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
452+
check_host = _resolve_check_host(host)
429453
try:
430-
result = sock.connect_ex(("127.0.0.1", service_port))
431-
if result == 0:
432-
sock.close()
454+
connection_result = sock.connect_ex((check_host, service_port))
455+
if connection_result == 0:
433456
return {"success": True, "message": f"Service {service} already running on port {service_port}"}
434-
except:
457+
except OSError:
435458
pass
436459
finally:
437460
sock.close()
438461

462+
log_handle = None
439463
try:
440464
# Create log directory for troubleshooting
441465
log_dir = Path.home() / ".praisonai" / "unified" / "logs"
442466
log_dir.mkdir(parents=True, exist_ok=True)
443467
log_file = log_dir / f"{service}.log"
468+
log_handle = open(log_file, "a", encoding="utf-8")
444469

445470
if service == "flow":
446-
# Use proper Typer entry point for flow
471+
# Use praisonai module entrypoint so command resolution matches CLI behavior.
447472
proc = subprocess.Popen([
448473
sys.executable, "-m", "praisonai", "flow",
449474
"--port", str(service_port), "--no-open"
450-
], stdout=open(log_file, "w"), stderr=subprocess.STDOUT)
475+
], stdout=log_handle, stderr=subprocess.STDOUT)
451476
elif service == "claw":
452477
proc = subprocess.Popen([
453478
sys.executable, "-m", "praisonai", "claw",
454479
"--port", str(service_port)
455-
], stdout=open(log_file, "w"), stderr=subprocess.STDOUT)
480+
], stdout=log_handle, stderr=subprocess.STDOUT)
456481
elif service == "ui":
457482
proc = subprocess.Popen([
458483
sys.executable, "-m", "praisonai", "ui",
459484
"--port", str(service_port)
460-
], stdout=open(log_file, "w"), stderr=subprocess.STDOUT)
485+
], stdout=log_handle, stderr=subprocess.STDOUT)
461486

462487
# Track process for cleanup
463488
_ACTIVE_PROCESSES.add(proc)
489+
_PROCESS_LOG_HANDLES[proc] = log_handle
464490

465491
return {"success": True, "message": f"Started {service} on port {service_port}"}
466492

467493
except Exception as e:
494+
if log_handle and not log_handle.closed:
495+
log_handle.close()
468496
raise HTTPException(status_code=500, detail=f"Failed to start {service}: {str(e)}")
469497

470498
@fastapi_app.get("/health")
@@ -482,7 +510,7 @@ async def health():
482510
fastapi_app,
483511
host=host,
484512
port=port,
485-
log_level="info" # Show startup logs for debugging
513+
log_level="error"
486514
)
487515
except KeyboardInterrupt:
488516
console.print("\n[yellow]🌟 Unified Dashboard stopped.[/yellow]")

0 commit comments

Comments
 (0)