Changed code to support older Python versions

This commit is contained in:
Malasaur 2025-12-01 23:27:09 +01:00
parent eb92d2d36f
commit 582458cdd0
5027 changed files with 794942 additions and 4 deletions

View file

@ -0,0 +1,202 @@
from __future__ import annotations
import dns.resolver
import sys
import json
import argparse
import socket
import dataclasses
from typing import TYPE_CHECKING
from mcstatus import JavaServer, BedrockServer
from mcstatus.responses import JavaStatusResponse
from mcstatus.motd import Motd
if TYPE_CHECKING:
from typing_extensions import TypeAlias
SupportedServers: TypeAlias = "JavaServer | BedrockServer"
PING_PACKET_FAIL_WARNING = (
"warning: contacting {address} failed with a 'ping' packet but succeeded with a 'status' packet,\n"
" this is likely a bug in the server-side implementation.\n"
' (note: ping packet failed due to "{ping_exc}")\n'
" for more details, see: https://mcstatus.readthedocs.io/en/stable/pages/faq/\n"
)
QUERY_FAIL_WARNING = (
"The server did not respond to the query protocol."
"\nPlease ensure that the server has enable-query turned on,"
" and that the necessary port (same as server-port unless query-port is set) is open in any firewall(s)."
"\nSee https://minecraft.wiki/w/Query for further information."
)
def _motd(motd: Motd) -> str:
"""Formats MOTD for human-readable output, with leading line break
if multiline."""
s = motd.to_ansi()
return f"\n{s}" if "\n" in s else f" {s}"
def _kind(serv: SupportedServers) -> str:
if isinstance(serv, JavaServer):
return "Java"
elif isinstance(serv, BedrockServer):
return "Bedrock"
else:
raise ValueError(f"unsupported server for kind: {serv}")
def _ping_with_fallback(server: SupportedServers) -> float:
# bedrock doesn't have ping method
if isinstance(server, BedrockServer):
return server.status().latency
# try faster ping packet first, falling back to status with a warning.
ping_exc = None
try:
return server.ping(tries=1)
except Exception as e:
ping_exc = e
latency = server.status().latency
address = f"{server.address.host}:{server.address.port}"
print(
PING_PACKET_FAIL_WARNING.format(address=address, ping_exc=ping_exc),
file=sys.stderr,
)
return latency
def ping_cmd(server: SupportedServers) -> int:
print(_ping_with_fallback(server))
return 0
def status_cmd(server: SupportedServers) -> int:
response = server.status()
java_res = response if isinstance(response, JavaStatusResponse) else None
if not java_res:
player_sample = ""
elif java_res.players.sample is not None:
player_sample = str([f"{player.name} ({player.id})" for player in java_res.players.sample])
else:
player_sample = "No players online"
if player_sample:
player_sample = " " + player_sample
print(f"version: {_kind(server)} {response.version.name} (protocol {response.version.protocol})")
print(f"motd:{_motd(response.motd)}")
print(f"players: {response.players.online}/{response.players.max}{player_sample}")
print(f"ping: {response.latency:.2f} ms")
return 0
def json_cmd(server: SupportedServers) -> int:
data = {"online": False, "kind": _kind(server)}
status_res = query_res = exn = None
try:
status_res = server.status(tries=1)
except Exception as e:
exn = exn or e
try:
if isinstance(server, JavaServer):
query_res = server.query(tries=1)
except Exception as e:
exn = exn or e
# construct 'data' dict outside try/except to ensure data processing errors
# are noticed.
data["online"] = bool(status_res or query_res)
if not data["online"]:
assert exn, "server offline but no exception?"
data["error"] = str(exn)
if status_res is not None:
data["status"] = dataclasses.asdict(status_res)
# ensure we are overwriting the motd and not making a new dict field
assert "motd" in data["status"], "motd field missing. has it been renamed?"
data["status"]["motd"] = status_res.motd.simplify().to_minecraft()
if query_res is not None:
# TODO: QueryResponse is not (yet?) a dataclass
data["query"] = qdata = {}
qdata["ip"] = query_res.raw["hostip"]
qdata["port"] = query_res.raw["hostport"]
qdata["map"] = query_res.map_name
qdata["plugins"] = query_res.software.plugins
qdata["raw"] = query_res.raw
json.dump(data, sys.stdout)
return 0
def query_cmd(server: SupportedServers) -> int:
if not isinstance(server, JavaServer):
print("The 'query' protocol is only supported by Java servers.", file=sys.stderr)
return 1
try:
response = server.query()
except socket.timeout:
print(QUERY_FAIL_WARNING, file=sys.stderr)
return 1
print(f"host: {response.raw['hostip']}:{response.raw['hostport']}")
print(f"software: {_kind(server)} {response.software.version} {response.software.brand}")
print(f"motd:{_motd(response.motd)}")
print(f"plugins: {response.software.plugins}")
print(f"players: {response.players.online}/{response.players.max} {response.players.list}")
return 0
def main(argv: list[str] = sys.argv[1:]) -> int:
parser = argparse.ArgumentParser(
"mcstatus",
description="""
mcstatus provides an easy way to query 1.7 or newer Minecraft servers for any
information they can expose. It provides three modes of access: query, status,
ping and json.
""",
)
parser.add_argument("address", help="The address of the server.")
parser.add_argument("--bedrock", help="Specifies that 'address' is a Bedrock server (default: Java).", action="store_true")
subparsers = parser.add_subparsers(title="commands", description="Command to run, defaults to 'status'.")
parser.set_defaults(func=status_cmd)
subparsers.add_parser("ping", help="Ping server for latency.").set_defaults(func=ping_cmd)
subparsers.add_parser("status", help="Prints server status.").set_defaults(func=status_cmd)
subparsers.add_parser(
"query", help="Prints detailed server information. Must be enabled in servers' server.properties file."
).set_defaults(func=query_cmd)
subparsers.add_parser(
"json",
help="Prints server status and query in json.",
).set_defaults(func=json_cmd)
args = parser.parse_args(argv)
lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup
try:
server = lookup(args.address)
return args.func(server)
except (socket.timeout, socket.gaierror, dns.resolver.NoNameservers, ConnectionError, TimeoutError) as e:
# catch and hide traceback for expected user-facing errors
print(f"Error: {e!r}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())