Add connection history and some automatic server create process.

This commit is contained in:
2026-05-19 23:30:00 +08:00
parent 8bd09688bb
commit c50603a47f
5 changed files with 444 additions and 28 deletions
+137 -4
View File
@@ -26,7 +26,9 @@ SERVER_JAR_DIR = Path.home() / ".serverjar"
CLIENT_CERT_DIR = Path.home() / ".serverjar" / "client" / "cert" CLIENT_CERT_DIR = Path.home() / ".serverjar" / "client" / "cert"
CLIENT_CERT_SUFFIXES = {".pem", ".crt", ".cer"} CLIENT_CERT_SUFFIXES = {".pem", ".crt", ".cer"}
CLIENT_HISTORY_FILE = Path.home() / ".serverjar" / "history" / "history.txt" 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" CLIENT_LOG_DIR = Path.home() / ".serverjar" / "client" / "logs"
DEFAULT_CONNECT_TIMEOUT = 10.0
def add_client_cert(cert_path): def add_client_cert(cert_path):
source = Path(cert_path).expanduser() source = Path(cert_path).expanduser()
@@ -94,6 +96,68 @@ def create_client_tls_context(log=None, warn=None):
return context 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): def get_history(log=None, warn=None):
if not CLIENT_HISTORY_FILE.exists(): if not CLIENT_HISTORY_FILE.exists():
@@ -227,6 +291,7 @@ class ServerJarClient(Application):
self.cmds = [] self.cmds = []
self.current_index = None self.current_index = None
self.history_draft = "" self.history_draft = ""
self.connection_history = []
@self.kb.add("c-c") @self.kb.add("c-c")
def closing_kb(event): def closing_kb(event):
@@ -426,6 +491,43 @@ class ServerJarClient(Application):
self._log("History cleared") self._log("History cleared")
return True 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 = { cmd_map = {
"_exit": { "_exit": {
"func": _shutdown, "func": _shutdown,
@@ -459,10 +561,23 @@ class ServerJarClient(Application):
"func": _clear, "func": _clear,
"description": "Clear the log area", "description": "Clear the log area",
}, },
"_retry": {
"func": _enable_retry_mode,
"description": "Reconnect to the remote server if connection fails",
},
"_help": { "_help": {
"func": _help, "func": _help,
"description": "Display the help message", "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(): if not command.strip():
@@ -513,6 +628,8 @@ class ServerJarClient(Application):
required=False) required=False)
parser.add_argument('-r', '--retry', help="Retry when disconnect", action='store_true', default=False, parser.add_argument('-r', '--retry', help="Retry when disconnect", action='store_true', default=False,
required=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) parser.add_argument('--add-cert', help="Add server certificate", type=str, default=None, required=False)
args = parser.parse_args() args = parser.parse_args()
@@ -541,6 +658,7 @@ class ServerJarClient(Application):
# Save history # Save history
save_history(self.cmds, self._log, self._warn) save_history(self.cmds, self._log, self._warn)
save_connection_history(self.connection_history, self._log, self._warn)
# Exit ui event loop # Exit ui event loop
self.full_exit() self.full_exit()
@@ -600,6 +718,17 @@ class ServerJarClient(Application):
self.invalidate() 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): def client(self):
ptk_clear() ptk_clear()
@@ -627,10 +756,9 @@ class ServerJarClient(Application):
# Create connect # Create connect
if self.args.no_tls: if self.args.no_tls:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.create_connection((self.host, self.port), timeout=self.args.connect_timeout)
s.connect((self.host, self.port))
else: 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() context = self.get_tls_context()
s = context.wrap_socket(raw, server_hostname=self.host) s = context.wrap_socket(raw, server_hostname=self.host)
@@ -649,6 +777,10 @@ class ServerJarClient(Application):
data = s.recv(4096) data = s.recv(4096)
if not data: if not data:
raise ConnectionError("Server closed") 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") buffer += data.decode("utf-8", errors="replace")
while "\n" in buffer: while "\n" in buffer:
line, buffer = buffer.split("\n", 1) line, buffer = buffer.split("\n", 1)
@@ -721,7 +853,7 @@ class ServerJarClient(Application):
except (ConnectionError, OSError) as e: except (ConnectionError, OSError) as e:
if not self.closing_event.is_set(): 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...") self._warn(f"Disconnected: {e}, retrying...")
else: else:
self._err(f"Disconnected: {e}") self._err(f"Disconnected: {e}")
@@ -757,6 +889,7 @@ class ServerJarClient(Application):
self.args = self.arguments_parser() self.args = self.arguments_parser()
self.layout.focus(self.input_area) self.layout.focus(self.input_area)
self.cmds = get_history(self._log, self._warn) self.cmds = get_history(self._log, self._warn)
self.connection_history = get_connection_history(self._log, self._warn)
asyncio.create_task(self.consume_incoming()) asyncio.create_task(self.consume_incoming())
self.client_thread.start() self.client_thread.start()
+158 -23
View File
@@ -3,6 +3,7 @@ ServerJar
Wei - 2026 Wei - 2026
""" """
import platform
import re import re
import signal import signal
import socketserver import socketserver
@@ -17,11 +18,14 @@ import time
import datetime import datetime
import base64 import base64
import hmac import hmac
import ipaddress
import shlex
from pathlib import Path from pathlib import Path
import click import click
import yaml import yaml
from utils.common import 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, \
read_server_properties, save_server_properties, activate_eula_file
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
from cryptography import x509 from cryptography import x509
@@ -89,6 +93,86 @@ def load_settings():
return s 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() @main.command()
@click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name") @click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name")
@click.option("--mc-version", "-m", @click.option("--mc-version", "-m",
@@ -126,10 +210,13 @@ def load_settings():
help="Hostname of the server", required=True) help="Hostname of the server", required=True)
@click.option("--server-port", "-srp", @click.option("--server-port", "-srp",
help="Port of the server", required=True) 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, 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_dir = Path("servers", name)
server_type = server_type.lower() server_type = server_type.lower()
property_overrides = parse_server_property_overrides(server_properties)
def prepare_server_dir(): def prepare_server_dir():
if server_dir.exists(): if server_dir.exists():
@@ -210,22 +297,32 @@ def create_server(name, mc_version, build, server_type, snapshot, latest, list_b
exit("User aborted.") exit("User aborted.")
return return
extra_args += "nogui" if nogui else ""
args = [ args = [
java_exec_path, java_exec_path,
"-Xms{}".format(x_memory_initial), "-Xms{}".format(x_memory_initial),
"-Xmx{}".format(x_memory_maximum), "-Xmx{}".format(x_memory_maximum),
"-jar", "-jar",
out.absolute().as_posix(), out.absolute().as_posix(),
extra_args,
] ]
if extra_args:
args.extend(shlex.split(extra_args))
if nogui:
args.append("nogui")
if custom_args: if custom_args:
print("Will use custom commands as replacement.") print("Will use custom commands as replacement.")
args = custom_args args = shlex.split(custom_args)
print(f"Server command: {' '.join(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: with settings.edit() as s:
print("Saving...") print("Saving...")
s["servers"].append({ s["servers"].append({
@@ -292,6 +389,8 @@ class SocketServer:
self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
self._ssl_context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile) self._ssl_context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
self.commands = {}
# ------------------------- # -------------------------
# Socket Server # Socket Server
# ------------------------- # -------------------------
@@ -339,11 +438,12 @@ class SocketServer:
"__help": "Display this help message", "__help": "Display this help message",
"__list": "List available server shells", "__list": "List available server shells",
"__exit": "Close the current socket connection", "__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", "__c <server name>": "Connect to a server shell",
"__d": "Disconnect from the current 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", "__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", "__download_log": "Download the attached server's saved log file",
"__system_info": "Show system information",
} }
attached_commands = { attached_commands = {
"__status": "Show target server process status", "__status": "Show target server process status",
@@ -597,12 +697,18 @@ class SocketServer:
self.request.sendall(b"[SYS] bye\n") self.request.sendall(b"[SYS] bye\n")
return return
elif cmd == "__stop_all": elif cmd == "__stop_all":
self.request.sendall( if not mgr.enable_tls:
f"[SYS] Stopping all servers...bye\n".encode("utf-8") self.request.sendall(
) f"[SYS] Command \"__stop_all\" only works when tls support is enabled.\n".encode("utf-8")
mgr.broadcast_all(mgr.tcp_server.clients, "[SYS] Server stopping... (Stop by remote client)") )
mgr.stop_event.set() else:
return 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"): elif cmd.startswith("__sync_log"):
mgr.send_sync_log(self.request, current_server, cmd) mgr.send_sync_log(self.request, current_server, cmd)
elif cmd == "__download_log": elif cmd == "__download_log":
@@ -633,6 +739,14 @@ class SocketServer:
f"[SYS] Disconnected from current server \"{current_server}\"'s shell.\n".encode( f"[SYS] Disconnected from current server \"{current_server}\"'s shell.\n".encode(
"utf-8") "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: else:
if current_server is not None: if current_server is not None:
receiver = mgr.get_command_receiver(current_server) receiver = mgr.get_command_receiver(current_server)
@@ -887,12 +1001,12 @@ class Server:
def _stdout_reader_loop(self): def _stdout_reader_loop(self):
self.logger.info("[PROC] stdout reader started") self.logger.info("[PROC] stdout reader started")
while self.running: while True:
with self.proc_lock: with self.proc_lock:
proc = self.proc proc = self.proc
out = proc.stdout if proc else None 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") self.logger.info("[PROC] process ended / stdout closed")
break break
@@ -915,8 +1029,11 @@ class Server:
if not self.proc.stdin: if not self.proc.stdin:
return False return False
self.proc.stdin.write(command + "\n") try:
self.proc.stdin.flush() self.proc.stdin.write(command + "\n")
self.proc.stdin.flush()
except OSError:
return False
return True return True
def stop_process(self, timeout: float = 10.0): def stop_process(self, timeout: float = 10.0):
@@ -959,9 +1076,9 @@ class Server:
return return
self.stopping = True self.stopping = True
self.running = False
self.stop_process() self.stop_process()
self.running = False
def restart(self): def restart(self):
if self.running: if self.running:
@@ -995,7 +1112,7 @@ class Server:
return True, f"Server \"{self.name}\" process started" return True, f"Server \"{self.name}\" process started"
elif command == "__info": elif command == "__info":
return True, (f"serverName: {self.name}\n" 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"description: {self.description}\n"
f"arguments: {self.args}\n" f"arguments: {self.args}\n"
f"host: {self.host}\n" f"host: {self.host}\n"
@@ -1012,8 +1129,11 @@ class Server:
if not self.proc.stdin: if not self.proc.stdin:
return False, "Process standard input are not available." return False, "Process standard input are not available."
self.proc.stdin.write(command + "\n") try:
self.proc.stdin.flush() 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." return True, "Command sent."
def load_all_server_from_settings(settings: FileSettings): def load_all_server_from_settings(settings: FileSettings):
@@ -1138,7 +1258,8 @@ def runserver(keep_running: bool):
logger.info("Stopped!") logger.info("Stopped!")
@main.command() @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...") print("Generating TLS key...")
key = rsa.generate_private_key( key = rsa.generate_private_key(
public_exponent=65537, public_exponent=65537,
@@ -1150,9 +1271,23 @@ def generate_tls_key():
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"),
x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"), x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Dev Org"), 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( cert = x509.CertificateBuilder().subject_name(
subject subject
).issuer_name( ).issuer_name(
@@ -1167,7 +1302,7 @@ def generate_tls_key():
# Valid for 1 year # Valid for 1 year
datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365) datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365)
).add_extension( ).add_extension(
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]), x509.SubjectAlternativeName(alt_names),
critical=False, critical=False,
).sign(key, hashes.SHA256()) ).sign(key, hashes.SHA256())
+1
View File
@@ -1,2 +1,3 @@
prompt-toolkit==3.0.52 prompt-toolkit==3.0.52
cryptography==48.0.0 cryptography==48.0.0
jproperties==2.1.2
+45
View File
@@ -2,11 +2,56 @@ from pathlib import Path
import requests import requests
import click import click
import os import os
from jproperties import Properties
PAPER_VERSION_API = "https://api.papermc.io/v2/projects/paper/versions/{}" 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" 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" 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: def jar_filename(filename: str | None, default: str) -> str:
if filename: if filename:
+102
View File
@@ -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