-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMinecraftServer.py
More file actions
339 lines (279 loc) · 13.1 KB
/
Copy pathMinecraftServer.py
File metadata and controls
339 lines (279 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
import tkinter
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from tkinter.messagebox import showerror
from PIL import ImageTk, Image
import threading
import os
import subprocess
from datetime import datetime
import shutil
import re
import urllib.request
import io
import atexit
import psutil
import time
import json
def get_time(format: str = "%Y_%m_%d-%H_%M_%S") -> str:
return datetime.now().strftime(format)
def kill_process_tree(pid):
"""Tue un processus et tous ses enfants."""
try:
parent = psutil.Process(pid)
for child in parent.children(recursive=True): # Récupère tous les processus enfants
child.terminate()
parent.terminate()
except psutil.NoSuchProcess:
pass
def extract_content(message: str) -> str:
pattern_tag = re.compile(
r"\[([0-9]+(:[0-9]+)+) (TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|SEVERE|FATAL)]: ",
re.IGNORECASE
)
return pattern_tag.sub("", message).strip()
def check_log_level(line: str) -> str:
pattern_tag = re.compile(
r"\[([0-9]+(:[0-9]+)+) (TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|SEVERE|FATAL)]",
re.IGNORECASE
)
if match := pattern_tag.match(line):
return match.group(3)
return "LOG"
def is_op(server_path, uuid):
with open(server_path+"/ops.json", "r") as f:
data = json.load(f)
for player in data:
if player["uuid"] == uuid:
return True
return False
class MinecraftServer:
def __init__(self, root, config, console_text, start_button, stop_button, backup_button, kill_button, PWD,
auto_scroll_var, command_entry, player_list):
self.stop_requested = None
self.server_process = None
self.root = root
self.config = config
self.console_text = console_text
self.start_button = start_button
self.stop_button = stop_button
self.kill_button = kill_button
self.backup_button = backup_button
self.PWD = PWD
self.auto_scroll_var = auto_scroll_var
self.command_entry = command_entry
self.player_list = player_list
self.players = {}
def start_server(self):
if self.server_process is not None:
self.log_message("Le serveur est déjà en cours d'exécution.", "WARN")
return
try:
if self.config.get("server_path"):
os.chdir(self.config.get("server_path"))
self.server_process = subprocess.Popen(
[self.config.get("java_path"), f"-Xmx{self.config.get("ram")}M", "-jar", self.config.get("server_jar"),
"nogui"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
creationflags=subprocess.CREATE_NO_WINDOW
)
atexit.register(self.server_process.kill)
self.log_message("Démarrage du serveur.", "LOG")
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.kill_button.config(state=tk.NORMAL)
self.backup_button.config(state=tk.DISABLED)
server_log_parser = ServerLogParser(self.server_process, self.log_message, self.player_list, self.players, self.config)
threading.Thread(target=server_log_parser.monitor_server_process, daemon=True).start()
os.chdir(self.PWD)
except Exception as e:
self.log_message(f"Erreur lors du démarrage du serveur : {e}", "WARN")
messagebox.showerror("Erreur", "Impossible de démarrer le serveur.")
def stop_server(self):
if self.server_process:
self.stop_requested = True
self.send_command("stop")
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.kill_button.config(state=tk.DISABLED)
self.backup_button.config(state=tk.NORMAL)
self.log_message("Serveur Minecraft arrêté.", "LOG")
self.server_process.wait()
self.server_process = None
else:
self.log_message("Aucun serveur n'est en cours d'exécution.", "WARN")
# Fonction pour arrêter le serveur
def kill_server(self):
if self.server_process:
kill_process_tree(self.server_process.pid)
self.server_process.wait()
self.server_process = None
try:
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.kill_button.config(state=tk.DISABLED)
self.backup_button.config(state=tk.NORMAL)
except tkinter.TclError:
pass
self.log_message("Processus Java tué", "LOG")
self.log_message("Serveur Minecraft arrêté.", "LOG")
else:
self.log_message("Aucun serveur n'est en cours d'exécution.", "WARN")
def backup_server(self):
if self.server_process:
showerror("Backup", "Le serveur est en fonctionnement, arretez le pour effetuer la backup")
return
self.log_message("Starting Backup Now", "LOG")
backup_path = os.path.join(self.PWD, "..", "backup")
if not os.path.exists(backup_path):
os.mkdir(backup_path)
now = get_time()
shutil.make_archive(os.path.join(backup_path, f"Backup-{now}"), "zip", self.PWD)
self.log_message("Server backed up", "LOG")
def log_message(self, message, tag="LOG", mc_console: bool = False):
if not mc_console:
now = get_time("%H:%M:%S")
time = f"[{now} {tag}]"
message = f"{time}: {message}"
try:
self.console_text.config(state=tk.NORMAL)
self.console_text.insert(tk.END, message + "\n", tag)
self.console_text.config(state=tk.DISABLED)
if self.auto_scroll_var.get() == 1:
self.console_text.yview(tk.END)
return
except tkinter.TclError:
pass
def send_command(self, command=None):
if not command or not isinstance(command, str):
command = self.command_entry.get()
if not self.server_process:
self.log_message("Serveur non démarré.", "WARN")
elif not command:
self.log_message("Commande vide.", "WARN")
else:
try:
self.server_process.stdin.write(command + "\n")
self.server_process.stdin.flush()
self.log_message(f"Commande envoyée : {command}", "INFO")
self.command_entry.delete(0, tk.END)
except Exception as e:
self.log_message(f"Erreur d'envoi de commande : {e}", "WARN")
def kick(self):
current_item = self.player_list.focus()
player_name = self.player_list.item(current_item)["text"]
if player_name:
self.send_command(f"kick {player_name}")
def ban(self):
current_item = self.player_list.focus()
player_name = self.player_list.item(current_item)["text"]
if player_name:
self.send_command(f"ban {player_name}")
class ServerLogParser:
def __init__(self, server_process, log_message, player_treeview: ttk.Treeview, players, config):
"""
:param server_process: The running Minecraft server process.
:param log_message: Reference to the existing logging function (self.log_message).
"""
self.stop_requested = None
self.server_process = server_process
self.log_message = log_message # Use your existing logging function
self.players_heads = {}
self.player_treeview = player_treeview
self.config = config
# Define regex patterns for parsing different events
self.log_patterns = [
{"pattern": r"UUID of player (\w+) is ([a-f0-9\-]+)", "handler": self.handle_player_uuid},
{"pattern": r"(\w+)\[\/([\d\.]+):\d+\] logged in with entity id (\d+) at \(\[([\w]+)\]([\d\.\-]+), ([\d\.\-]+), ([\d\.\-]+)\)", "handler": self.handle_player_login},
{"pattern": r"(\w+) issued server command: (.+)", "handler": self.handle_command},
{"pattern": r"(\w+) was slain by (\w+)", "handler": self.handle_death},
{"pattern": r"(\w+) left the game", "handler": self.handle_player_leave},
{"pattern": r"ERROR\]: (.+)", "handler": self.handle_server_error},
{"pattern": r"\[\d{2}:\d{2}:\d{2} INFO\]: \[@: The entity UUID provided is in an invalid format\]",
"handler": self.handle_exclusion},
{"pattern": r"\[@: Given ([A-Za-z\s]+) \(ID (\d+)\) \* (\d+) to ([A-Za-z0-9]+) for (\d+) seconds\]",
"handler": self.handle_effect},
{"pattern": r"Changing to (\w+)\sweather", "handler": self.handle_exclusion},
{"pattern": r"(?<=Opped\s)(\w+)", "handler": self.handle_opped},
{"pattern": r"(?<=De-opped\s)(\w+)", "handler": self.handle_deopped}
]
def handle_player_uuid(self, match):
player_name, uuid = match.groups()
# noinspection PyRedundantParentheses
tags = "op" if is_op(self.config.get("server_path"), uuid) else ""
head = self.get_player_head(uuid)
self.player_treeview.insert("", "end", text=player_name, image=head, tags=tags)
self.log_message(f"🟢 Player Joined: {player_name} (UUID: {uuid})", "INFO")
def handle_player_login(self, match):
player_name, ip, entity_id, world, x, y, z = match.groups()
self.log_message(f"🌍 {player_name} logged in from {ip} (Entity ID: {entity_id}) at {world} ({x}, {y}, {z})",
"INFO")
def handle_command(self, match):
player_name, command = match.groups()
self.log_message(f"🛠 Command Executed: {player_name} -> {command}", "COMMAND")
def handle_death(self, match):
victim, killer = match.groups()
self.log_message(f"☠️ Death: {victim} was killed by {killer}", "DEATH")
def handle_player_leave(self, match):
player_name = match.group(1)
for item in self.player_treeview.get_children(): # Parcours de tous les éléments
if self.player_treeview.item(item, "text") == player_name: # Comparaison du texte
self.player_treeview.delete(item) # Suppression de l'élément
break
self.log_message(f"👋 {player_name} left the game", "INFO")
def handle_server_error(self, match):
error_message = match.group(1)
self.log_message(f"🚨 Server Error: {error_message}", "ERROR")
def handle_exclusion(self, match):
pass
def handle_effect(self, match):
pass
def handle_opped(self, match):
player_name = match.group(1)
for item in self.player_treeview.get_children(): # Parcours de tous les éléments
if self.player_treeview.item(item, "text") == player_name: # Comparaison du texte
self.player_treeview.item(item, tags="op")
def handle_deopped(self, match):
player_name = match.group(1)
for item in self.player_treeview.get_children(): # Parcours de tous les éléments
if self.player_treeview.item(item, "text") == player_name: # Comparaison du texte
self.player_treeview.item(item, tags="")
def process_log_line(self, line):
"""Check the line against all patterns and execute the matched handler."""
for entry in self.log_patterns:
match = re.search(entry["pattern"], line)
if match:
entry["handler"](match)
return
self.log_message(line, mc_console=True)
def monitor_server_process(self):
"""Monitor the Minecraft server console in real-time and parse events."""
self.stop_requested = False
crash_detected = False
while self.server_process:
exit_code = self.server_process.poll() # Check if the server has stopped
# Process server console output
if self.server_process.stdout:
for line in self.server_process.stdout:
line = line.strip()
if line:
self.process_log_line(line) # Use regex-based log parsing
if exit_code is not None: # The server has stopped
if self.stop_requested:
self.log_message("✅ The server stopped normally.", "INFO")
else:
if crash_detected:
self.log_message(f"⚠️ The server crashed (Exit code: {exit_code})", "ERROR")
# Add GUI pop-up or additional handling here if needed
self.server_process = None
break
time.sleep(1) # Check logs every second
def get_player_head(self, uuid):
with urllib.request.urlopen(f"https://mc-heads.net/avatar/{uuid}") as url:
raw_data = url.read()
head = Image.open(io.BytesIO(raw_data))
head = head.resize((16, 16))
tk_head = ImageTk.PhotoImage(head)
self.players_heads[uuid] = tk_head
return tk_head