pyservectl init
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
PyServe CLI State Management
|
||||
|
||||
Manages the state of running services.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceHealth:
|
||||
status: str = "unknown" # healthy, unhealthy, degraded, unknown
|
||||
last_check: Optional[float] = None
|
||||
failures: int = 0
|
||||
response_time_ms: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceState:
|
||||
name: str
|
||||
state: str = "stopped" # pending, starting, running, stopping, stopped, failed, restarting
|
||||
pid: Optional[int] = None
|
||||
port: int = 0
|
||||
workers: int = 0
|
||||
started_at: Optional[float] = None
|
||||
restart_count: int = 0
|
||||
health: ServiceHealth = field(default_factory=ServiceHealth)
|
||||
config_hash: str = ""
|
||||
|
||||
@property
|
||||
def uptime(self) -> float:
|
||||
if self.started_at is None:
|
||||
return 0.0
|
||||
return time.time() - self.started_at
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"state": self.state,
|
||||
"pid": self.pid,
|
||||
"port": self.port,
|
||||
"workers": self.workers,
|
||||
"started_at": self.started_at,
|
||||
"restart_count": self.restart_count,
|
||||
"health": asdict(self.health),
|
||||
"config_hash": self.config_hash,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ServiceState":
|
||||
health_data = data.pop("health", {})
|
||||
health = ServiceHealth(**health_data) if health_data else ServiceHealth()
|
||||
return cls(**data, health=health)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectState:
|
||||
version: str = "1.0"
|
||||
project: str = ""
|
||||
config_file: str = ""
|
||||
config_hash: str = ""
|
||||
started_at: Optional[float] = None
|
||||
daemon_pid: Optional[int] = None
|
||||
services: Dict[str, ServiceState] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"version": self.version,
|
||||
"project": self.project,
|
||||
"config_file": self.config_file,
|
||||
"config_hash": self.config_hash,
|
||||
"started_at": self.started_at,
|
||||
"daemon_pid": self.daemon_pid,
|
||||
"services": {name: svc.to_dict() for name, svc in self.services.items()},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ProjectState":
|
||||
services_data = data.pop("services", {})
|
||||
services = {name: ServiceState.from_dict(svc) for name, svc in services_data.items()}
|
||||
return cls(**data, services=services)
|
||||
|
||||
|
||||
class StateManager:
|
||||
STATE_FILE = "state.json"
|
||||
PID_FILE = "pyserve.pid"
|
||||
SOCKET_FILE = "pyserve.sock"
|
||||
LOGS_DIR = "logs"
|
||||
|
||||
def __init__(self, state_dir: Path, project: Optional[str] = None):
|
||||
self.state_dir = Path(state_dir)
|
||||
self.project = project or self._detect_project()
|
||||
self._state: Optional[ProjectState] = None
|
||||
|
||||
def _detect_project(self) -> str:
|
||||
return Path.cwd().name
|
||||
|
||||
@property
|
||||
def state_file(self) -> Path:
|
||||
return self.state_dir / self.STATE_FILE
|
||||
|
||||
@property
|
||||
def pid_file(self) -> Path:
|
||||
return self.state_dir / self.PID_FILE
|
||||
|
||||
@property
|
||||
def socket_file(self) -> Path:
|
||||
return self.state_dir / self.SOCKET_FILE
|
||||
|
||||
@property
|
||||
def logs_dir(self) -> Path:
|
||||
return self.state_dir / self.LOGS_DIR
|
||||
|
||||
def ensure_dirs(self) -> None:
|
||||
self.state_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def load(self) -> ProjectState:
|
||||
if self._state is not None:
|
||||
return self._state
|
||||
|
||||
if self.state_file.exists():
|
||||
try:
|
||||
with open(self.state_file) as f:
|
||||
data = json.load(f)
|
||||
self._state = ProjectState.from_dict(data)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
self._state = ProjectState(project=self.project)
|
||||
else:
|
||||
self._state = ProjectState(project=self.project)
|
||||
|
||||
return self._state
|
||||
|
||||
def save(self) -> None:
|
||||
if self._state is None:
|
||||
return
|
||||
|
||||
self.ensure_dirs()
|
||||
|
||||
with open(self.state_file, "w") as f:
|
||||
json.dump(self._state.to_dict(), f, indent=2)
|
||||
|
||||
def get_state(self) -> ProjectState:
|
||||
return self.load()
|
||||
|
||||
def update_service(self, name: str, **kwargs) -> ServiceState:
|
||||
state = self.load()
|
||||
|
||||
if name not in state.services:
|
||||
state.services[name] = ServiceState(name=name)
|
||||
|
||||
service = state.services[name]
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(service, key):
|
||||
setattr(service, key, value)
|
||||
|
||||
self.save()
|
||||
return service
|
||||
|
||||
def remove_service(self, name: str) -> None:
|
||||
state = self.load()
|
||||
if name in state.services:
|
||||
del state.services[name]
|
||||
self.save()
|
||||
|
||||
def get_service(self, name: str) -> Optional[ServiceState]:
|
||||
state = self.load()
|
||||
return state.services.get(name)
|
||||
|
||||
def get_all_services(self) -> Dict[str, ServiceState]:
|
||||
state = self.load()
|
||||
return state.services.copy()
|
||||
|
||||
def clear(self) -> None:
|
||||
self._state = ProjectState(project=self.project)
|
||||
self.save()
|
||||
|
||||
def is_daemon_running(self) -> bool:
|
||||
if not self.pid_file.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
pid = int(self.pid_file.read_text().strip())
|
||||
# Check if process exists
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except (ValueError, ProcessLookupError, PermissionError):
|
||||
return False
|
||||
|
||||
def get_daemon_pid(self) -> Optional[int]:
|
||||
if not self.is_daemon_running():
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(self.pid_file.read_text().strip())
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def set_daemon_pid(self, pid: int) -> None:
|
||||
self.ensure_dirs()
|
||||
self.pid_file.write_text(str(pid))
|
||||
|
||||
state = self.load()
|
||||
state.daemon_pid = pid
|
||||
self.save()
|
||||
|
||||
def clear_daemon_pid(self) -> None:
|
||||
if self.pid_file.exists():
|
||||
self.pid_file.unlink()
|
||||
|
||||
state = self.load()
|
||||
state.daemon_pid = None
|
||||
self.save()
|
||||
|
||||
def get_service_log_file(self, service_name: str) -> Path:
|
||||
self.ensure_dirs()
|
||||
return self.logs_dir / f"{service_name}.log"
|
||||
|
||||
def compute_config_hash(self, config_file: str) -> str:
|
||||
import hashlib
|
||||
|
||||
path = Path(config_file)
|
||||
if not path.exists():
|
||||
return ""
|
||||
|
||||
content = path.read_bytes()
|
||||
return hashlib.sha256(content).hexdigest()[:16]
|
||||
Reference in New Issue
Block a user