Add color logs and some fix.

This commit is contained in:
2026-05-18 23:29:58 +08:00
parent 8a3b1b1638
commit 8bd09688bb
3 changed files with 122 additions and 24 deletions
+54 -4
View File
@@ -3,6 +3,7 @@ import asyncio
import base64 import base64
import binascii import binascii
import queue import queue
import re
import shutil import shutil
import ssl import ssl
import sys import sys
@@ -13,6 +14,7 @@ import traceback
from pathlib import Path from pathlib import Path
from prompt_toolkit import Application from prompt_toolkit import Application
from prompt_toolkit.layout import Layout, HSplit from prompt_toolkit.layout import Layout, HSplit
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.widgets import TextArea from prompt_toolkit.widgets import TextArea
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.styles import Style from prompt_toolkit.styles import Style
@@ -131,11 +133,51 @@ class ServerJarClient(Application):
# Text style # Text style
self.style = Style.from_dict({ self.style = Style.from_dict({
"log": "bg:#000000 #ffffff",
"input": "bg:#222222 #ffffff", "input": "bg:#222222 #ffffff",
"separator-area": "bg:#000000 #ffffff", "separator-area": "bg:#000000 #ffffff",
"message-area": "bg:#111111 #ffffff" "message-area": "bg:#111111 #ffffff",
"log": "bg:#000000 #ffffff",
"warning": "bg:#000000 ansiyellow",
"error": "bg:#000000 ansibrightred bold",
"system": "bg:#000000 ansicyan",
"process-log": "bg:#000000 ansigreen",
"process-error": "bg:#000000 ansired",
"unknown": "bg:#000000 ansiwhite bold",
}) })
class LogLexer(Lexer):
tag_pattern = re.compile(r"^\[([A-Za-z0-9_. -]+)([:|][A-Za-z0-9_. -]+)?\]")
line_style = {
"auth_err": "class:error",
"auth_ok": "class:process-log",
"auth_required": "class:warning",
"client": "class:log",
"client|err": "class:error",
"client|warn": "class:warning",
"download_log_begin": "class:system",
"download_log_end": "class:system",
"err": "class:process-error",
"log": "class:process-log",
"ok": "class:process-log",
"sys": "class:system",
"sys:err": "class:error",
"unknown": "class:unknown",
}
def lex_document(self, document):
def get_line(lineno):
line = document.lines[lineno]
match = self.tag_pattern.match(line)
tag = "unknown"
if match:
tag = (match.group(1) + (match.group(2) or "")).lower()
style = self.line_style.get(tag, self.line_style["unknown"])
return [(style, line)]
return get_line
# Socket # Socket
self.sock = None self.sock = None
@@ -155,6 +197,7 @@ class ServerJarClient(Application):
self.log_area = TextArea( self.log_area = TextArea(
style="class:log", style="class:log",
wrap_lines=True, wrap_lines=True,
lexer=LogLexer(),
) )
self.separator_area = TextArea(text="=" * 10 + " Enter Command Here " + "=" * 10, height=1, self.separator_area = TextArea(text="=" * 10 + " Enter Command Here " + "=" * 10, height=1,
@@ -368,7 +411,7 @@ class ServerJarClient(Application):
def _help(cmd): def _help(cmd):
for key, value in cmd_map.items(): for key, value in cmd_map.items():
self._log(f"{key}: {value.get("description")}") self._log(f"{key}: {value.get('description')}")
return True return True
def _clear(cmd): def _clear(cmd):
@@ -506,8 +549,15 @@ class ServerJarClient(Application):
# def clear_screen(): # def clear_screen():
# os.system("cls" if os.name == "nt" else "clear") # os.system("cls" if os.name == "nt" else "clear")
@staticmethod
def _ensure_log_tag(message):
message = f"{message}"
if re.match(r"^\[[A-Za-z0-9_. -]+(?:[:|][A-Za-z0-9_. -]+)?\]", message):
return message
return f"[unknown] {message}"
def log(self, message): def log(self, message):
self.incoming.put(f"{message}") self.incoming.put(self._ensure_log_tag(message))
def _log(self, message): def _log(self, message):
# Nothing change # Nothing change
+66 -20
View File
@@ -4,7 +4,6 @@ ServerJar
Wei - 2026 Wei - 2026
""" """
import re import re
import shlex
import signal import signal
import socketserver import socketserver
import logging import logging
@@ -21,7 +20,7 @@ import hmac
from pathlib import Path from pathlib import Path
import click import click
import yaml import yaml
from utils.common import download_latest_paper_jar, get_latest_version_minecraft, get_specific_version_paper_builds, \ from utils.common import get_latest_version_minecraft, get_specific_version_paper_builds, \
download_server_jar, download_latest_build_paper_jar, get_latest_paper_version, download_vanilla_server_jar download_server_jar, download_latest_build_paper_jar, get_latest_paper_version, download_vanilla_server_jar
from utils.file_settings import FileSettings from utils.file_settings import FileSettings
from utils.file_settings import required_list, required_value from utils.file_settings import required_list, required_value
@@ -33,7 +32,7 @@ from cryptography.hazmat.primitives import serialization
ROOT_DIR = Path(os.getcwd()) ROOT_DIR = Path(os.getcwd())
SERVER_CONFIG_PATH = ROOT_DIR / "config" / "server.yml" SERVER_CONFIG_PATH = ROOT_DIR / "config" / "server.yml"
VERSION = "1.0" VERSION = "1.1"
LOG_DIR_NAME = "logs" LOG_DIR_NAME = "logs"
SERVERJAR_LOG_FILE = "serverjar.log" SERVERJAR_LOG_FILE = "serverjar.log"
LOG_DOWNLOAD_CHUNK_SIZE = 4096 LOG_DOWNLOAD_CHUNK_SIZE = 4096
@@ -42,7 +41,6 @@ def exit(message):
click.echo(click.style(message, fg='green')) click.echo(click.style(message, fg='green'))
sys.exit(0) sys.exit(0)
@click.group() @click.group()
def main(): def main():
print(f"ServerJar v{VERSION}" print(f"ServerJar v{VERSION}"
@@ -243,6 +241,12 @@ def create_server(name, mc_version, build, server_type, snapshot, latest, list_b
print("Done") print("Done")
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.clients = []
self.clients_lock = threading.Lock()
class SocketServer: class SocketServer:
def __init__(self, host, port, enable_tls, certfile: Path, keyfile: Path, password: str = ""): def __init__(self, host, port, enable_tls, certfile: Path, keyfile: Path, password: str = ""):
self.logger = logging.getLogger("SocketServer") self.logger = logging.getLogger("SocketServer")
@@ -259,7 +263,7 @@ class SocketServer:
self.host = host self.host = host
self.port = port self.port = port
self._tcp_server: socketserver.ThreadingTCPServer | None = None self.tcp_server: ThreadedTCPServer | None = None
self._tcp_thread: threading.Thread | None = None self._tcp_thread: threading.Thread | None = None
self._log_subscribers: set[queue.Queue] = set() self._log_subscribers: set[queue.Queue] = set()
@@ -293,14 +297,16 @@ class SocketServer:
# ------------------------- # -------------------------
def publish_log(self, server_name: str, line: str | None = None): def publish_log(self, server_name: str, line: str | None = None):
if line is None: if line is None:
log_server = None
message = server_name message = server_name
else: else:
log_server = server_name
message = f"[{server_name}] {line}" message = f"[{server_name}] {line}"
with self._sub_lock: with self._sub_lock:
for q in list(self._log_subscribers): for q in list(self._log_subscribers):
try: try:
q.put_nowait(message) q.put_nowait((log_server, message))
except queue.Full: except queue.Full:
pass pass
@@ -316,10 +322,18 @@ class SocketServer:
def _format_command_help(self, command_map): def _format_command_help(self, command_map):
return "\n".join( return "\n".join(
f"{command}: {description}" f"[SYS] {command}: {description}"
for command, description in command_map.items() for command, description in command_map.items()
) )
@staticmethod
def broadcast_all(socks, message):
for sock in list(socks):
try:
sock.sendall((message + "\n").encode("utf-8"))
except OSError:
pass
def get_socket_help_message(self, current_server=None): def get_socket_help_message(self, current_server=None):
socket_commands = { socket_commands = {
"__help": "Display this help message", "__help": "Display this help message",
@@ -335,6 +349,8 @@ class SocketServer:
"__status": "Show target server process status", "__status": "Show target server process status",
"__stop": "Stop the target server process", "__stop": "Stop the target server process",
"__start": "Start the target server process", "__start": "Start the target server process",
"__restart": "Restart the target server process",
"__info": "Show server information",
"<minecraft command>": "Send command to the target Minecraft server process", "<minecraft command>": "Send command to the target Minecraft server process",
} }
@@ -355,7 +371,7 @@ class SocketServer:
server_names = sorted(self.command_receivers.keys()) server_names = sorted(self.command_receivers.keys())
return "[SYS] Available server shells:\n" + "\n".join( return "[SYS] Available server shells:\n" + "\n".join(
f"- {server_name}" for server_name in server_names f"[SYS] - {server_name}" for server_name in server_names
) )
def handler_command(self, command: str): def handler_command(self, command: str):
@@ -437,7 +453,7 @@ class SocketServer:
def _build_tcp_server(self): def _build_tcp_server(self):
manager = self manager = self
class TCPServer(socketserver.ThreadingTCPServer): class TCPServer(ThreadedTCPServer):
allow_reuse_address = True allow_reuse_address = True
daemon_threads = True daemon_threads = True
@@ -466,6 +482,20 @@ class SocketServer:
mgr: Server = self.server.manager mgr: Server = self.server.manager
mgr.logger.info(f"[SYS] Client from {self.client_address[0]}:{self.client_address[1]} connected,") mgr.logger.info(f"[SYS] Client from {self.client_address[0]}:{self.client_address[1]} connected,")
with self.server.clients_lock:
self.server.clients.append(self.request)
def finish(self):
with self.server.clients_lock:
try:
self.server.clients.remove(self.request)
except ValueError:
pass
self.current_server_record.pop(
f"{self.client_address[0]}:{self.client_address[1]}",
None,
)
super().finish()
def handle(self): def handle(self):
mgr: Server = self.server.manager mgr: Server = self.server.manager
@@ -477,9 +507,16 @@ class SocketServer:
def push_logs(): def push_logs():
while not stop_evt.is_set(): while not stop_evt.is_set():
try: try:
line = log_q.get(timeout=0.5) log_server, line = log_q.get(timeout=0.5)
except Exception: except Exception:
continue continue
current_server = self.current_server_record.get(
f"{self.client_address[0]}:{self.client_address[1]}",
None,
)
# Only push client connected server's log
if log_server is not None and log_server != current_server:
continue
try: try:
self.request.sendall(f"[LOG] {line}\n".encode("utf-8")) self.request.sendall(f"[LOG] {line}\n".encode("utf-8"))
except OSError: except OSError:
@@ -563,6 +600,7 @@ class SocketServer:
self.request.sendall( self.request.sendall(
f"[SYS] Stopping all servers...bye\n".encode("utf-8") f"[SYS] Stopping all servers...bye\n".encode("utf-8")
) )
mgr.broadcast_all(mgr.tcp_server.clients, "[SYS] Server stopping... (Stop by remote client)")
mgr.stop_event.set() mgr.stop_event.set()
return return
elif cmd.startswith("__sync_log"): elif cmd.startswith("__sync_log"):
@@ -646,26 +684,27 @@ class SocketServer:
return TCPServer((self.host, self.port), Handler) return TCPServer((self.host, self.port), Handler)
def start_socket_server(self): def start_socket_server(self):
if self._tcp_server: if self.tcp_server:
print("[SOCK] already running") print("[SOCK] already running")
return return
self._tcp_server = self._build_tcp_server() self.tcp_server = self._build_tcp_server()
def loop(): def loop():
self.logger.info(f"[SOCK] listening on {self.host}:{self.port}") self.logger.info(f"[SOCK] listening on {self.host}:{self.port}")
self._tcp_server.serve_forever(poll_interval=0.5) self.tcp_server.serve_forever(poll_interval=0.5)
self._tcp_thread = threading.Thread(target=loop, daemon=True) self._tcp_thread = threading.Thread(target=loop, daemon=True)
self._tcp_thread.start() self._tcp_thread.start()
def stop_socket_server(self): def stop_socket_server(self):
if not self._tcp_server: if not self.tcp_server:
return return
self.logger.info("[SOCK] shutting down") self.logger.info("[SOCK] shutting down")
self._tcp_server.shutdown() self.broadcast_all(self.tcp_server.clients, "[SYS] Server stopping...")
self._tcp_server.server_close() self.tcp_server.shutdown()
self._tcp_server = None self.tcp_server.server_close()
self.tcp_server = None
if self._tcp_thread and self._tcp_thread.is_alive(): if self._tcp_thread and self._tcp_thread.is_alive():
self._tcp_thread.join(timeout=2) self._tcp_thread.join(timeout=2)
self._tcp_thread = None self._tcp_thread = None
@@ -940,6 +979,7 @@ class Server:
"__stop: Stop the target server process\n" "__stop: Stop the target server process\n"
"__start: Start the target server process\n" "__start: Start the target server process\n"
"__restart: Restart the target server process\n" "__restart: Restart the target server process\n"
"__info: Show target server process status\n"
"<minecraft command>: Send command to the target Minecraft server process") "<minecraft command>: Send command to the target Minecraft server process")
elif command == "__status": elif command == "__status":
with self.proc_lock: with self.proc_lock:
@@ -953,9 +993,15 @@ class Server:
elif command == "__start": elif command == "__start":
self.start() self.start()
return True, f"Server \"{self.name}\" process started" return True, f"Server \"{self.name}\" process started"
elif command == "__restart": elif command == "__info":
self.restart() return True, (f"serverName: {self.name}\n"
return True, f"Server \"{self.name}\" process restarted" f"serverPID: {self.proc.pid}\n"
f"description: {self.description}\n"
f"arguments: {self.args}\n"
f"host: {self.host}\n"
f"port: {self.port}\n"
f"version: {self.version}\n"
f"status: {"Running" if self.is_process_alive() else "Died"}")
else: else:
return False, f"Unknown command: {command}" return False, f"Unknown command: {command}"
+2
View File
@@ -0,0 +1,2 @@
prompt-toolkit==3.0.52
cryptography==48.0.0