Some changes.
This commit is contained in:
@@ -16,11 +16,13 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
import datetime
|
||||
import base64
|
||||
import hmac
|
||||
from pathlib import Path
|
||||
import click
|
||||
import yaml
|
||||
from utils.common import download_latest_paper_jar, get_latest_version_minecraft, get_specific_version_paper_builds, \
|
||||
download_server_jar, download_latest_build_paper_jar, get_latest_paper_version
|
||||
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 required_list, required_value
|
||||
from cryptography import x509
|
||||
@@ -32,6 +34,9 @@ from cryptography.hazmat.primitives import serialization
|
||||
ROOT_DIR = Path(os.getcwd())
|
||||
SERVER_CONFIG_PATH = ROOT_DIR / "config" / "server.yml"
|
||||
VERSION = "1.0"
|
||||
LOG_DIR_NAME = "logs"
|
||||
SERVERJAR_LOG_FILE = "serverjar.log"
|
||||
LOG_DOWNLOAD_CHUNK_SIZE = 4096
|
||||
|
||||
def exit(message):
|
||||
click.echo(click.style(message, fg='green'))
|
||||
@@ -50,11 +55,13 @@ def load_settings():
|
||||
{
|
||||
"servers": [],
|
||||
"socketServerHostname": "127.0.0.1",
|
||||
"socketServerPort": 25560
|
||||
"socketServerPort": 25560,
|
||||
"socketServerPassword": ""
|
||||
},
|
||||
{
|
||||
"socketServerHostname": required_value("127.0.0.1"),
|
||||
"socketServerPort": required_value(25560),
|
||||
"socketServerPassword": required_value(""),
|
||||
"enableTLSSupport": required_value(True),
|
||||
"socketServerCertfile": required_value("data/server-public.pem"),
|
||||
"socketServerKeyfile": required_value("data/server-private.pem"),
|
||||
@@ -92,9 +99,14 @@ def load_settings():
|
||||
required=False)
|
||||
@click.option("--build", "-b", default=None,
|
||||
help="Specify paper build to download (Use latest Minecraft version if not specified)")
|
||||
@click.option("--server-type", "-t",
|
||||
type=click.Choice(["paper", "vanilla"], case_sensitive=False),
|
||||
default="paper",
|
||||
show_default=True,
|
||||
help="Server jar type to download")
|
||||
@click.option("--snapshot", is_flag=True,
|
||||
help="Download snapshot version Minecraft (Use it if the current mc-version type is snapshot)")
|
||||
@click.option("--latest", is_flag=True, help="Download latest Minecraft version (With latest build paper)")
|
||||
@click.option("--latest", is_flag=True, help="Download latest Minecraft version")
|
||||
@click.option("--list-builds", is_flag=True, help="List available paper build versions")
|
||||
@click.option("--filename", default=None, help="Custom SERVER.jar file name")
|
||||
@click.option("--extra-args", "-e",
|
||||
@@ -116,26 +128,43 @@ def load_settings():
|
||||
help="Hostname of the server", required=True)
|
||||
@click.option("--server-port", "-srp",
|
||||
help="Port of the server", required=True)
|
||||
def create_server(name, mc_version, build, snapshot, latest, list_builds, filename, extra_args, java_exec_path,
|
||||
def create_server(name, mc_version, build, server_type, snapshot, latest, list_builds, filename, extra_args, java_exec_path,
|
||||
x_memory_initial, x_memory_maximum, nogui, custom_args, server_port, server_host):
|
||||
server_dir = Path("servers", name)
|
||||
server_type = server_type.lower()
|
||||
|
||||
if server_dir.exists():
|
||||
result = str(input("Found existing server dir. Do you want to overwrite it and continue? [y/N] "))
|
||||
def prepare_server_dir():
|
||||
if server_dir.exists():
|
||||
result = str(input("Found existing server dir. Do you want to overwrite it and continue? [y/N] "))
|
||||
|
||||
if not result.lower() == "y":
|
||||
exit("User aborted.")
|
||||
if not result.lower() == "y":
|
||||
exit("User aborted.")
|
||||
|
||||
server_dir.mkdir(parents=True, exist_ok=True)
|
||||
server_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
latest_ver = None
|
||||
|
||||
try:
|
||||
release = True if not snapshot else False
|
||||
if latest:
|
||||
if server_type == "vanilla":
|
||||
if build:
|
||||
raise click.ClickException("--build is only available when --server-type paper is used.")
|
||||
if list_builds:
|
||||
raise click.ClickException("--list-builds is only available when --server-type paper is used.")
|
||||
|
||||
if latest or mc_version is None:
|
||||
click.echo("Fetching latest Minecraft version...")
|
||||
mc_version = get_latest_version_minecraft(release=release)
|
||||
|
||||
prepare_server_dir()
|
||||
click.echo(f"Downloading Vanilla Minecraft server {mc_version} ...")
|
||||
out = download_vanilla_server_jar(mc_version, server_dir, filename=filename)
|
||||
click.echo(f"Done: {out}")
|
||||
elif latest:
|
||||
click.echo("Fetching latest Mojang release version...")
|
||||
latest_ver = get_latest_paper_version(release=release)
|
||||
builds = get_specific_version_paper_builds(latest_ver)
|
||||
prepare_server_dir()
|
||||
out = download_server_jar(latest_ver, builds[-1], server_dir)
|
||||
else:
|
||||
if mc_version is None:
|
||||
@@ -154,10 +183,12 @@ def create_server(name, mc_version, build, snapshot, latest, list_builds, filena
|
||||
|
||||
if build:
|
||||
click.echo(f"Downloading Paper {mc_version} build {build} ...")
|
||||
prepare_server_dir()
|
||||
out = download_server_jar(mc_version, str(build), server_dir, filename=filename)
|
||||
click.echo(f"Done: {out}")
|
||||
else:
|
||||
click.echo(f"Downloading latest Paper build for {mc_version} ...")
|
||||
prepare_server_dir()
|
||||
out = download_latest_build_paper_jar(mc_version, server_dir, filename=filename)
|
||||
click.echo(f"Done: {out}")
|
||||
|
||||
@@ -184,8 +215,8 @@ def create_server(name, mc_version, build, snapshot, latest, list_builds, filena
|
||||
extra_args += "nogui" if nogui else ""
|
||||
args = [
|
||||
java_exec_path,
|
||||
"--Xms{}".format(x_memory_initial),
|
||||
"--Xmx{}".format(x_memory_maximum),
|
||||
"-Xms{}".format(x_memory_initial),
|
||||
"-Xmx{}".format(x_memory_maximum),
|
||||
"-jar",
|
||||
out.absolute().as_posix(),
|
||||
extra_args,
|
||||
@@ -195,7 +226,7 @@ def create_server(name, mc_version, build, snapshot, latest, list_builds, filena
|
||||
print("Will use custom commands as replacement.")
|
||||
args = custom_args
|
||||
|
||||
print(f"Server command: {" ".join(args)}")
|
||||
print(f"Server command: {' '.join(args)}")
|
||||
|
||||
with settings.edit() as s:
|
||||
print("Saving...")
|
||||
@@ -213,10 +244,13 @@ def create_server(name, mc_version, build, snapshot, latest, list_builds, filena
|
||||
print("Done")
|
||||
|
||||
class SocketServer:
|
||||
def __init__(self, host, port, enable_tls, certfile: Path, keyfile: Path):
|
||||
def __init__(self, host, port, enable_tls, certfile: Path, keyfile: Path, password: str = ""):
|
||||
self.logger = logging.getLogger("SocketServer")
|
||||
self.logger.setLevel(logging.INFO)
|
||||
self.stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
self.stdout_handler.setFormatter(logging.Formatter("%(level)s:%(message)s"))
|
||||
self.stdout_handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
|
||||
if not self.logger.handlers:
|
||||
self.logger.addHandler(self.stdout_handler)
|
||||
|
||||
# flags
|
||||
self.stop_event = threading.Event()
|
||||
@@ -235,15 +269,24 @@ class SocketServer:
|
||||
self.enable_tls = enable_tls
|
||||
self.certfile = certfile
|
||||
self.keyfile = keyfile
|
||||
self.password = "" if password is None else str(password)
|
||||
self._ssl_context = None
|
||||
|
||||
if not self.certfile.exists():
|
||||
raise FileNotFoundError("Certfile not found")
|
||||
if self.password and not self.enable_tls:
|
||||
self.logger.warning(
|
||||
"[SECURITY] socketServerPassword is enabled while TLS is disabled. "
|
||||
"Client passwords will be sent in plaintext."
|
||||
)
|
||||
|
||||
if not self.keyfile.exists():
|
||||
raise FileNotFoundError("Keyfile not found")
|
||||
if self.enable_tls:
|
||||
if not self.certfile.exists():
|
||||
raise FileNotFoundError("Certfile not found")
|
||||
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
|
||||
if not self.keyfile.exists():
|
||||
raise FileNotFoundError("Keyfile not found")
|
||||
|
||||
self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
self._ssl_context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
|
||||
|
||||
# -------------------------
|
||||
# Socket Server
|
||||
@@ -271,18 +314,129 @@ class SocketServer:
|
||||
with self._sub_lock:
|
||||
self._log_subscribers.discard(q)
|
||||
|
||||
def _format_command_help(self, command_map):
|
||||
return "\n".join(
|
||||
f"{command}: {description}"
|
||||
for command, description in command_map.items()
|
||||
)
|
||||
|
||||
def get_socket_help_message(self, current_server=None):
|
||||
socket_commands = {
|
||||
"__help": "Display this help message",
|
||||
"__list": "List available server shells",
|
||||
"__exit": "Close the current socket connection",
|
||||
"__stop_all": "Stop all servers and close the socket server",
|
||||
"__c <server name>": "Connect to a server shell",
|
||||
"__d": "Disconnect from the current server shell",
|
||||
"__sync_log [fromDate:endDate]": "Sync saved log lines from the attached server. Empty syncs the latest 300 lines",
|
||||
"__download_log": "Download the attached server's saved log file",
|
||||
}
|
||||
attached_commands = {
|
||||
"__status": "Show target server process status",
|
||||
"__stop": "Stop the target server process",
|
||||
"__start": "Start the target server process",
|
||||
"<minecraft command>": "Send command to the target Minecraft server process",
|
||||
}
|
||||
|
||||
message = "[SYS] ServerJar (Server-side):\n" + self._format_command_help(socket_commands)
|
||||
if current_server is not None:
|
||||
message += (
|
||||
f"\n[SYS] Attached server commands for \"{current_server}\":\n"
|
||||
+ self._format_command_help(attached_commands)
|
||||
)
|
||||
else:
|
||||
message += "\n[SYS] Use __c <server name> before attached server commands."
|
||||
|
||||
return message
|
||||
|
||||
def get_server_list_message(self):
|
||||
if not self.command_receivers:
|
||||
return "[SYS] No server shells are available."
|
||||
|
||||
server_names = sorted(self.command_receivers.keys())
|
||||
return "[SYS] Available server shells:\n" + "\n".join(
|
||||
f"- {server_name}" for server_name in server_names
|
||||
)
|
||||
|
||||
def handler_command(self, command: str):
|
||||
self.logger.info("On...no co", command)
|
||||
|
||||
@staticmethod
|
||||
def _send_text(sock, message: str):
|
||||
sock.sendall((message + "\n").encode("utf-8"))
|
||||
|
||||
def requires_password(self):
|
||||
return bool(self.password)
|
||||
|
||||
def check_password(self, candidate: str):
|
||||
return hmac.compare_digest(self.password, candidate)
|
||||
|
||||
def _get_current_server_log_reader(self, current_server):
|
||||
if current_server is None:
|
||||
return None, "[SYS:ERR] You are not connected to any target server."
|
||||
|
||||
receiver = self.get_command_receiver(current_server)
|
||||
if receiver is None:
|
||||
return None, f"[SYS:ERR] Target server \"{current_server}\" does not exist."
|
||||
|
||||
log_reader = receiver.get("logReader")
|
||||
if not callable(log_reader):
|
||||
return None, "[SYS:ERR] Target server's logReader is not callable."
|
||||
|
||||
return log_reader, None
|
||||
|
||||
def send_sync_log(self, sock, current_server, command: str):
|
||||
log_reader, error = self._get_current_server_log_reader(current_server)
|
||||
if error:
|
||||
self._send_text(sock, error)
|
||||
return
|
||||
|
||||
date_range = command[len("__sync_log"):].strip()
|
||||
try:
|
||||
lines = log_reader("sync", date_range)
|
||||
except ValueError as e:
|
||||
self._send_text(sock, f"[SYS:ERR] {e}")
|
||||
return
|
||||
except OSError as e:
|
||||
self._send_text(sock, f"[SYS:ERR] Unable to read log: {e}")
|
||||
return
|
||||
|
||||
if not lines:
|
||||
self._send_text(sock, "[SYS] No saved log lines matched.")
|
||||
return
|
||||
|
||||
self._send_text(sock, f"[SYS] Syncing {len(lines)} saved log line(s).")
|
||||
for line in lines:
|
||||
self._send_text(sock, f"[LOG] {line}")
|
||||
|
||||
def send_download_log(self, sock, current_server):
|
||||
log_reader, error = self._get_current_server_log_reader(current_server)
|
||||
if error:
|
||||
self._send_text(sock, error)
|
||||
return
|
||||
|
||||
try:
|
||||
log_path = log_reader("path")
|
||||
if not log_path.exists():
|
||||
self._send_text(sock, "[SYS:ERR] No saved log file exists yet.")
|
||||
return
|
||||
|
||||
safe_server_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", current_server).strip("._") or "server"
|
||||
file_name = f"{safe_server_name}-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}.log"
|
||||
self._send_text(sock, f"[DOWNLOAD_LOG_BEGIN] {file_name}")
|
||||
with log_path.open("rb") as f:
|
||||
while True:
|
||||
chunk = f.read(LOG_DOWNLOAD_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
self._send_text(sock, base64.b64encode(chunk).decode("ascii"))
|
||||
self._send_text(sock, "[DOWNLOAD_LOG_END]")
|
||||
except OSError as e:
|
||||
self._send_text(sock, f"[SYS:ERR] Unable to download log: {e}")
|
||||
|
||||
def _build_tcp_server(self):
|
||||
manager = self
|
||||
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
try:
|
||||
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
|
||||
except ssl.SSLError as e:
|
||||
self.logger.fatal("SSL error", exc_info=e)
|
||||
|
||||
class TCPServer(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
daemon_threads = True
|
||||
@@ -291,13 +445,19 @@ class SocketServer:
|
||||
super().__init__(server_address, RequestHandlerClass)
|
||||
self.manager = manager
|
||||
|
||||
if self.manager.enable_tls:
|
||||
def get_request():
|
||||
sock, addr = super().get_request()
|
||||
tls_sock = context.wrap_socket(sock, server_side=True)
|
||||
return tls_sock, addr
|
||||
def get_request(self):
|
||||
sock, addr = super().get_request()
|
||||
|
||||
self.get_request = get_request
|
||||
if not self.manager.enable_tls:
|
||||
return sock, addr
|
||||
|
||||
try:
|
||||
tls_sock = self.manager._ssl_context.wrap_socket(sock, server_side=True)
|
||||
except ssl.SSLError:
|
||||
sock.close()
|
||||
raise
|
||||
|
||||
return tls_sock, addr
|
||||
|
||||
class Handler(socketserver.BaseRequestHandler):
|
||||
current_server_record = {
|
||||
@@ -310,7 +470,8 @@ class SocketServer:
|
||||
def handle(self):
|
||||
mgr: Server = self.server.manager
|
||||
|
||||
log_q = mgr.subscribe_logs()
|
||||
authenticated = not mgr.requires_password()
|
||||
log_q = None
|
||||
stop_evt = threading.Event()
|
||||
|
||||
def push_logs():
|
||||
@@ -324,11 +485,17 @@ class SocketServer:
|
||||
except OSError:
|
||||
break
|
||||
|
||||
t = threading.Thread(target=push_logs, daemon=True)
|
||||
t.start()
|
||||
t = None
|
||||
|
||||
try:
|
||||
self.request.sendall(b"[SYS] connected\n")
|
||||
if authenticated:
|
||||
self.request.sendall(b"[SYS] connected\n")
|
||||
log_q = mgr.subscribe_logs()
|
||||
t = threading.Thread(target=push_logs, daemon=True)
|
||||
t.start()
|
||||
else:
|
||||
self.request.sendall(b"[AUTH_REQUIRED] Password required before continuing.\n")
|
||||
|
||||
buf = b""
|
||||
|
||||
while True:
|
||||
@@ -344,6 +511,32 @@ class SocketServer:
|
||||
if not cmd:
|
||||
continue
|
||||
|
||||
if not authenticated:
|
||||
if cmd == "__exit":
|
||||
self.request.sendall(b"[SYS] bye\n")
|
||||
return
|
||||
|
||||
if cmd.startswith("__auth "):
|
||||
password = cmd[len("__auth "):]
|
||||
if mgr.check_password(password):
|
||||
authenticated = True
|
||||
self.request.sendall(b"[AUTH_OK] authenticated\n")
|
||||
self.request.sendall(b"[SYS] connected\n")
|
||||
log_q = mgr.subscribe_logs()
|
||||
t = threading.Thread(target=push_logs, daemon=True)
|
||||
t.start()
|
||||
else:
|
||||
mgr.logger.warning(
|
||||
"[SECURITY] Authentication failed from %s:%s",
|
||||
self.client_address[0],
|
||||
self.client_address[1],
|
||||
)
|
||||
self.request.sendall(b"[AUTH_ERR] Invalid password.\n")
|
||||
continue
|
||||
|
||||
self.request.sendall(b"[AUTH_REQUIRED] Password required before continuing.\n")
|
||||
continue
|
||||
|
||||
current_server = self.current_server_record.get(
|
||||
f"{self.client_address[0]}:{self.client_address[1]}",
|
||||
None)
|
||||
@@ -354,16 +547,28 @@ class SocketServer:
|
||||
message = None
|
||||
|
||||
if cmd.startswith("__"):
|
||||
if cmd == "__exit":
|
||||
if cmd == "__help":
|
||||
self.request.sendall(
|
||||
(mgr.get_socket_help_message(current_server) + "\n").encode("utf-8")
|
||||
)
|
||||
elif cmd == "__list":
|
||||
self.request.sendall(
|
||||
(mgr.get_server_list_message() + "\n").encode("utf-8")
|
||||
)
|
||||
elif cmd == "__exit":
|
||||
# Exit socket
|
||||
self.request.sendall(b"[SYS] bye\n")
|
||||
return
|
||||
if cmd == "__stop_all":
|
||||
elif cmd == "__stop_all":
|
||||
self.request.sendall(
|
||||
f"[SYS] Stopping all servers...bye\n".encode("utf-8")
|
||||
)
|
||||
mgr.stop_event.set()
|
||||
return
|
||||
elif cmd.startswith("__sync_log"):
|
||||
mgr.send_sync_log(self.request, current_server, cmd)
|
||||
elif cmd == "__download_log":
|
||||
mgr.send_download_log(self.request, current_server)
|
||||
elif cmd.startswith("__c"):
|
||||
match = re.match(r"^__c\s+(.+)$", cmd)
|
||||
if match:
|
||||
@@ -430,12 +635,13 @@ class SocketServer:
|
||||
if message is not None:
|
||||
msg = msg + message + "\n"
|
||||
self.request.sendall(msg.encode("utf-8"))
|
||||
except ConnectionResetError:
|
||||
except (ConnectionResetError, OSError):
|
||||
mgr.logger.info(
|
||||
"[SYS] Client disconnected. From {}:{}".format(self.client_address[0], self.client_address[1]))
|
||||
finally:
|
||||
stop_evt.set()
|
||||
mgr.unsubscribe_logs(log_q)
|
||||
if log_q is not None:
|
||||
mgr.unsubscribe_logs(log_q)
|
||||
|
||||
return TCPServer((self.host, self.port), Handler)
|
||||
|
||||
@@ -464,13 +670,14 @@ class SocketServer:
|
||||
self._tcp_thread.join(timeout=2)
|
||||
self._tcp_thread = None
|
||||
|
||||
def register_command_receiver(self, server_name, receiver, process_receiver):
|
||||
def register_command_receiver(self, server_name, receiver, process_receiver, log_reader=None):
|
||||
if server_name in self.command_receivers.keys():
|
||||
self.logger.warning(f"[SYS] Command receiver name \"{server_name}\" already registered")
|
||||
else:
|
||||
self.command_receivers[server_name] = {
|
||||
"receiver": receiver,
|
||||
"processReceiver": process_receiver,
|
||||
"logReader": log_reader,
|
||||
}
|
||||
|
||||
def get_command_receiver(self, server_name):
|
||||
@@ -514,11 +721,93 @@ class Server:
|
||||
self.port = port
|
||||
self.host = host
|
||||
self.enable = enable
|
||||
self.log_dir = Path(self.work_dir) / LOG_DIR_NAME
|
||||
self.log_path = self.log_dir / SERVERJAR_LOG_FILE
|
||||
|
||||
self.log_queue = queue.Queue() # stdout lines
|
||||
self._threads: list[threading.Thread] = []
|
||||
self.broadcaster = None
|
||||
|
||||
def _append_saved_log(self, line: str):
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = datetime.datetime.now().isoformat(timespec="seconds")
|
||||
with self.log_path.open("a", encoding="utf-8") as f:
|
||||
f.write(f"{timestamp}\t{line}\n")
|
||||
|
||||
@staticmethod
|
||||
def _parse_log_date(value: str, *, is_end=False):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return None
|
||||
|
||||
date_only = re.fullmatch(r"\d{4}-\d{2}-\d{2}", value) is not None
|
||||
try:
|
||||
parsed = datetime.datetime.fromisoformat(value)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid date '{value}'. Use ISO date format, for example 2026-05-17.") from e
|
||||
|
||||
if date_only and is_end:
|
||||
return datetime.datetime.combine(parsed.date(), datetime.time.max)
|
||||
return parsed
|
||||
|
||||
@staticmethod
|
||||
def _split_saved_log_line(raw_line: str):
|
||||
raw_line = raw_line.rstrip("\n")
|
||||
timestamp, sep, message = raw_line.partition("\t")
|
||||
if not sep:
|
||||
return None, raw_line
|
||||
|
||||
try:
|
||||
parsed = datetime.datetime.fromisoformat(timestamp)
|
||||
except ValueError:
|
||||
return None, raw_line
|
||||
|
||||
return parsed, f"{timestamp} {message}"
|
||||
|
||||
def read_saved_logs(self, date_range: str = ""):
|
||||
if not self.log_path.exists():
|
||||
return []
|
||||
|
||||
start = None
|
||||
end = None
|
||||
date_range = date_range.strip()
|
||||
limit = 300 if not date_range else None
|
||||
|
||||
if date_range:
|
||||
if ":" not in date_range:
|
||||
raise ValueError("Usage: __sync_log fromDate:endDate")
|
||||
start_raw, end_raw = date_range.split(":", 1)
|
||||
start = self._parse_log_date(start_raw)
|
||||
end = self._parse_log_date(end_raw, is_end=True)
|
||||
if start and end and start > end:
|
||||
raise ValueError("fromDate must be earlier than endDate.")
|
||||
|
||||
matched = []
|
||||
with self.log_path.open("r", encoding="utf-8") as f:
|
||||
for raw_line in f:
|
||||
timestamp, display_line = self._split_saved_log_line(raw_line)
|
||||
if start or end:
|
||||
if timestamp is None:
|
||||
continue
|
||||
if start and timestamp < start:
|
||||
continue
|
||||
if end and timestamp > end:
|
||||
continue
|
||||
matched.append(display_line)
|
||||
|
||||
if limit is not None:
|
||||
return matched[-limit:]
|
||||
|
||||
return matched
|
||||
|
||||
def log_reader(self, action, date_range=""):
|
||||
if action == "sync":
|
||||
return self.read_saved_logs(date_range)
|
||||
if action == "path":
|
||||
return self.log_path
|
||||
|
||||
raise ValueError(f"Unknown log action: {action}")
|
||||
|
||||
def start_process(self):
|
||||
self.logger.info("Starting process...")
|
||||
|
||||
@@ -574,6 +863,10 @@ class Server:
|
||||
|
||||
line = line.rstrip("\n")
|
||||
self.logger.info("[PROC] %s", line)
|
||||
try:
|
||||
self._append_saved_log(line)
|
||||
except OSError as e:
|
||||
self.logger.warning("[PROC] unable to save log: %s", e)
|
||||
self.publish_log(line)
|
||||
|
||||
def send_command(self, command: str) -> bool:
|
||||
@@ -631,12 +924,24 @@ class Server:
|
||||
|
||||
self.stop_process()
|
||||
|
||||
def restart(self):
|
||||
if self.running:
|
||||
self.stop()
|
||||
|
||||
self.start()
|
||||
|
||||
def is_process_alive(self) -> bool:
|
||||
with self.proc_lock:
|
||||
return self.proc is not None and self.proc.poll() is None
|
||||
|
||||
def command_receiver(self, command):
|
||||
if command == "__status":
|
||||
if command == "__help":
|
||||
return True, ("__status: Show target server process status\n"
|
||||
"__stop: Stop the target server process\n"
|
||||
"__start: Start the target server process\n"
|
||||
"__restart: Restart the target server process\n"
|
||||
"<minecraft command>: Send command to the target Minecraft server process")
|
||||
elif command == "__status":
|
||||
with self.proc_lock:
|
||||
pid = self.proc.pid if self.proc else None
|
||||
return True, (f"processAlive: {self.is_process_alive()}\n"
|
||||
@@ -648,6 +953,9 @@ class Server:
|
||||
elif command == "__start":
|
||||
self.start()
|
||||
return True, f"Server \"{self.name}\" process started"
|
||||
elif command == "__restart":
|
||||
self.restart()
|
||||
return True, f"Server \"{self.name}\" process restarted"
|
||||
else:
|
||||
return False, f"Unknown command: {command}"
|
||||
|
||||
@@ -681,7 +989,9 @@ def load_all_server_from_settings(settings: FileSettings):
|
||||
|
||||
|
||||
@main.command()
|
||||
def runserver():
|
||||
@click.option("--keep-running", required=False, help="Keep server running while all Minecraft servers are not running.",
|
||||
default=True, show_default=True, flag_value=True)
|
||||
def runserver(keep_running: bool):
|
||||
logger = logging.getLogger(__name__)
|
||||
formatter = logging.Formatter('[%(asctime)s:%(levelname)s:runServer]: %(message)s')
|
||||
logger.setLevel(logging.INFO)
|
||||
@@ -699,7 +1009,8 @@ def runserver():
|
||||
settings.get("socketServerPort", 25560),
|
||||
settings.get("enableTLSSupport", True),
|
||||
Path(settings.get("socketServerCertfile", "data/server.crt")),
|
||||
Path(settings.get( "socketServerKeyfile", "data/server.key")))
|
||||
Path(settings.get( "socketServerKeyfile", "data/server.key")),
|
||||
settings.get("socketServerPassword", ""))
|
||||
|
||||
# Flags
|
||||
stop_once = False
|
||||
@@ -739,7 +1050,8 @@ def runserver():
|
||||
if server.enable:
|
||||
server.register_broadcaster(socket_server.publish_log)
|
||||
socket_server.register_command_receiver(server.name, server.command_receiver,
|
||||
server.process_command_receiver)
|
||||
server.process_command_receiver,
|
||||
server.log_reader)
|
||||
try:
|
||||
server.start()
|
||||
except Exception as e:
|
||||
@@ -763,7 +1075,7 @@ def runserver():
|
||||
logger.info(f"Server {server.name} stopped.")
|
||||
server.running = False
|
||||
|
||||
if servers and not any(server.running for server in servers):
|
||||
if servers and not any(server.running for server in servers) and not keep_running:
|
||||
cleanup()
|
||||
stop = True
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user