Spaces:
Build error
Build error
Update server_manager.py
Browse files- server_manager.py +84 -56
server_manager.py
CHANGED
|
@@ -11,7 +11,8 @@ import psutil
|
|
| 11 |
from functools import wraps
|
| 12 |
|
| 13 |
# Constants for stats monitoring
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
class MinecraftServerManager:
|
| 17 |
def __init__(self, socketio=None):
|
|
@@ -21,7 +22,11 @@ class MinecraftServerManager:
|
|
| 21 |
self.stopping = False
|
| 22 |
self.socketio = socketio
|
| 23 |
self.output_thread = None
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
self.console_history = []
|
| 26 |
self.max_history = 1000
|
| 27 |
self.start_time = None
|
|
@@ -33,6 +38,9 @@ class MinecraftServerManager:
|
|
| 33 |
'uptime': 0.0
|
| 34 |
}
|
| 35 |
self._lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
# --- Utility Methods ---
|
| 38 |
|
|
@@ -62,8 +70,6 @@ class MinecraftServerManager:
|
|
| 62 |
|
| 63 |
def _emit_event(self, event, data):
|
| 64 |
"""Emit a socketio event safely"""
|
| 65 |
-
# Note: We must use socketio.start_background_task or run in the main thread context
|
| 66 |
-
# when emitting from a non-Flask thread. Flask-SocketIO handles this.
|
| 67 |
if self.socketio:
|
| 68 |
self.socketio.emit(event, data)
|
| 69 |
|
|
@@ -92,8 +98,7 @@ class MinecraftServerManager:
|
|
| 92 |
|
| 93 |
self.starting = True
|
| 94 |
|
| 95 |
-
#
|
| 96 |
-
self._emit_status_update()
|
| 97 |
|
| 98 |
try:
|
| 99 |
# Ensure eula is accepted
|
|
@@ -123,18 +128,17 @@ class MinecraftServerManager:
|
|
| 123 |
self.output_thread = threading.Thread(target=self._read_output, daemon=True)
|
| 124 |
self.output_thread.start()
|
| 125 |
|
| 126 |
-
# Start stats monitoring thread
|
| 127 |
-
self.
|
| 128 |
-
self.
|
| 129 |
|
| 130 |
-
#
|
| 131 |
-
self._emit_status_update()
|
| 132 |
self.log("Server process started successfully")
|
| 133 |
return {"success": True, "message": "Server starting..."}
|
| 134 |
|
| 135 |
except Exception as e:
|
| 136 |
self.starting = False
|
| 137 |
-
self._emit_status_update()
|
| 138 |
self.log(f"Failed to start server: {str(e)}", "error")
|
| 139 |
return {"success": False, "message": str(e)}
|
| 140 |
|
|
@@ -146,8 +150,7 @@ class MinecraftServerManager:
|
|
| 146 |
|
| 147 |
self.stopping = True
|
| 148 |
|
| 149 |
-
#
|
| 150 |
-
self._emit_status_update()
|
| 151 |
|
| 152 |
try:
|
| 153 |
if force:
|
|
@@ -158,32 +161,32 @@ class MinecraftServerManager:
|
|
| 158 |
|
| 159 |
# Wait for graceful shutdown
|
| 160 |
for _ in range(30): # 30 second timeout
|
| 161 |
-
if self.process.poll() is not None:
|
| 162 |
break
|
| 163 |
time.sleep(1)
|
| 164 |
else:
|
| 165 |
# Force kill if not stopped
|
| 166 |
-
self.process
|
|
|
|
| 167 |
|
| 168 |
-
#
|
| 169 |
self.running = False
|
| 170 |
self.stopping = False
|
| 171 |
self.start_time = None
|
| 172 |
self.players_online = []
|
| 173 |
|
| 174 |
-
# Reset stats
|
| 175 |
self.server_stats = {'cpu': 0.0, 'memory': 0.0, 'tps': 0.0, 'uptime': 0.0}
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
self.
|
| 179 |
-
self._emit_player_update() # Push empty player list
|
| 180 |
self.log("Server stopped")
|
| 181 |
return {"success": True, "message": "Server stopped"}
|
| 182 |
|
| 183 |
except Exception as e:
|
| 184 |
self.stopping = False
|
| 185 |
self.log(f"Error stopping server: {str(e)}", "error")
|
| 186 |
-
self._emit_status_update()
|
| 187 |
return {"success": False, "message": str(e)}
|
| 188 |
|
| 189 |
def restart(self):
|
|
@@ -210,10 +213,65 @@ class MinecraftServerManager:
|
|
| 210 |
|
| 211 |
# --- Threaded Methods ---
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
def _read_output(self):
|
| 214 |
"""Read and process server output"""
|
| 215 |
try:
|
| 216 |
-
# Use 'iter' to continuously read the stream until the process closes
|
| 217 |
for line in iter(self.process.stdout.readline, ''):
|
| 218 |
if not line:
|
| 219 |
break
|
|
@@ -221,13 +279,13 @@ class MinecraftServerManager:
|
|
| 221 |
line = line.strip()
|
| 222 |
if line:
|
| 223 |
self._process_line(line)
|
| 224 |
-
self.log(line)
|
| 225 |
|
| 226 |
except Exception as e:
|
| 227 |
self.log(f"Error reading output: {str(e)}", "error")
|
| 228 |
finally:
|
| 229 |
self.log("Server process ended")
|
| 230 |
-
#
|
| 231 |
self.running = False
|
| 232 |
self.starting = False
|
| 233 |
self.stopping = False
|
|
@@ -265,36 +323,6 @@ class MinecraftServerManager:
|
|
| 265 |
with self._lock:
|
| 266 |
self.server_stats['tps'] = float(tps_match.group(1))
|
| 267 |
|
| 268 |
-
def _monitor_stats(self):
|
| 269 |
-
"""Monitor server resource usage"""
|
| 270 |
-
while self.running:
|
| 271 |
-
try:
|
| 272 |
-
if self.process and self.process.pid:
|
| 273 |
-
# Check if process is still running via poll
|
| 274 |
-
if self.process.poll() is not None:
|
| 275 |
-
self.running = False
|
| 276 |
-
self.log("Stats monitor detected server process termination.", "warn")
|
| 277 |
-
break
|
| 278 |
-
|
| 279 |
-
proc = psutil.Process(self.process.pid)
|
| 280 |
-
|
| 281 |
-
with self._lock:
|
| 282 |
-
self.server_stats['cpu'] = proc.cpu_percent(interval=None) # Non-blocking read
|
| 283 |
-
# Use get_memory_info() for reliable RSS value
|
| 284 |
-
self.server_stats['memory'] = proc.memory_info().rss / (1024 * 1024) # MB
|
| 285 |
-
|
| 286 |
-
if self.start_time:
|
| 287 |
-
self.server_stats['uptime'] = (datetime.now() - self.start_time).total_seconds()
|
| 288 |
-
|
| 289 |
-
# CRITICAL FIX: Emit stats update here periodically
|
| 290 |
-
self._emit_stats_update()
|
| 291 |
-
|
| 292 |
-
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
| 293 |
-
self.running = False
|
| 294 |
-
self.log("Stats monitor lost server process.", "warn")
|
| 295 |
-
|
| 296 |
-
time.sleep(STATS_INTERVAL)
|
| 297 |
-
|
| 298 |
# --- Client Data Methods ---
|
| 299 |
|
| 300 |
def get_status(self):
|
|
|
|
| 11 |
from functools import wraps
|
| 12 |
|
| 13 |
# Constants for stats monitoring
|
| 14 |
+
GLOBAL_STATUS_INTERVAL = 5 # seconds for global status/stats check
|
| 15 |
+
PROCESS_STATS_INTERVAL = 1 # seconds for detailed running process stats
|
| 16 |
|
| 17 |
class MinecraftServerManager:
|
| 18 |
def __init__(self, socketio=None):
|
|
|
|
| 22 |
self.stopping = False
|
| 23 |
self.socketio = socketio
|
| 24 |
self.output_thread = None
|
| 25 |
+
|
| 26 |
+
# Threads for live data
|
| 27 |
+
self.process_stats_thread = None # Runs only when MC server process is active
|
| 28 |
+
self.global_monitor_thread = None # Runs perpetually to send status updates
|
| 29 |
+
|
| 30 |
self.console_history = []
|
| 31 |
self.max_history = 1000
|
| 32 |
self.start_time = None
|
|
|
|
| 38 |
'uptime': 0.0
|
| 39 |
}
|
| 40 |
self._lock = threading.Lock()
|
| 41 |
+
|
| 42 |
+
# Start the global monitor immediately upon initialization
|
| 43 |
+
self.start_global_monitor()
|
| 44 |
|
| 45 |
# --- Utility Methods ---
|
| 46 |
|
|
|
|
| 70 |
|
| 71 |
def _emit_event(self, event, data):
|
| 72 |
"""Emit a socketio event safely"""
|
|
|
|
|
|
|
| 73 |
if self.socketio:
|
| 74 |
self.socketio.emit(event, data)
|
| 75 |
|
|
|
|
| 98 |
|
| 99 |
self.starting = True
|
| 100 |
|
| 101 |
+
self._emit_status_update() # Notify client 'Starting...' status
|
|
|
|
| 102 |
|
| 103 |
try:
|
| 104 |
# Ensure eula is accepted
|
|
|
|
| 128 |
self.output_thread = threading.Thread(target=self._read_output, daemon=True)
|
| 129 |
self.output_thread.start()
|
| 130 |
|
| 131 |
+
# Start the detailed process stats monitoring thread
|
| 132 |
+
self.process_stats_thread = threading.Thread(target=self._monitor_stats_process, daemon=True)
|
| 133 |
+
self.process_stats_thread.start()
|
| 134 |
|
| 135 |
+
self._emit_status_update() # Final status update (Online)
|
|
|
|
| 136 |
self.log("Server process started successfully")
|
| 137 |
return {"success": True, "message": "Server starting..."}
|
| 138 |
|
| 139 |
except Exception as e:
|
| 140 |
self.starting = False
|
| 141 |
+
self._emit_status_update()
|
| 142 |
self.log(f"Failed to start server: {str(e)}", "error")
|
| 143 |
return {"success": False, "message": str(e)}
|
| 144 |
|
|
|
|
| 150 |
|
| 151 |
self.stopping = True
|
| 152 |
|
| 153 |
+
self._emit_status_update() # Notify client 'Stopping...' status
|
|
|
|
| 154 |
|
| 155 |
try:
|
| 156 |
if force:
|
|
|
|
| 161 |
|
| 162 |
# Wait for graceful shutdown
|
| 163 |
for _ in range(30): # 30 second timeout
|
| 164 |
+
if self.process and self.process.poll() is not None:
|
| 165 |
break
|
| 166 |
time.sleep(1)
|
| 167 |
else:
|
| 168 |
# Force kill if not stopped
|
| 169 |
+
if self.process:
|
| 170 |
+
self.process.kill()
|
| 171 |
|
| 172 |
+
# Clean up state
|
| 173 |
self.running = False
|
| 174 |
self.stopping = False
|
| 175 |
self.start_time = None
|
| 176 |
self.players_online = []
|
| 177 |
|
| 178 |
+
# Reset stats to zero
|
| 179 |
self.server_stats = {'cpu': 0.0, 'memory': 0.0, 'tps': 0.0, 'uptime': 0.0}
|
| 180 |
+
|
| 181 |
+
self._emit_status_update()
|
| 182 |
+
self._emit_player_update()
|
|
|
|
| 183 |
self.log("Server stopped")
|
| 184 |
return {"success": True, "message": "Server stopped"}
|
| 185 |
|
| 186 |
except Exception as e:
|
| 187 |
self.stopping = False
|
| 188 |
self.log(f"Error stopping server: {str(e)}", "error")
|
| 189 |
+
self._emit_status_update()
|
| 190 |
return {"success": False, "message": str(e)}
|
| 191 |
|
| 192 |
def restart(self):
|
|
|
|
| 213 |
|
| 214 |
# --- Threaded Methods ---
|
| 215 |
|
| 216 |
+
def start_global_monitor(self):
|
| 217 |
+
"""Starts a persistent thread to monitor status and periodically update client."""
|
| 218 |
+
if self.global_monitor_thread is None or not self.global_monitor_thread.is_alive():
|
| 219 |
+
self.global_monitor_thread = threading.Thread(target=self._monitor_stats_global, daemon=True)
|
| 220 |
+
self.global_monitor_thread.start()
|
| 221 |
+
self.log("Global status monitor started.", "internal")
|
| 222 |
+
|
| 223 |
+
def _monitor_stats_global(self):
|
| 224 |
+
"""Monitors status and periodically emits status_update and stats_update."""
|
| 225 |
+
while True:
|
| 226 |
+
try:
|
| 227 |
+
# 1. Update Uptime if server is running
|
| 228 |
+
if self.running and self.start_time:
|
| 229 |
+
with self._lock:
|
| 230 |
+
self.server_stats['uptime'] = (datetime.now() - self.start_time).total_seconds()
|
| 231 |
+
|
| 232 |
+
# 2. Emit updates to keep the dashboard fresh
|
| 233 |
+
self._emit_status_update()
|
| 234 |
+
self._emit_stats_update()
|
| 235 |
+
|
| 236 |
+
except Exception as e:
|
| 237 |
+
# Log any errors but keep the monitor thread running
|
| 238 |
+
print(f"Global Stats Monitor Error: {e}")
|
| 239 |
+
|
| 240 |
+
time.sleep(GLOBAL_STATUS_INTERVAL)
|
| 241 |
+
|
| 242 |
+
def _monitor_stats_process(self):
|
| 243 |
+
"""Monitor server resource usage, running ONLY when the MC process is active."""
|
| 244 |
+
# Use a short interval for high-detail stats (1 sec)
|
| 245 |
+
while self.running:
|
| 246 |
+
try:
|
| 247 |
+
if self.process and self.process.pid:
|
| 248 |
+
# Check if process is still running via poll
|
| 249 |
+
if self.process.poll() is not None:
|
| 250 |
+
self.running = False
|
| 251 |
+
self.log("Process stats monitor detected server termination.", "warn")
|
| 252 |
+
break
|
| 253 |
+
|
| 254 |
+
proc = psutil.Process(self.process.pid)
|
| 255 |
+
|
| 256 |
+
with self._lock:
|
| 257 |
+
# Non-blocking read (CPU usage since last call)
|
| 258 |
+
self.server_stats['cpu'] = proc.cpu_percent(interval=None)
|
| 259 |
+
# RSS (Resident Set Size) in MB
|
| 260 |
+
self.server_stats['memory'] = proc.memory_info().rss / (1024 * 1024)
|
| 261 |
+
|
| 262 |
+
# We don't emit here; the global monitor will use these stats
|
| 263 |
+
# and emit them at the slower GLOBAL_STATUS_INTERVAL.
|
| 264 |
+
|
| 265 |
+
time.sleep(PROCESS_STATS_INTERVAL)
|
| 266 |
+
|
| 267 |
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
| 268 |
+
self.running = False
|
| 269 |
+
self.log("Stats monitor lost server process.", "warn")
|
| 270 |
+
break # Exit the process-specific loop
|
| 271 |
+
|
| 272 |
def _read_output(self):
|
| 273 |
"""Read and process server output"""
|
| 274 |
try:
|
|
|
|
| 275 |
for line in iter(self.process.stdout.readline, ''):
|
| 276 |
if not line:
|
| 277 |
break
|
|
|
|
| 279 |
line = line.strip()
|
| 280 |
if line:
|
| 281 |
self._process_line(line)
|
| 282 |
+
self.log(line)
|
| 283 |
|
| 284 |
except Exception as e:
|
| 285 |
self.log(f"Error reading output: {str(e)}", "error")
|
| 286 |
finally:
|
| 287 |
self.log("Server process ended")
|
| 288 |
+
# Reset state when the process dies
|
| 289 |
self.running = False
|
| 290 |
self.starting = False
|
| 291 |
self.stopping = False
|
|
|
|
| 323 |
with self._lock:
|
| 324 |
self.server_stats['tps'] = float(tps_match.group(1))
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
# --- Client Data Methods ---
|
| 327 |
|
| 328 |
def get_status(self):
|