smodusermc commited on
Commit
f663fd1
·
verified ·
1 Parent(s): 538a9d7

Update server_manager.py

Browse files
Files changed (1) hide show
  1. 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
- STATS_INTERVAL = 5 # seconds
 
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
- self.stats_thread = None
 
 
 
 
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
- # CRITICAL FIX: Emit status change immediately
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.stats_thread = threading.Thread(target=self._monitor_stats, daemon=True)
128
- self.stats_thread.start()
129
 
130
- # CRITICAL FIX: Final status update
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() # Notify client of failure
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
- # CRITICAL FIX: Emit status change immediately
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.kill()
 
167
 
168
- # Ensure threads are cleaned up
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
- self._emit_stats_update() # Push zero stats
177
-
178
- self._emit_status_update() # Final status update (Offline)
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() # Revert status if error
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) # Logs the line and emits 'console_output'
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
- # CRITICAL FIX: Reset state when the process dies unexpectedly
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):