""" ServerJar Wei - 2026 """ import re import shlex import signal import socketserver import logging import os import queue import sys import subprocess import threading import time 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 from utils.file_settings import FileSettings from utils.file_settings import required_list, required_value ROOT_DIR = Path(os.getcwd()) SERVER_CONFIG_PATH = ROOT_DIR / "config" / "server.yml" def exit(message): click.echo(click.style(message, fg='green')) @click.group() def main(): print("ServerJar\n" "WorkDir: {}".format(ROOT_DIR)) @main.command() @click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name") @click.option("--mc-version", "-m", default=None, help="Specify Minecraft version to download (If not specified, download latest Minecraft version)", required=False) @click.option("--build", "-b", default=None, help="Specify paper build to download (Use latest Minecraft version if not specified)") @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("--list-builds", is_flag=True, help="List available paper build versions") @click.option("--filename", default=None, help="Custom SERVER.jar file name") def create_server(name, mc_version, build, snapshot, latest, list_builds, filename): server_dir = Path("servers", name) 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.") server_dir.mkdir(parents=True, exist_ok=True) try: release = True if not snapshot else False if latest: click.echo("Fetching latest Mojang release version...") out = download_latest_paper_jar(server_dir, filename=filename, release=release) click.echo(f"Done: {out}") return if mc_version is None: click.echo("The mc-version is not specified. Fetching latest Minecraft release version...") mc_version = get_latest_version_minecraft(release=release) if list_builds: builds = get_specific_version_paper_builds(mc_version) if not builds: click.echo(f"No builds found for Paper {mc_version}") return click.echo(f"Paper {mc_version} builds:") click.echo(", ".join(map(str, builds[-20:]))) click.echo("(Only list latest 20 builds)") return if build: click.echo(f"Downloading Paper {mc_version} build {build} ...") 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} ...") out = download_latest_build_paper_jar(mc_version, server_dir, filename=filename) click.echo(f"Done: {out}") except Exception as e: raise click.ClickException(str(e)) def load_settings(): s = FileSettings( SERVER_CONFIG_PATH, { "servers": [], "socketServerHostname": "127.0.0.1", "socketServerPort": 25560 }, { "socketServerHostname": required_value("127.0.0.1"), "socketServerPort": required_value(25560), "servers": required_list( { "name": "Unnamed Server", "version": "unknown", "description": "", "command": "", "workDir": "", "port": 25565, "host": "127.0.0.1", "enable": True }, use_same_form=True, ) }, dumps_func=yaml.safe_dump, load_func=yaml.safe_load, ) if not s.exists(): s.create() s.read_from_exist() return s @main.command() @click.option("--server-folder-path", "-sf", help="The destination of the folder", required=True) @click.option("--server-jar-path", "-sp", help="The destination of the SERVER.jar", required=True) @click.option("--socket-server-host", "-srh", help="Hostname of the socket server", required=True) @click.option("--socket-server-port", "-srp", help="Port of the socket server", required=True) @click.option("--java-exec-path", "-p", show_default=True, help="The destination of the java executable", default="java") @click.option("--x-memory-initial", "-xms", show_default=True, help="Initial allocation size of the memory for server", type=str, default="1G") @click.option("--x-memory-maximum", "-xmx", show_default=True, help="Maximum allocation size of the memory for server", type=str, default="4G") @click.option("--nogui", "-ng", help="Disable server window", is_flag=True) @click.option("--extra-args", "-e", help="Extra java arguments", type=str, default="") @click.option("--custom-commands", "-cd", help="Custom run commands", type=str, default="") def create_bootstrap(server_folder_path, server_jar_path, socket_server_host, socket_server_port, java_exec_path, x_memory_initial, x_memory_maximum, nogui, extra_args, custom_commands): settings = load_settings() print("There's some information you need to fill for server config.") name = str(input("New server name: ")) version = str(input("Server version: ")) desc = str(input("Server description: ")) found_exist = False for srv in settings["servers"]: if name == srv["name"]: found_exist = True if found_exist: result = str(input("WARNING: Found duplicate server name. Would you like to continue? [y/N] ")) if not result.lower() == "y": exit("User aborted.") extra_args += " nogui" if nogui else "" cmd = f"{java_exec_path} -Xms{x_memory_initial} -Xmx{x_memory_maximum} -jar {server_jar_path} {extra_args}" if custom_commands: print("Will use custom commands as replacement.") cmd = custom_commands print(f"Server command: {cmd}") with settings.edit() as s: print("Saving...") s["servers"].append({ "name": name, "version": version, "description": desc, "command": cmd, "workDir": server_folder_path, "port": socket_server_port, "host": socket_server_host, "enable": True, }) print("Done") class SocketServer: def __init__(self, host, port): self.logger = logging.getLogger("SocketServer") self.stdout_handler = logging.StreamHandler(sys.stdout) self.stdout_handler.setFormatter(logging.Formatter("%(level)s:%(message)s")) # flags self.stop_event = threading.Event() # Server self.host = host self.port = port self._tcp_server: socketserver.ThreadingTCPServer | None = None self._tcp_thread: threading.Thread | None = None self._log_subscribers: set[queue.Queue] = set() self._sub_lock = threading.Lock() self.command_receivers = {} # ------------------------- # Socket Server # ------------------------- def publish_log(self, server_name: str, line: str | None = None): if line is None: message = server_name else: message = f"[{server_name}] {line}" with self._sub_lock: for q in list(self._log_subscribers): try: q.put_nowait(message) except queue.Full: pass def subscribe_logs(self) -> queue.Queue: q = queue.Queue(maxsize=2000) with self._sub_lock: self._log_subscribers.add(q) return q def unsubscribe_logs(self, q: queue.Queue): with self._sub_lock: self._log_subscribers.discard(q) def handler_command(self, command: str): self.logger.info("On...no co", command) def _build_tcp_server(self): manager = self class TCPServer(socketserver.ThreadingTCPServer): allow_reuse_address = True daemon_threads = True def __init__(self, server_address, RequestHandlerClass): super().__init__(server_address, RequestHandlerClass) self.manager = manager class Handler(socketserver.BaseRequestHandler): current_server_record = { } def setup(self): mgr: Server = self.server.manager mgr.logger.info(f"[SYS] Client from {self.client_address[0]}:{self.client_address[1]} connected,") def handle(self): mgr: Server = self.server.manager log_q = mgr.subscribe_logs() stop_evt = threading.Event() def push_logs(): while not stop_evt.is_set(): try: line = log_q.get(timeout=0.5) except Exception: continue try: self.request.sendall(f"[LOG] {line}\n".encode("utf-8")) except OSError: break t = threading.Thread(target=push_logs, daemon=True) t.start() try: self.request.sendall(b"[SYS] connected\n") buf = b"" while True: data = self.request.recv(4096) if not data: break buf += data while b"\n" in buf: raw, buf = buf.split(b"\n", 1) cmd = raw.decode("utf-8", errors="replace").strip() if not cmd: continue current_server = self.current_server_record.get( f"{self.client_address[0]}:{self.client_address[1]}", None) mgr.logger.info(f"[SYS] Client from {self.client_address[0]}:{self.client_address[1]} send command \"{cmd}\".") ok = None message = None if cmd.startswith("__"): if cmd == "__exit": # Exit socket self.request.sendall(b"[SYS] bye\n") return if cmd == "__stop_all": self.request.sendall( f"[SYS] Stopping all servers...bye\n".encode("utf-8") ) mgr.stop_event.set() return elif cmd.startswith("__c"): match = re.match(r"^__c\s+(.+)$", cmd) if match: server_name = match.group(1).strip() receiver = mgr.get_command_receiver(server_name) else: server_name = None receiver = None if receiver is not None: self.current_server_record[f"{self.client_address[0]}:{self.client_address[1]}"] = server_name self.request.sendall( f"[SYS] Connected to server \"{server_name}\" shell.\n".encode( "utf-8") ) else: self.request.sendall( f"[SYS:ERR] Target server \"{server_name}\" does not exist.\n".encode( "utf-8") ) elif cmd == "__d": self.current_server_record[f"{self.client_address[0]}:{self.client_address[1]}"] = None self.request.sendall( f"[SYS] Disconnected from current server \"{current_server}\"'s shell.\n".encode( "utf-8") ) else: if current_server is not None: receiver = mgr.get_command_receiver(current_server) func = receiver.get("receiver") if receiver else None if callable(func): ok, message = func(cmd) else: self.request.sendall( "[SYS:ERR] Target server's receiver are not callable.\n".encode( "utf-8") ) else: # "target server" is Minecraft server self.request.sendall( "[SYS:ERR] You are not connected to any target server.\n".encode("utf-8") ) else: if current_server is not None: receiver = mgr.get_command_receiver(current_server) func = receiver.get("processReceiver") if receiver else None if callable(func): ok, message = func(cmd) else: self.request.sendall( "[SYS:ERR] Target server's processReceiver are not callable.\n".encode( "utf-8") ) else: # "target server" is Minecraft server self.request.sendall( "[SYS:ERR] You are not connected to any target server.\n".encode("utf-8") ) if ok is not None: msg = f"[OK] Command received. {cmd}\n" if ok else "[ERR] An error occurred\n" if message is not None: msg = msg + message + "\n" self.request.sendall(msg.encode("utf-8")) except ConnectionResetError: 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) return TCPServer((self.host, self.port), Handler) def start_socket_server(self): if self._tcp_server: print("[SOCK] already running") return self._tcp_server = self._build_tcp_server() def loop(): self.logger.info(f"[SOCK] listening on {self.host}:{self.port}") self._tcp_server.serve_forever(poll_interval=0.5) self._tcp_thread = threading.Thread(target=loop, daemon=True) self._tcp_thread.start() def stop_socket_server(self): if not self._tcp_server: return self.logger.info("[SOCK] shutting down") self._tcp_server.shutdown() self._tcp_server.server_close() self._tcp_server = None if self._tcp_thread and self._tcp_thread.is_alive(): self._tcp_thread.join(timeout=2) self._tcp_thread = None def register_command_receiver(self, server_name, receiver, process_receiver): 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, } def get_command_receiver(self, server_name): if server_name in self.command_receivers.keys(): return self.command_receivers[server_name] return None class Server: def __init__(self, name, version, description, command, work_dir, port, host, enable): self._stdout_thread = None # Process self.proc: subprocess.Popen | None = None self.proc_lock = threading.Lock() self.running = False self.stopping = False # logger self.logger = None # Ensure all servers name are not duplicated if f"Server.{name}" in logging.root.manager.loggerDict: index = 1 name = f"Server.{name}_1" while name not in logging.root.manager.loggerDict: name = f"Server.{name}_{index}" self.logger = logging.getLogger(name) self.logger.setLevel(logging.INFO) self.stdout_handler = logging.StreamHandler(sys.stdout) self.stdout_handler.setFormatter(logging.Formatter("[%(asctime)s:%(level)s]: %(message)s")) self.logger.addHandler(self.stdout_handler) # Values from config self.name = name self.version = version self.description = description self.command = command self.work_dir = work_dir self.port = port self.host = host self.enable = enable self.log_queue = queue.Queue() # stdout lines self._threads: list[threading.Thread] = [] self.broadcaster = None def start_process(self): self.logger.info("Starting process...") with self.proc_lock: if self.proc and self.proc.poll() is None: self.logger.warning("[PROC] already running, skip") return args = shlex.split(self.command) if not args: raise ValueError(f"Server \"{self.name}\" command is empty.") self.logger.info("[PROC] spawning: %s", self.command) self.proc = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, cwd=self.work_dir, encoding="utf-8", errors="replace" ) self.logger.info(f"[PROC] Process spawned. PID = {self.proc.pid}") self._stdout_thread = threading.Thread(target=self._stdout_reader_loop, daemon=True) self._stdout_thread.start() def publish_log(self, line): if self.broadcaster is None: return self.broadcaster(self.name, line) def register_broadcaster(self, broadcaster): self.broadcaster = broadcaster def _stdout_reader_loop(self): self.logger.info("[PROC] stdout reader started") while self.running: with self.proc_lock: proc = self.proc out = proc.stdout if proc else None if not proc or proc.poll() is not None or not out: self.logger.info("[PROC] process ended / stdout closed") break line = out.readline() if not line: break line = line.rstrip("\n") self.logger.info("[PROC] %s", line) self.publish_log(line) def send_command(self, command: str) -> bool: with self.proc_lock: if not self.proc or self.proc.poll() is not None: return False if not self.proc.stdin: return False self.proc.stdin.write(command + "\n") self.proc.stdin.flush() return True def stop_process(self, timeout: float = 10.0): with self.proc_lock: proc = self.proc if not proc: return # Minecraft only self.send_command("stop") try: proc.wait(timeout=timeout) except subprocess.TimeoutExpired: proc.kill() proc.wait() with self.proc_lock: self.proc = None # ------------------------- # Manager lifecycle # ------------------------- def start(self): if self.running: return self.running = True self.stopping = False try: self.start_process() except Exception: self.running = False self.stopping = False raise def stop(self): if not self.running: return self.stopping = True self.running = False self.stop_process() 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": with self.proc_lock: pid = self.proc.pid if self.proc else None return True, (f"processAlive: {self.is_process_alive()}\n" f"processPID: {pid}\n" f"workdir: {self.work_dir}") elif command == "__stop": self.stop() return True, f"Server \"{self.name}\" process stopped" elif command == "__start": self.start() return True, f"Server \"{self.name}\" process started" else: return False, f"Unknown command: {command}" def process_command_receiver(self, command): with self.proc_lock: if not self.proc or self.proc.poll() is not None: return False, "Process is not running." if not self.proc.stdin: return False, "Process standard input are not available." self.proc.stdin.write(command + "\n") self.proc.stdin.flush() return True, "Command sent." def load_all_server_from_settings(settings: FileSettings): servers = [] with settings.edit() as s: for server_conf in s.get("servers", []): servers.append(Server( name=server_conf.get("name"), version=server_conf.get("version"), description=server_conf.get("description"), command=server_conf.get("command"), work_dir=server_conf.get("workDir"), port=server_conf.get("port"), host=server_conf.get("host"), enable=server_conf.get("enable") )) return servers @main.command() def runserver(): logger = logging.getLogger(__name__) formatter = logging.Formatter('%(asctime)s:%(levelname)s: %(message)s') logger.setLevel(logging.INFO) stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(formatter) logger.addHandler(stdout_handler) # Server settings = load_settings() servers = load_all_server_from_settings(settings) logger.info("{} servers available".format(len(servers))) # Socket logger.info("Starting socket server") socket_server = SocketServer(settings.get("socketServerHostname", "127.0.0.1"), settings.get("socketServerPort", 25560)) socket_server.start_socket_server() # Flags stop_once = False def cleanup(): nonlocal stop_once if stop_once: return stop_once = True for s in servers: logger.info("Stopping server {}...".format(s.name)) s.stop() logger.info("Closing socket server...") socket_server.stop_socket_server() logger.info("Done") def sigint_handler(signum, frame): logger.info("Caught SIGINT, exiting...") cleanup() sys.exit(0) signal.signal( signal.SIGINT, sigint_handler ) # Boot server logger.info("Starting server") for server in servers: if server.enable: server.register_broadcaster(socket_server.publish_log) socket_server.register_command_receiver(server.name, server.command_receiver, server.process_command_receiver) try: server.start() except Exception as e: logger.error(f"Server {server.name} failed to start: {e}") else: logger.info(f"Server {server.name} started.") else: logger.info(f"Server {server.name} is disabled.") try: stop = False while not stop: if socket_server.stop_event.is_set(): logger.info("Remote stop event triggered. Stopping...") cleanup() stop = True continue for server in list(servers): if server.running and not server.is_process_alive(): logger.info(f"Server {server.name} stopped.") server.running = False if servers and not any(server.running for server in servers): cleanup() stop = True continue time.sleep(0.5) except KeyboardInterrupt: logger.info("Stopping server...") cleanup() logger.info("Stopped!") if __name__ == "__main__": main()