Add connection history and some automatic server create process.
This commit is contained in:
@@ -26,7 +26,9 @@ SERVER_JAR_DIR = Path.home() / ".serverjar"
|
||||
CLIENT_CERT_DIR = Path.home() / ".serverjar" / "client" / "cert"
|
||||
CLIENT_CERT_SUFFIXES = {".pem", ".crt", ".cer"}
|
||||
CLIENT_HISTORY_FILE = Path.home() / ".serverjar" / "history" / "history.txt"
|
||||
CLIENT_CONNECTION_HISTORY_FILE = Path.home() / ".serverjar" / "history" / "connection_history.txt"
|
||||
CLIENT_LOG_DIR = Path.home() / ".serverjar" / "client" / "logs"
|
||||
DEFAULT_CONNECT_TIMEOUT = 10.0
|
||||
|
||||
def add_client_cert(cert_path):
|
||||
source = Path(cert_path).expanduser()
|
||||
@@ -94,6 +96,68 @@ def create_client_tls_context(log=None, warn=None):
|
||||
|
||||
return context
|
||||
|
||||
def get_connection_history(log=None, warn=None):
|
||||
connection_history = []
|
||||
|
||||
if not CLIENT_CONNECTION_HISTORY_FILE.exists():
|
||||
return []
|
||||
|
||||
if callable(log):
|
||||
log("Searching existing connections history...")
|
||||
|
||||
try:
|
||||
with CLIENT_CONNECTION_HISTORY_FILE.open(mode="r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
try:
|
||||
line = line.strip()
|
||||
if line.count(":") < 2:
|
||||
continue
|
||||
|
||||
server_name, host, port = line.rsplit(":", 2)
|
||||
|
||||
# Ignore unknown format line record
|
||||
if len(server_name) == 0 or len(host) == 0 or len(port) == 0:
|
||||
continue
|
||||
|
||||
int(port)
|
||||
|
||||
connection_history.append({
|
||||
"serverName": server_name,
|
||||
"host": host,
|
||||
"port": port,
|
||||
})
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
if callable(warn):
|
||||
warn(f"Unable to load connection history file: {e}")
|
||||
|
||||
return connection_history
|
||||
|
||||
|
||||
def save_connection_history(connection_history, log=None, warn=None):
|
||||
if not connection_history:
|
||||
return
|
||||
|
||||
CLIENT_CONNECTION_HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if callable(log):
|
||||
log("Saving connection history file...")
|
||||
|
||||
try:
|
||||
with CLIENT_CONNECTION_HISTORY_FILE.open(mode="w", encoding="utf-8") as f:
|
||||
for server in connection_history:
|
||||
server_name = server.get("serverName")
|
||||
host = server.get("host")
|
||||
port = server.get("port")
|
||||
if not server_name or not host or not port:
|
||||
continue
|
||||
f.write(f"{server_name}:{host}:{port}\n")
|
||||
except Exception as e:
|
||||
if callable(warn):
|
||||
warn(f"Unable to save connection history file: {e}")
|
||||
|
||||
|
||||
def get_history(log=None, warn=None):
|
||||
if not CLIENT_HISTORY_FILE.exists():
|
||||
@@ -227,6 +291,7 @@ class ServerJarClient(Application):
|
||||
self.cmds = []
|
||||
self.current_index = None
|
||||
self.history_draft = ""
|
||||
self.connection_history = []
|
||||
|
||||
@self.kb.add("c-c")
|
||||
def closing_kb(event):
|
||||
@@ -426,6 +491,43 @@ class ServerJarClient(Application):
|
||||
self._log("History cleared")
|
||||
return True
|
||||
|
||||
def _enable_retry_mode(cmd):
|
||||
self.args.retry = True
|
||||
self._log("Auto-retry mode enabled")
|
||||
return True
|
||||
|
||||
def _list_connection_history(cmd):
|
||||
if len(self.connection_history) == 0:
|
||||
self._log("No connection history exists.")
|
||||
return True
|
||||
|
||||
for index, server in enumerate(self.connection_history):
|
||||
self._log("{}: {}".format(index, server.get("serverName")))
|
||||
|
||||
return True
|
||||
|
||||
def _connect_to_record_server(cmd):
|
||||
parts = cmd.split()
|
||||
if len(parts) != 2:
|
||||
self._err("Usage: _hc ${index}")
|
||||
return True
|
||||
|
||||
try:
|
||||
index = int(parts[1])
|
||||
server = self.connection_history[index]
|
||||
host = server.get("host")
|
||||
port = int(server.get("port"))
|
||||
if not host:
|
||||
raise ValueError("empty host")
|
||||
except (ValueError, TypeError, IndexError):
|
||||
self._err("Invalid index")
|
||||
return True
|
||||
|
||||
connect_to_server(host, port)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
cmd_map = {
|
||||
"_exit": {
|
||||
"func": _shutdown,
|
||||
@@ -459,10 +561,23 @@ class ServerJarClient(Application):
|
||||
"func": _clear,
|
||||
"description": "Clear the log area",
|
||||
},
|
||||
"_retry": {
|
||||
"func": _enable_retry_mode,
|
||||
"description": "Reconnect to the remote server if connection fails",
|
||||
},
|
||||
"_help": {
|
||||
"func": _help,
|
||||
"description": "Display the help message",
|
||||
},
|
||||
"_lhc": {
|
||||
"func": _list_connection_history,
|
||||
"description": "List the connection history (Only list the server that have one connection is successful)",
|
||||
}
|
||||
,
|
||||
"_hc": {
|
||||
"func": _connect_to_record_server,
|
||||
"description": "Connect to the server listed in the connection history. (Usage: _hc ${index})",
|
||||
}
|
||||
}
|
||||
|
||||
if not command.strip():
|
||||
@@ -513,6 +628,8 @@ class ServerJarClient(Application):
|
||||
required=False)
|
||||
parser.add_argument('-r', '--retry', help="Retry when disconnect", action='store_true', default=False,
|
||||
required=False)
|
||||
parser.add_argument('--connect-timeout', type=float, default=DEFAULT_CONNECT_TIMEOUT,
|
||||
help="Socket connection timeout in seconds", required=False)
|
||||
parser.add_argument('--add-cert', help="Add server certificate", type=str, default=None, required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -541,6 +658,7 @@ class ServerJarClient(Application):
|
||||
|
||||
# Save history
|
||||
save_history(self.cmds, self._log, self._warn)
|
||||
save_connection_history(self.connection_history, self._log, self._warn)
|
||||
|
||||
# Exit ui event loop
|
||||
self.full_exit()
|
||||
@@ -600,6 +718,17 @@ class ServerJarClient(Application):
|
||||
|
||||
self.invalidate()
|
||||
|
||||
def save_server(self, host, port):
|
||||
for server in self.connection_history:
|
||||
if server.get("host") == host and str(server.get("port")) == str(port):
|
||||
return
|
||||
|
||||
self.connection_history.append({
|
||||
"serverName": f"{host}:{port}",
|
||||
"host": host,
|
||||
"port": port,
|
||||
})
|
||||
|
||||
def client(self):
|
||||
ptk_clear()
|
||||
|
||||
@@ -627,10 +756,9 @@ class ServerJarClient(Application):
|
||||
|
||||
# Create connect
|
||||
if self.args.no_tls:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect((self.host, self.port))
|
||||
s = socket.create_connection((self.host, self.port), timeout=self.args.connect_timeout)
|
||||
else:
|
||||
raw = socket.create_connection((self.host, self.port))
|
||||
raw = socket.create_connection((self.host, self.port), timeout=self.args.connect_timeout)
|
||||
context = self.get_tls_context()
|
||||
s = context.wrap_socket(raw, server_hostname=self.host)
|
||||
|
||||
@@ -649,6 +777,10 @@ class ServerJarClient(Application):
|
||||
data = s.recv(4096)
|
||||
if not data:
|
||||
raise ConnectionError("Server closed")
|
||||
|
||||
# Only save the server that have at least one connection is successful
|
||||
self.save_server(self.host, self.port)
|
||||
|
||||
buffer += data.decode("utf-8", errors="replace")
|
||||
while "\n" in buffer:
|
||||
line, buffer = buffer.split("\n", 1)
|
||||
@@ -721,7 +853,7 @@ class ServerJarClient(Application):
|
||||
|
||||
except (ConnectionError, OSError) as e:
|
||||
if not self.closing_event.is_set():
|
||||
if self.args.retry:
|
||||
if self.args.retry and self.host is not None and self.port is not None:
|
||||
self._warn(f"Disconnected: {e}, retrying...")
|
||||
else:
|
||||
self._err(f"Disconnected: {e}")
|
||||
@@ -757,6 +889,7 @@ class ServerJarClient(Application):
|
||||
self.args = self.arguments_parser()
|
||||
self.layout.focus(self.input_area)
|
||||
self.cmds = get_history(self._log, self._warn)
|
||||
self.connection_history = get_connection_history(self._log, self._warn)
|
||||
asyncio.create_task(self.consume_incoming())
|
||||
|
||||
self.client_thread.start()
|
||||
|
||||
@@ -3,6 +3,7 @@ ServerJar
|
||||
|
||||
Wei - 2026
|
||||
"""
|
||||
import platform
|
||||
import re
|
||||
import signal
|
||||
import socketserver
|
||||
@@ -17,11 +18,14 @@ import time
|
||||
import datetime
|
||||
import base64
|
||||
import hmac
|
||||
import ipaddress
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
import click
|
||||
import yaml
|
||||
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, \
|
||||
read_server_properties, save_server_properties, activate_eula_file
|
||||
from utils.file_settings import FileSettings
|
||||
from utils.file_settings import required_list, required_value
|
||||
from cryptography import x509
|
||||
@@ -89,6 +93,86 @@ def load_settings():
|
||||
return s
|
||||
|
||||
|
||||
def run_initial_server_start(args, server_dir: Path, timeout: float = 120.0):
|
||||
click.echo("Starting server once to generate initial files...")
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
cwd=server_dir,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
try:
|
||||
output, _ = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
click.echo("Initial start is still running. Sending stop command...")
|
||||
try:
|
||||
if proc.stdin:
|
||||
proc.stdin.write("stop\n")
|
||||
proc.stdin.flush()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
output, _ = proc.communicate(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
click.echo("Server did not stop in time. Killing initial start process...")
|
||||
proc.kill()
|
||||
output, _ = proc.communicate()
|
||||
|
||||
if output:
|
||||
for line in output.splitlines():
|
||||
click.echo(f"[BOOT] {line}")
|
||||
|
||||
return proc.returncode
|
||||
|
||||
|
||||
def configure_initial_server_files(server_dir: Path, server_name: str, server_host: str, server_port: str,
|
||||
property_overrides=None):
|
||||
try:
|
||||
properties = read_server_properties(server_dir)
|
||||
except FileNotFoundError as e:
|
||||
click.echo(click.style(f"Unable to read server.properties: {e}", fg="yellow"))
|
||||
else:
|
||||
property_values = {
|
||||
"server-name": str(server_name),
|
||||
"server-ip": str(server_host),
|
||||
"server-port": str(server_port),
|
||||
}
|
||||
if property_overrides:
|
||||
property_values.update(property_overrides)
|
||||
|
||||
for key, value in property_values.items():
|
||||
properties[key] = (value, {})
|
||||
|
||||
save_server_properties(properties, server_dir)
|
||||
click.echo("server.properties updated.")
|
||||
|
||||
try:
|
||||
if activate_eula_file(server_dir):
|
||||
click.echo("EULA activated.")
|
||||
else:
|
||||
click.echo(click.style("EULA was not activated.", fg="yellow"))
|
||||
except FileNotFoundError as e:
|
||||
click.echo(click.style(f"Unable to activate EULA: {e}", fg="yellow"))
|
||||
|
||||
|
||||
def parse_server_property_overrides(values):
|
||||
overrides = {}
|
||||
for value in values:
|
||||
key, separator, property_value = value.partition("=")
|
||||
key = key.strip()
|
||||
if separator == "" or key == "":
|
||||
raise click.ClickException(f"Invalid server property override: {value}. Use key=value.")
|
||||
overrides[key] = property_value.strip()
|
||||
return overrides
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name")
|
||||
@click.option("--mc-version", "-m",
|
||||
@@ -126,10 +210,13 @@ def load_settings():
|
||||
help="Hostname of the server", required=True)
|
||||
@click.option("--server-port", "-srp",
|
||||
help="Port of the server", required=True)
|
||||
@click.option("--server-property", "-sp", "server_properties", multiple=True,
|
||||
help="server.properties override written after the first startup. Use key=value; can be repeated.")
|
||||
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):
|
||||
x_memory_initial, x_memory_maximum, nogui, custom_args, server_port, server_host, server_properties):
|
||||
server_dir = Path("servers", name)
|
||||
server_type = server_type.lower()
|
||||
property_overrides = parse_server_property_overrides(server_properties)
|
||||
|
||||
def prepare_server_dir():
|
||||
if server_dir.exists():
|
||||
@@ -210,22 +297,32 @@ def create_server(name, mc_version, build, server_type, snapshot, latest, list_b
|
||||
exit("User aborted.")
|
||||
return
|
||||
|
||||
extra_args += "nogui" if nogui else ""
|
||||
args = [
|
||||
java_exec_path,
|
||||
"-Xms{}".format(x_memory_initial),
|
||||
"-Xmx{}".format(x_memory_maximum),
|
||||
"-jar",
|
||||
out.absolute().as_posix(),
|
||||
extra_args,
|
||||
]
|
||||
if extra_args:
|
||||
args.extend(shlex.split(extra_args))
|
||||
if nogui:
|
||||
args.append("nogui")
|
||||
|
||||
if custom_args:
|
||||
print("Will use custom commands as replacement.")
|
||||
args = custom_args
|
||||
args = shlex.split(custom_args)
|
||||
|
||||
print(f"Server command: {' '.join(args)}")
|
||||
|
||||
try:
|
||||
return_code = run_initial_server_start(args, server_dir)
|
||||
if return_code not in (0, None):
|
||||
click.echo(click.style(f"Initial server start exited with code {return_code}.", fg="yellow"))
|
||||
configure_initial_server_files(server_dir, name, server_host, server_port, property_overrides)
|
||||
except Exception as e:
|
||||
raise click.ClickException(f"Unable to bootstrap server files: {e}") from e
|
||||
|
||||
with settings.edit() as s:
|
||||
print("Saving...")
|
||||
s["servers"].append({
|
||||
@@ -292,6 +389,8 @@ class SocketServer:
|
||||
self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
self._ssl_context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
|
||||
|
||||
self.commands = {}
|
||||
|
||||
# -------------------------
|
||||
# Socket Server
|
||||
# -------------------------
|
||||
@@ -339,11 +438,12 @@ class SocketServer:
|
||||
"__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",
|
||||
"__stop_all": "Stop all servers and close the socket server (TLS Support must be enabled)",
|
||||
"__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",
|
||||
"__system_info": "Show system information",
|
||||
}
|
||||
attached_commands = {
|
||||
"__status": "Show target server process status",
|
||||
@@ -597,12 +697,18 @@ class SocketServer:
|
||||
self.request.sendall(b"[SYS] bye\n")
|
||||
return
|
||||
elif cmd == "__stop_all":
|
||||
self.request.sendall(
|
||||
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()
|
||||
return
|
||||
if not mgr.enable_tls:
|
||||
self.request.sendall(
|
||||
f"[SYS] Command \"__stop_all\" only works when tls support is enabled.\n".encode("utf-8")
|
||||
)
|
||||
else:
|
||||
self.request.sendall(
|
||||
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()
|
||||
return
|
||||
elif cmd.startswith("__sync_log"):
|
||||
mgr.send_sync_log(self.request, current_server, cmd)
|
||||
elif cmd == "__download_log":
|
||||
@@ -633,6 +739,14 @@ class SocketServer:
|
||||
f"[SYS] Disconnected from current server \"{current_server}\"'s shell.\n".encode(
|
||||
"utf-8")
|
||||
)
|
||||
elif cmd == "__system_info":
|
||||
self.request.sendall(
|
||||
f"[SYS] Connected clients number: {len(self.current_server_record)}\n"
|
||||
f"[SYS] Hostname: {mgr.host}\n"
|
||||
f"[SYS] Port: {mgr.port}\n"
|
||||
f"[SYS] Working Directory: {os.getcwd()}\n"
|
||||
f"[SYS] Platform: {platform.system()} ({platform.architecture()[0]})\n"
|
||||
f"[SYS] Version: {VERSION}\n".encode("utf-8"))
|
||||
else:
|
||||
if current_server is not None:
|
||||
receiver = mgr.get_command_receiver(current_server)
|
||||
@@ -887,12 +1001,12 @@ class Server:
|
||||
|
||||
def _stdout_reader_loop(self):
|
||||
self.logger.info("[PROC] stdout reader started")
|
||||
while self.running:
|
||||
while True:
|
||||
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:
|
||||
if not proc or not out:
|
||||
self.logger.info("[PROC] process ended / stdout closed")
|
||||
break
|
||||
|
||||
@@ -915,8 +1029,11 @@ class Server:
|
||||
if not self.proc.stdin:
|
||||
return False
|
||||
|
||||
self.proc.stdin.write(command + "\n")
|
||||
self.proc.stdin.flush()
|
||||
try:
|
||||
self.proc.stdin.write(command + "\n")
|
||||
self.proc.stdin.flush()
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def stop_process(self, timeout: float = 10.0):
|
||||
@@ -959,9 +1076,9 @@ class Server:
|
||||
return
|
||||
|
||||
self.stopping = True
|
||||
self.running = False
|
||||
|
||||
self.stop_process()
|
||||
self.running = False
|
||||
|
||||
def restart(self):
|
||||
if self.running:
|
||||
@@ -995,7 +1112,7 @@ class Server:
|
||||
return True, f"Server \"{self.name}\" process started"
|
||||
elif command == "__info":
|
||||
return True, (f"serverName: {self.name}\n"
|
||||
f"serverPID: {self.proc.pid}\n"
|
||||
f"serverPID: {self.proc.pid if self.proc else None}\n"
|
||||
f"description: {self.description}\n"
|
||||
f"arguments: {self.args}\n"
|
||||
f"host: {self.host}\n"
|
||||
@@ -1012,8 +1129,11 @@ class Server:
|
||||
if not self.proc.stdin:
|
||||
return False, "Process standard input are not available."
|
||||
|
||||
self.proc.stdin.write(command + "\n")
|
||||
self.proc.stdin.flush()
|
||||
try:
|
||||
self.proc.stdin.write(command + "\n")
|
||||
self.proc.stdin.flush()
|
||||
except OSError as e:
|
||||
return False, f"Unable to write process standard input: {e}"
|
||||
return True, "Command sent."
|
||||
|
||||
def load_all_server_from_settings(settings: FileSettings):
|
||||
@@ -1138,7 +1258,8 @@ def runserver(keep_running: bool):
|
||||
logger.info("Stopped!")
|
||||
|
||||
@main.command()
|
||||
def generate_tls_key():
|
||||
@click.option("--hostname", default="localhost", show_default=True, help="Hostname or IP address of the server.")
|
||||
def generate_tls_key(hostname: str):
|
||||
print("Generating TLS key...")
|
||||
key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
@@ -1150,9 +1271,23 @@ def generate_tls_key():
|
||||
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"),
|
||||
x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Dev Org"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, u"localhost"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, u"{}".format(hostname)),
|
||||
])
|
||||
|
||||
alt_names = [x509.DNSName(u"localhost")]
|
||||
|
||||
def add_alt_name(name):
|
||||
if name not in alt_names:
|
||||
alt_names.append(name)
|
||||
|
||||
try:
|
||||
add_alt_name(x509.IPAddress(ipaddress.ip_address(hostname)))
|
||||
except ValueError:
|
||||
add_alt_name(x509.DNSName(hostname))
|
||||
|
||||
for loopback in ("127.0.0.1", "::1"):
|
||||
add_alt_name(x509.IPAddress(ipaddress.ip_address(loopback)))
|
||||
|
||||
cert = x509.CertificateBuilder().subject_name(
|
||||
subject
|
||||
).issuer_name(
|
||||
@@ -1167,7 +1302,7 @@ def generate_tls_key():
|
||||
# Valid for 1 year
|
||||
datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365)
|
||||
).add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]),
|
||||
x509.SubjectAlternativeName(alt_names),
|
||||
critical=False,
|
||||
).sign(key, hashes.SHA256())
|
||||
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
prompt-toolkit==3.0.52
|
||||
cryptography==48.0.0
|
||||
cryptography==48.0.0
|
||||
jproperties==2.1.2
|
||||
@@ -2,11 +2,56 @@ from pathlib import Path
|
||||
import requests
|
||||
import click
|
||||
import os
|
||||
from jproperties import Properties
|
||||
|
||||
PAPER_VERSION_API = "https://api.papermc.io/v2/projects/paper/versions/{}"
|
||||
PAPER_SERVER_JAR_API = "https://api.papermc.io/v2/projects/paper/versions/{}/builds/{}/downloads/paper-{}-{}.jar"
|
||||
MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
|
||||
|
||||
def read_server_properties(server_dir: Path) -> Properties:
|
||||
if not server_dir.exists():
|
||||
raise FileNotFoundError("Server directory does not exist")
|
||||
|
||||
props = Properties()
|
||||
|
||||
with open(server_dir / 'server.properties', 'rb') as config_file:
|
||||
props.load(config_file, encoding='utf-8')
|
||||
return props
|
||||
|
||||
|
||||
def save_server_properties(properties: Properties, server_dir: Path) -> None:
|
||||
path = server_dir / 'server.properties'
|
||||
|
||||
with path.open("wb") as config_file:
|
||||
properties.store(config_file, encoding='utf-8')
|
||||
|
||||
def activate_eula_file(server_dir: Path) -> bool:
|
||||
path = server_dir / 'eula.txt'
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError("EULA file does not exist. Did you run the server?")
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
for line in lines:
|
||||
print(f"{line}\n")
|
||||
|
||||
print("To activate eula file please read above text. If you agree this license, enter Y [not agree enter N]")
|
||||
result = str(input("Agree? :"))
|
||||
|
||||
if result.strip().lower() != "y":
|
||||
return False
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as file:
|
||||
for line in lines:
|
||||
if line.strip() == "eula=false":
|
||||
file.write("eula=true\n")
|
||||
continue
|
||||
file.write(line)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def jar_filename(filename: str | None, default: str) -> str:
|
||||
if filename:
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Source are from KiteeLauncher.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
def check_java_executable_and_major_version(java_executable_path):
|
||||
# test java runtimes are executable
|
||||
try:
|
||||
subprocess.run([java_executable_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
return True, None
|
||||
except PermissionError:
|
||||
return False, "Could not execute the Java executable. Permission denied."
|
||||
except Exception as e:
|
||||
return False, "Unknown error occurred while executing the Java executable. {}".format(e)
|
||||
|
||||
|
||||
def get_java_version_by_execute(java_executable_path):
|
||||
# test java runtimes are executable
|
||||
|
||||
# executable it
|
||||
try:
|
||||
result = subprocess.run([java_executable_path, '-version'], stderr=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
text=True)
|
||||
except PermissionError:
|
||||
return False, None, "Could not execute the Java executable. Permission denied."
|
||||
except Exception as e:
|
||||
return False, None, "Unknown error occurred while executing the Java executable. {}".format(e)
|
||||
|
||||
# Get output
|
||||
output = result.stderr
|
||||
|
||||
# Get major version (e.g., "21.0.3") and full version in the output
|
||||
match = re.search(r'java version "(\d+)(?:\.(\d+))?', output)
|
||||
|
||||
# Is for installation by launcher runtimes (Because is openjdk not oracle java....)
|
||||
if not match:
|
||||
match = re.search(r'openjdk version "(\d+)(?:\.(\d+))?', output)
|
||||
|
||||
if not match:
|
||||
return False, None, "Unsupported Java runtime build or current path is not a valid java executable."
|
||||
|
||||
try:
|
||||
major_version = match.group(1)
|
||||
# Special case for Java 8 where we need to use the second part (8) instead of 1
|
||||
if major_version == "1" and match.group(2):
|
||||
major_version = match.group(2)
|
||||
return True, major_version, None
|
||||
except IndexError:
|
||||
return False, None, "The target Java runtime version is too old or is not supported by this method."
|
||||
|
||||
def convert_java_version_tuple_to_major_version(java_version_tuple):
|
||||
try:
|
||||
_, major_version, *_ = java_version_tuple.split(".")
|
||||
return True, major_version
|
||||
except ValueError:
|
||||
return False, None
|
||||
|
||||
|
||||
def get_java_version_by_checkmyduke(java_executable_path):
|
||||
checkmyduke_jar = os.path.join("jar_files", "CheckMyDuke.jar")
|
||||
|
||||
if not os.path.exists(checkmyduke_jar):
|
||||
return False, None, "CheckMyDuke.jar not found"
|
||||
|
||||
# executable it
|
||||
try:
|
||||
result = subprocess.run([java_executable_path, '-jar', checkmyduke_jar], stderr=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
text=True)
|
||||
except PermissionError:
|
||||
return False, None, "Could not execute the Java executable. Permission denied."
|
||||
except Exception as e:
|
||||
return False, None, "Unknown error occurred while executing the Java executable. {}".format(e)\
|
||||
|
||||
|
||||
status, major_version = convert_java_version_tuple_to_major_version(result.stdout.strip())
|
||||
|
||||
if status:
|
||||
return True, major_version, None
|
||||
else:
|
||||
return False, None, "Unsupported Java runtime build or current path is not a valid java executable."
|
||||
|
||||
def search_available_java_runtimes_in_directory(target_directory, java_executable_name="java.exe" if os.name == "nt" else "java"):
|
||||
"""
|
||||
Using this function may take ~1min to search for available Java executable files.
|
||||
"""
|
||||
found_java_runtimes = []
|
||||
|
||||
for root, dirs, files in os.walk(target_directory):
|
||||
for file in files:
|
||||
if file == java_executable_name:
|
||||
found_java_runtimes.append(os.path.join(root, file))
|
||||
|
||||
|
||||
return found_java_runtimes
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user