mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 13:35:12 +09:00
- Extend Application model with source, compose_file, systemd_unit, managed_by, version, and upload_path columns. - Add migration 017_manual_apps for the new columns. - Add POST /apps/manual to register existing server paths/systemd units. - Add POST /apps/upload with versioning, rollback, and .env preservation. - Update DockerService compose helpers to accept a custom compose_file. - Update ProcessService to query systemd units and compose projects. - Update apps API start/stop/restart/status/delete to honor compose_file and avoid deleting unmanaged manual paths. - Add Manual/Local and Upload ZIP source modes to NewService. - Show source badges in Services list and disable Git controls for manual/upload apps in ServiceDetail. - Add version list, rollback, and new-version upload UI for upload apps.
219 lines
8.5 KiB
Python
219 lines
8.5 KiB
Python
import psutil
|
|
import subprocess
|
|
import platform
|
|
from typing import List, Dict, Optional
|
|
|
|
from app.utils.system import run_privileged
|
|
|
|
|
|
class ProcessService:
|
|
"""Service for process and service management."""
|
|
|
|
# Common services to monitor
|
|
MONITORED_SERVICES = [
|
|
'nginx',
|
|
'mysql', 'mysqld', 'mariadb',
|
|
'postgresql', 'postgres',
|
|
'redis', 'redis-server',
|
|
'docker', 'dockerd',
|
|
'php-fpm', 'php-fpm8.2', 'php-fpm8.1', 'php-fpm8.0',
|
|
'gunicorn',
|
|
'supervisor', 'supervisord'
|
|
]
|
|
|
|
@classmethod
|
|
def get_processes(cls, limit: int = 50, sort_by: str = 'cpu') -> List[Dict]:
|
|
"""Get list of running processes sorted by resource usage."""
|
|
processes = []
|
|
|
|
for proc in psutil.process_iter(['pid', 'name', 'username', 'cpu_percent',
|
|
'memory_percent', 'status', 'create_time']):
|
|
try:
|
|
pinfo = proc.info
|
|
# Filter out idle processes
|
|
if pinfo['cpu_percent'] > 0 or pinfo['memory_percent'] > 0.1:
|
|
processes.append({
|
|
'pid': pinfo['pid'],
|
|
'name': pinfo['name'],
|
|
'username': pinfo['username'],
|
|
'cpu_percent': round(pinfo['cpu_percent'], 1),
|
|
'memory_percent': round(pinfo['memory_percent'], 2),
|
|
'status': pinfo['status'],
|
|
'create_time': pinfo['create_time']
|
|
})
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
continue
|
|
|
|
# Sort by specified field
|
|
sort_key = 'cpu_percent' if sort_by == 'cpu' else 'memory_percent'
|
|
processes.sort(key=lambda x: x[sort_key], reverse=True)
|
|
|
|
return processes[:limit]
|
|
|
|
@classmethod
|
|
def get_process_details(cls, pid: int) -> Optional[Dict]:
|
|
"""Get detailed information about a specific process."""
|
|
try:
|
|
proc = psutil.Process(pid)
|
|
with proc.oneshot():
|
|
return {
|
|
'pid': proc.pid,
|
|
'name': proc.name(),
|
|
'status': proc.status(),
|
|
'username': proc.username(),
|
|
'cpu_percent': proc.cpu_percent(),
|
|
'memory_percent': round(proc.memory_percent(), 2),
|
|
'memory_info': {
|
|
'rss': proc.memory_info().rss,
|
|
'vms': proc.memory_info().vms
|
|
},
|
|
'create_time': proc.create_time(),
|
|
'cmdline': proc.cmdline(),
|
|
'cwd': proc.cwd() if hasattr(proc, 'cwd') else None,
|
|
'num_threads': proc.num_threads(),
|
|
'connections': len(proc.connections()) if hasattr(proc, 'connections') else 0
|
|
}
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
return None
|
|
|
|
@classmethod
|
|
def kill_process(cls, pid: int, force: bool = False) -> Dict:
|
|
"""Kill a process by PID."""
|
|
try:
|
|
proc = psutil.Process(pid)
|
|
name = proc.name()
|
|
|
|
if force:
|
|
proc.kill()
|
|
else:
|
|
proc.terminate()
|
|
|
|
return {'success': True, 'message': f'Process {name} (PID: {pid}) terminated'}
|
|
except psutil.NoSuchProcess:
|
|
return {'success': False, 'error': f'Process {pid} not found'}
|
|
except psutil.AccessDenied:
|
|
return {'success': False, 'error': f'Access denied to kill process {pid}'}
|
|
|
|
@classmethod
|
|
def get_services_status(cls) -> List[Dict]:
|
|
"""Get status of monitored services."""
|
|
services = []
|
|
running_procs = {p.name().lower(): p for p in psutil.process_iter(['name', 'pid'])}
|
|
|
|
for service_name in cls.MONITORED_SERVICES:
|
|
status = 'stopped'
|
|
pid = None
|
|
|
|
# Check if service is running
|
|
for proc_name, proc in running_procs.items():
|
|
if service_name.lower() in proc_name.lower():
|
|
status = 'running'
|
|
pid = proc.pid
|
|
break
|
|
|
|
services.append({
|
|
'name': service_name,
|
|
'status': status,
|
|
'pid': pid
|
|
})
|
|
|
|
return services
|
|
|
|
@classmethod
|
|
def control_service(cls, service_name: str, action: str) -> Dict:
|
|
"""Control a system service (start, stop, restart, reload)."""
|
|
valid_actions = ['start', 'stop', 'restart', 'reload', 'status']
|
|
if action not in valid_actions:
|
|
return {'success': False, 'error': f'Invalid action. Must be one of: {valid_actions}'}
|
|
|
|
system = platform.system()
|
|
|
|
try:
|
|
if system == 'Linux':
|
|
# Try systemctl first (systemd)
|
|
result = run_privileged(
|
|
['systemctl', action, service_name], timeout=30
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
# Fall back to service command
|
|
result = run_privileged(
|
|
['service', service_name, action], timeout=30
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return {'success': True, 'message': f'Service {service_name} {action} successful'}
|
|
else:
|
|
return {'success': False, 'error': result.stderr or 'Command failed'}
|
|
|
|
elif system == 'Windows':
|
|
if action == 'status':
|
|
cmd = ['sc', 'query', service_name]
|
|
else:
|
|
action_map = {'start': 'start', 'stop': 'stop', 'restart': 'restart'}
|
|
cmd = ['net', action_map.get(action, action), service_name]
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
return {
|
|
'success': result.returncode == 0,
|
|
'message': result.stdout if result.returncode == 0 else result.stderr
|
|
}
|
|
|
|
else:
|
|
return {'success': False, 'error': f'Unsupported platform: {system}'}
|
|
|
|
except FileNotFoundError:
|
|
return {'success': False, 'error': 'systemctl/service command not found'}
|
|
except subprocess.TimeoutExpired:
|
|
return {'success': False, 'error': 'Command timed out'}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
@classmethod
|
|
def get_service_logs(cls, service_name: str, lines: int = 100) -> Dict:
|
|
"""Get recent logs for a service via LogService fallback chain."""
|
|
from app.services.log_service import LogService
|
|
return LogService.get_journalctl_logs(unit=service_name, lines=lines)
|
|
|
|
@classmethod
|
|
def get_systemd_unit_status(cls, unit_name: str) -> Dict:
|
|
"""Check whether a systemd unit is active using systemctl."""
|
|
system = platform.system()
|
|
if system != 'Linux':
|
|
return {'success': True, 'status': 'unknown', 'active': False}
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
['systemctl', 'is-active', unit_name],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
active = result.returncode == 0 and result.stdout.strip() == 'active'
|
|
status = result.stdout.strip() if result.stdout else 'unknown'
|
|
return {'success': True, 'status': status, 'active': active}
|
|
except FileNotFoundError:
|
|
return {'success': False, 'error': 'systemctl not found'}
|
|
except subprocess.TimeoutExpired:
|
|
return {'success': False, 'error': 'Command timed out'}
|
|
except Exception as e:
|
|
return {'success': False, 'error': str(e)}
|
|
|
|
@classmethod
|
|
def get_compose_project_status(cls, project_path: str, compose_file: str = None) -> Dict:
|
|
"""Return running/stopped status for a Docker Compose project directory."""
|
|
from app.services.docker_service import DockerService
|
|
containers = DockerService.compose_ps(project_path, compose_file=compose_file)
|
|
running = sum(
|
|
1 for c in containers
|
|
if (c.get('Status', c.get('status', '')).startswith('Up'))
|
|
)
|
|
total = len(containers)
|
|
if total == 0:
|
|
actual = 'stopped'
|
|
elif running == total:
|
|
actual = 'running'
|
|
elif running > 0:
|
|
actual = 'partial'
|
|
else:
|
|
actual = 'stopped'
|
|
return {'success': True, 'status': actual, 'running': running, 'total': total, 'containers': containers}
|