From 2ee93a5f28c2a95b2531d4a07c2865d6fb283c42 Mon Sep 17 00:00:00 2001 From: rina Date: Mon, 15 Jul 2024 22:12:59 +1000 Subject: [PATCH 01/36] fix crash with one command-line argument --- mcstatus/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index e1244752..248edb21 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -81,7 +81,9 @@ def main() -> None: parser.add_argument("address", help="The address of the server.") - subparsers = parser.add_subparsers() + subparsers = parser.add_subparsers(title='commands', description="Command to run, defaults to 'status'.") + parser.set_defaults(func=status) + subparsers.add_parser("ping", help="Ping server for latency.").set_defaults(func=ping) subparsers.add_parser( "status", help="Prints server status. Supported by all Minecraft servers that are version 1.7 or higher." From ec1a0f5b65d4a16447a46e0f8d95693b1b67d530 Mon Sep 17 00:00:00 2001 From: rina Date: Mon, 15 Jul 2024 22:50:49 +1000 Subject: [PATCH 02/36] implement ping() on BedrockServer simply measures the latency of status() --- mcstatus/server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mcstatus/server.py b/mcstatus/server.py index 7bb57b4c..08369faf 100644 --- a/mcstatus/server.py +++ b/mcstatus/server.py @@ -188,6 +188,9 @@ class BedrockServer(MCServer): DEFAULT_PORT = 19132 + def ping(self) -> float: + return self.status().latency + @retry(tries=3) def status(self, **kwargs) -> BedrockStatusResponse: """Checks the status of a Minecraft Bedrock Edition server. From 60e3512494c9478e54eeb9ab749839c79e2920cf Mon Sep 17 00:00:00 2001 From: rina Date: Mon, 15 Jul 2024 22:51:23 +1000 Subject: [PATCH 03/36] support Bedrock servers in CLI done in a slightly ad-hoc way, but this is the best we can do given the split of the response types. --- mcstatus/__main__.py | 74 +++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 248edb21..ee974293 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -1,57 +1,76 @@ from __future__ import annotations +import sys import argparse import socket +from typing import TYPE_CHECKING from json import dumps as json_dumps -from mcstatus import JavaServer +from mcstatus import JavaServer, BedrockServer +from mcstatus.responses import JavaStatusResponse +if TYPE_CHECKING: + SupportedServers = JavaServer | BedrockServer -def ping(server: JavaServer) -> None: - print(f"{server.ping()}ms") +def ping(server: SupportedServers) -> int: + print(f"{server.ping():.2f}ms") + return 0 -def status(server: JavaServer) -> None: +def status(server: SupportedServers) -> int: response = server.status() - if response.players.sample is not None: - player_sample = str([f"{player.name} ({player.id})" for player in response.players.sample]) + + java_res = response if isinstance(response, JavaStatusResponse) else None + + if java_res and java_res.players.sample is not None: + player_sample = [f"{player.name} ({player.id})" for player in java_res.players.sample] else: player_sample = "No players online" print(f"version: v{response.version.name} (protocol {response.version.protocol})") - print(f'motd: "{response.motd}"') + print(f'motd: {response.motd.to_ansi()}') print(f"players: {response.players.online}/{response.players.max} {player_sample}") + print(f"ping: {response.latency:.2f} ms") + return 0 -def json(server: JavaServer) -> None: +def json(server: SupportedServers) -> int: data = {} data["online"] = False # Build data with responses and quit on exception try: status_res = server.status(tries=1) + java_res = status_res if isinstance(status_res, JavaStatusResponse) else None data["version"] = status_res.version.name data["protocol"] = status_res.version.protocol data["motd"] = status_res.motd.raw data["player_count"] = status_res.players.online data["player_max"] = status_res.players.max data["players"] = [] - if status_res.players.sample is not None: - data["players"] = [{"name": player.name, "id": player.id} for player in status_res.players.sample] + if java_res and java_res.players.sample is not None: + data["players"] = [{"name": player.name, "id": player.id} for player in java_res.players.sample] data["ping"] = status_res.latency data["online"] = True - query_res = server.query(tries=1) # type: ignore[call-arg] # tries is supported with retry decorator - data["host_ip"] = query_res.raw["hostip"] - data["host_port"] = query_res.raw["hostport"] - data["map"] = query_res.map - data["plugins"] = query_res.software.plugins + if isinstance(server, JavaServer): + query_res = server.query(tries=1) + data["host_ip"] = query_res.raw["hostip"] + data["host_port"] = query_res.raw["hostport"] + data["map"] = query_res.map + data["plugins"] = query_res.software.plugins except Exception: # TODO: Check what this actually excepts pass + print(json_dumps(data)) + return 0 -def query(server: JavaServer) -> None: +def query(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: @@ -59,17 +78,20 @@ def query(server: JavaServer) -> None: "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://wiki.vg/Query for further information." + "\nSee https://wiki.vg/Query for further information.", + file=sys.stderr, ) - return + return 1 + print(f"host: {response.raw['hostip']}:{response.raw['hostport']}") print(f"software: v{response.software.version} {response.software.brand}") print(f"plugins: {response.software.plugins}") - print(f'motd: "{response.motd}"') + print(f'motd: {response.motd.to_ansi()}') print(f"players: {response.players.online}/{response.players.max} {response.players.names}") + return 0 -def main() -> None: +def main(argv: list[str]) -> int: parser = argparse.ArgumentParser( "mcstatus", description=""" @@ -80,6 +102,8 @@ def main() -> None: ) parser.add_argument("address", help="The address of the server.") + parser.add_argument('--bedrock', help="Server 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) @@ -96,11 +120,11 @@ def main() -> None: help="Prints server status and query in json. Supported by all Minecraft servers that are version 1.7 or higher.", ).set_defaults(func=json) - args = parser.parse_args() - server = JavaServer.lookup(args.address) - - args.func(server) + args = parser.parse_args(argv) + lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup + server = lookup(args.address) + return args.func(server) if __name__ == "__main__": - main() + sys.exit(main(sys.argv[1:])) From a0e41a264533e4eca7b5a064558e5869467b0c4d Mon Sep 17 00:00:00 2001 From: rina Date: Mon, 15 Jul 2024 23:09:03 +1000 Subject: [PATCH 04/36] print server kind and tweak player sample printing --- mcstatus/__main__.py | 24 +++++++++++++++++------- mcstatus/server.py | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index ee974293..4ec5b5cc 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -13,7 +13,7 @@ SupportedServers = JavaServer | BedrockServer def ping(server: SupportedServers) -> int: - print(f"{server.ping():.2f}ms") + print(f"{server.ping():.2f}") return 0 @@ -22,14 +22,19 @@ def status(server: SupportedServers) -> int: java_res = response if isinstance(response, JavaStatusResponse) else None - if java_res and java_res.players.sample is not None: - player_sample = [f"{player.name} ({player.id})" for player in java_res.players.sample] + 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" - print(f"version: v{response.version.name} (protocol {response.version.protocol})") + if player_sample: + player_sample = " " + player_sample + + print(f"version: {server.kind()} {response.version.name} (protocol {response.version.protocol})") print(f'motd: {response.motd.to_ansi()}') - print(f"players: {response.players.online}/{response.players.max} {player_sample}") + print(f"players: {response.players.online}/{response.players.max}{player_sample}") print(f"ping: {response.latency:.2f} ms") return 0 @@ -37,6 +42,7 @@ def status(server: SupportedServers) -> int: def json(server: SupportedServers) -> int: data = {} data["online"] = False + data["kind"] = server.kind() # Build data with responses and quit on exception try: status_res = server.status(tries=1) @@ -102,7 +108,7 @@ def main(argv: list[str]) -> int: ) parser.add_argument("address", help="The address of the server.") - parser.add_argument('--bedrock', help="Server is a Bedrock server (default: Java).", + 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'.") @@ -124,7 +130,11 @@ def main(argv: list[str]) -> int: lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup server = lookup(args.address) - return args.func(server) + try: + return args.func(server) + except Exception as e: + print("Error:", e, file=sys.stderr) + return 1 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/mcstatus/server.py b/mcstatus/server.py index 08369faf..0f49d1ae 100644 --- a/mcstatus/server.py +++ b/mcstatus/server.py @@ -1,6 +1,6 @@ from __future__ import annotations -from abc import ABC +from abc import ABC, abstractmethod from typing import TYPE_CHECKING from mcstatus.address import Address, async_minecraft_srv_address_lookup, minecraft_srv_address_lookup @@ -54,11 +54,20 @@ def lookup(cls, address: str, timeout: float = 3) -> Self: return cls(addr.host, addr.port, timeout=timeout) + @classmethod + @abstractmethod + def kind(cls) -> str: ... + + class JavaServer(MCServer): """Base class for a Minecraft Java Edition server.""" DEFAULT_PORT = 25565 + @classmethod + def kind(cls) -> str: + return "Java" + @classmethod def lookup(cls, address: str, timeout: float = 3) -> Self: """Mimics minecraft's server address field. @@ -188,6 +197,10 @@ class BedrockServer(MCServer): DEFAULT_PORT = 19132 + @classmethod + def kind(cls) -> str: + return "Bedrock" + def ping(self) -> float: return self.status().latency From 47ce9448534d13f21af4d7a962ea626e96f1e0a7 Mon Sep 17 00:00:00 2001 From: rina Date: Tue, 16 Jul 2024 21:48:02 +1000 Subject: [PATCH 05/36] JavaServer ping() doesn't work? --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 4ec5b5cc..7829cdb9 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -13,7 +13,7 @@ SupportedServers = JavaServer | BedrockServer def ping(server: SupportedServers) -> int: - print(f"{server.ping():.2f}") + print(f"{server.status().latency}") # XXX: JavaServer.ping() not working? return 0 From 213fc2af0eebc88c36317f3e405b32b739caac7f Mon Sep 17 00:00:00 2001 From: rina Date: Wed, 17 Jul 2024 21:06:02 +1000 Subject: [PATCH 06/36] fix precommit warnings --- mcstatus/__main__.py | 11 ++++++----- mcstatus/server.py | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 7829cdb9..0b000c5e 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: SupportedServers = JavaServer | BedrockServer + def ping(server: SupportedServers) -> int: print(f"{server.status().latency}") # XXX: JavaServer.ping() not working? return 0 @@ -33,7 +34,7 @@ def status(server: SupportedServers) -> int: player_sample = " " + player_sample print(f"version: {server.kind()} {response.version.name} (protocol {response.version.protocol})") - print(f'motd: {response.motd.to_ansi()}') + print(f"motd: {response.motd.to_ansi()}") print(f"players: {response.players.online}/{response.players.max}{player_sample}") print(f"ping: {response.latency:.2f} ms") return 0 @@ -92,7 +93,7 @@ def query(server: SupportedServers) -> int: print(f"host: {response.raw['hostip']}:{response.raw['hostport']}") print(f"software: v{response.software.version} {response.software.brand}") print(f"plugins: {response.software.plugins}") - print(f'motd: {response.motd.to_ansi()}') + print(f"motd: {response.motd.to_ansi()}") print(f"players: {response.players.online}/{response.players.max} {response.players.names}") return 0 @@ -108,10 +109,9 @@ def main(argv: list[str]) -> int: ) 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') + 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'.") + subparsers = parser.add_subparsers(title="commands", description="Command to run, defaults to 'status'.") parser.set_defaults(func=status) subparsers.add_parser("ping", help="Ping server for latency.").set_defaults(func=ping) @@ -136,5 +136,6 @@ def main(argv: list[str]) -> int: print("Error:", e, file=sys.stderr) return 1 + if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/mcstatus/server.py b/mcstatus/server.py index 0f49d1ae..0dadef74 100644 --- a/mcstatus/server.py +++ b/mcstatus/server.py @@ -53,7 +53,6 @@ def lookup(cls, address: str, timeout: float = 3) -> Self: addr = Address.parse_address(address, default_port=cls.DEFAULT_PORT) return cls(addr.host, addr.port, timeout=timeout) - @classmethod @abstractmethod def kind(cls) -> str: ... From 848fc575e3fe47c32292438c6b114b1ecc20906a Mon Sep 17 00:00:00 2001 From: rina Date: Wed, 17 Jul 2024 21:28:05 +1000 Subject: [PATCH 07/36] review: remove Bedrock ping() --- mcstatus/server.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mcstatus/server.py b/mcstatus/server.py index 0dadef74..ad912035 100644 --- a/mcstatus/server.py +++ b/mcstatus/server.py @@ -200,9 +200,6 @@ class BedrockServer(MCServer): def kind(cls) -> str: return "Bedrock" - def ping(self) -> float: - return self.status().latency - @retry(tries=3) def status(self, **kwargs) -> BedrockStatusResponse: """Checks the status of a Minecraft Bedrock Edition server. From f9577b55eb9e1777fe55db242c8b4660defc42e7 Mon Sep 17 00:00:00 2001 From: rina Date: Wed, 17 Jul 2024 21:29:54 +1000 Subject: [PATCH 08/36] review: change CLI ping comment to be more permanent --- mcstatus/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 0b000c5e..78d995e4 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -14,7 +14,10 @@ def ping(server: SupportedServers) -> int: - print(f"{server.status().latency}") # XXX: JavaServer.ping() not working? + # this method supports both Java and Bedrock. + # only Java supports the `ping` packet, and even then not always: + # https://github.com/py-mine/mcstatus/issues/850 + print(f"{server.status().latency}") return 0 From 3a0ee8c39d21c3b76beb32e0eeb0d843ea8447b5 Mon Sep 17 00:00:00 2001 From: rina Date: Wed, 17 Jul 2024 21:40:30 +1000 Subject: [PATCH 09/36] review: formalise hostip/hostport within QueryResponse --- mcstatus/__main__.py | 6 +++--- mcstatus/querier.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 78d995e4..a37b3bee 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -65,8 +65,8 @@ def json(server: SupportedServers) -> int: if isinstance(server, JavaServer): query_res = server.query(tries=1) - data["host_ip"] = query_res.raw["hostip"] - data["host_port"] = query_res.raw["hostport"] + data["host_ip"] = query_res.hostip + data["host_port"] = query_res.hostport data["map"] = query_res.map data["plugins"] = query_res.software.plugins except Exception: # TODO: Check what this actually excepts @@ -93,7 +93,7 @@ def query(server: SupportedServers) -> int: ) return 1 - print(f"host: {response.raw['hostip']}:{response.raw['hostport']}") + print(f"host: {response.hostip}:{response.hostport}") print(f"software: v{response.software.version} {response.software.brand}") print(f"plugins: {response.software.plugins}") print(f"motd: {response.motd.to_ansi()}") diff --git a/mcstatus/querier.py b/mcstatus/querier.py index f5da4a5c..53669a05 100644 --- a/mcstatus/querier.py +++ b/mcstatus/querier.py @@ -132,6 +132,8 @@ def __init__(self, version: str, plugins: str): map: str players: Players software: Software + hostip: str + hostport: int def __init__(self, raw: dict[str, str], players: list[str]): try: @@ -140,8 +142,10 @@ def __init__(self, raw: dict[str, str], players: list[str]): self.map = raw["map"] self.players = QueryResponse.Players(raw["numplayers"], raw["maxplayers"], players) self.software = QueryResponse.Software(raw["version"], raw["plugins"]) - except KeyError: - raise ValueError("The provided data is not valid") + self.hostip = raw["hostip"] + self.hostport = int(raw["hostport"]) + except KeyError as e: + raise ValueError("The provided data is not valid") from e @classmethod def from_connection(cls, response: Connection) -> Self: From 73a454368de847de748dffc4135d57ea61920b33 Mon Sep 17 00:00:00 2001 From: rina Date: Wed, 17 Jul 2024 21:44:47 +1000 Subject: [PATCH 10/36] review: only squash traceback in common errors --- mcstatus/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index a37b3bee..7e32cd6d 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -135,8 +135,9 @@ def main(argv: list[str]) -> int: try: return args.func(server) - except Exception as e: - print("Error:", e, file=sys.stderr) + except (socket.timeout, socket.gaierror, ValueError) as e: + # catch and hide traceback for expected user-facing errors + print(f"Error: {e}", file=sys.stderr) return 1 From 47f62fc3ddcea06c74b62173ebb181cf03e757b7 Mon Sep 17 00:00:00 2001 From: rina Date: Fri, 19 Jul 2024 18:30:40 +1000 Subject: [PATCH 11/36] review: leading line break for multi-line motd --- mcstatus/__main__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 7e32cd6d..6d7b52d1 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -37,7 +37,9 @@ def status(server: SupportedServers) -> int: player_sample = " " + player_sample print(f"version: {server.kind()} {response.version.name} (protocol {response.version.protocol})") - print(f"motd: {response.motd.to_ansi()}") + motd = response.motd.to_ansi() + motd = f"\n{motd}" if "\n" in motd else f" {motd}" + print(f"motd:{motd}") print(f"players: {response.players.online}/{response.players.max}{player_sample}") print(f"ping: {response.latency:.2f} ms") return 0 @@ -96,7 +98,9 @@ def query(server: SupportedServers) -> int: print(f"host: {response.hostip}:{response.hostport}") print(f"software: v{response.software.version} {response.software.brand}") print(f"plugins: {response.software.plugins}") - print(f"motd: {response.motd.to_ansi()}") + motd = response.motd.to_ansi() + motd = f"\n{motd}" if "\n" in motd else f" {motd}" + print(f"motd:{motd}") print(f"players: {response.players.online}/{response.players.max} {response.players.names}") return 0 From 1130b99cc72854f684b7b902e02fc2ca79cc4160 Mon Sep 17 00:00:00 2001 From: rina Date: Fri, 19 Jul 2024 18:31:00 +1000 Subject: [PATCH 12/36] Revert "review: formalise hostip/hostport within QueryResponse" This reverts commit 3a0ee8c39d21c3b76beb32e0eeb0d843ea8447b5. --- mcstatus/__main__.py | 6 +++--- mcstatus/querier.py | 8 ++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 6d7b52d1..de97b8db 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -67,8 +67,8 @@ def json(server: SupportedServers) -> int: if isinstance(server, JavaServer): query_res = server.query(tries=1) - data["host_ip"] = query_res.hostip - data["host_port"] = query_res.hostport + data["host_ip"] = query_res.raw["hostip"] + data["host_port"] = query_res.raw["hostport"] data["map"] = query_res.map data["plugins"] = query_res.software.plugins except Exception: # TODO: Check what this actually excepts @@ -95,7 +95,7 @@ def query(server: SupportedServers) -> int: ) return 1 - print(f"host: {response.hostip}:{response.hostport}") + print(f"host: {response.raw['hostip']}:{response.raw['hostport']}") print(f"software: v{response.software.version} {response.software.brand}") print(f"plugins: {response.software.plugins}") motd = response.motd.to_ansi() diff --git a/mcstatus/querier.py b/mcstatus/querier.py index 53669a05..f5da4a5c 100644 --- a/mcstatus/querier.py +++ b/mcstatus/querier.py @@ -132,8 +132,6 @@ def __init__(self, version: str, plugins: str): map: str players: Players software: Software - hostip: str - hostport: int def __init__(self, raw: dict[str, str], players: list[str]): try: @@ -142,10 +140,8 @@ def __init__(self, raw: dict[str, str], players: list[str]): self.map = raw["map"] self.players = QueryResponse.Players(raw["numplayers"], raw["maxplayers"], players) self.software = QueryResponse.Software(raw["version"], raw["plugins"]) - self.hostip = raw["hostip"] - self.hostport = int(raw["hostport"]) - except KeyError as e: - raise ValueError("The provided data is not valid") from e + except KeyError: + raise ValueError("The provided data is not valid") @classmethod def from_connection(cls, response: Connection) -> Self: From bb51ac5ebd8ec00876c4ab8e4c7a48a1ef140daa Mon Sep 17 00:00:00 2001 From: rina Date: Fri, 19 Jul 2024 18:34:00 +1000 Subject: [PATCH 13/36] review: use motd.to_minecraft() in json --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index de97b8db..2389c7b7 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -55,7 +55,7 @@ def json(server: SupportedServers) -> int: java_res = status_res if isinstance(status_res, JavaStatusResponse) else None data["version"] = status_res.version.name data["protocol"] = status_res.version.protocol - data["motd"] = status_res.motd.raw + data["motd"] = status_res.motd.to_minecraft() data["player_count"] = status_res.players.online data["player_max"] = status_res.players.max data["players"] = [] From b6a28fa452818f1983cef30506c6b84ff4e635a8 Mon Sep 17 00:00:00 2001 From: rina Date: Fri, 19 Jul 2024 18:51:33 +1000 Subject: [PATCH 14/36] review amendment: factor out motd line breaking --- mcstatus/__main__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 2389c7b7..4bf258a8 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -8,11 +8,19 @@ from mcstatus import JavaServer, BedrockServer from mcstatus.responses import JavaStatusResponse +from mcstatus.motd import Motd if TYPE_CHECKING: SupportedServers = JavaServer | BedrockServer +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 ping(server: SupportedServers) -> int: # this method supports both Java and Bedrock. # only Java supports the `ping` packet, and even then not always: @@ -37,9 +45,7 @@ def status(server: SupportedServers) -> int: player_sample = " " + player_sample print(f"version: {server.kind()} {response.version.name} (protocol {response.version.protocol})") - motd = response.motd.to_ansi() - motd = f"\n{motd}" if "\n" in motd else f" {motd}" - print(f"motd:{motd}") + 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 @@ -97,10 +103,8 @@ def query(server: SupportedServers) -> int: print(f"host: {response.raw['hostip']}:{response.raw['hostport']}") print(f"software: v{response.software.version} {response.software.brand}") + print(f"motd:{_motd(response.motd)}") print(f"plugins: {response.software.plugins}") - motd = response.motd.to_ansi() - motd = f"\n{motd}" if "\n" in motd else f" {motd}" - print(f"motd:{motd}") print(f"players: {response.players.online}/{response.players.max} {response.players.names}") return 0 From de8be4d307675c62443d67f3f23118464ac7e74f Mon Sep 17 00:00:00 2001 From: rina Date: Sat, 20 Jul 2024 11:23:07 +1000 Subject: [PATCH 15/36] review: refactor CLI json() to use dataclasses.asdict() --- mcstatus/__main__.py | 56 ++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 4bf258a8..b31be34b 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -1,10 +1,11 @@ from __future__ import annotations import sys +import json as _json import argparse import socket +import dataclasses from typing import TYPE_CHECKING -from json import dumps as json_dumps from mcstatus import JavaServer, BedrockServer from mcstatus.responses import JavaStatusResponse @@ -52,35 +53,38 @@ def status(server: SupportedServers) -> int: def json(server: SupportedServers) -> int: - data = {} - data["online"] = False - data["kind"] = server.kind() - # Build data with responses and quit on exception + data = {"online": False, "kind": server.kind()} + + status_res = query_res = None try: status_res = server.status(tries=1) - java_res = status_res if isinstance(status_res, JavaStatusResponse) else None - data["version"] = status_res.version.name - data["protocol"] = status_res.version.protocol - data["motd"] = status_res.motd.to_minecraft() - data["player_count"] = status_res.players.online - data["player_max"] = status_res.players.max - data["players"] = [] - if java_res and java_res.players.sample is not None: - data["players"] = [{"name": player.name, "id": player.id} for player in java_res.players.sample] - - data["ping"] = status_res.latency - data["online"] = True - if isinstance(server, JavaServer): query_res = server.query(tries=1) - data["host_ip"] = query_res.raw["hostip"] - data["host_port"] = query_res.raw["hostport"] - data["map"] = query_res.map - data["plugins"] = query_res.software.plugins - except Exception: # TODO: Check what this actually excepts - pass - - print(json_dumps(data)) + except Exception as e: + if status_res is None: + data["error"] = str(e) + + # construct 'data' dict outside try/except to ensure data processing errors + # are noticed. + data["online"] = bool(status_res or query_res) + if status_res is not None: + data["status"] = dataclasses.asdict(status_res) + + # XXX: hack to fixup MOTD serialisation. should be implemented elsewhere. + assert "motd" in data["status"] + data["status"]["motd"] = {"raw": status_res.motd.to_minecraft()} + + if query_res is not None: + # TODO: QueryResponse is not (yet?) a dataclass + data["query"] = qdata = {} + + qdata["host_ip"] = query_res.raw["hostip"] + qdata["host_port"] = query_res.raw["hostport"] + qdata["map"] = query_res.map + qdata["plugins"] = query_res.software.plugins + qdata["raw"] = query_res.raw + + _json.dump(data, sys.stdout) return 0 From faf208bed7028d0b540d22deac4ace62e2441b23 Mon Sep 17 00:00:00 2001 From: rina Date: Fri, 19 Jul 2024 19:46:45 +1000 Subject: [PATCH 16/36] amendment: add NoNameservers and remove ValueError from squashed errors ValueError might be thrown by programming errors in json handling, for example. --- mcstatus/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index b31be34b..811a6aaf 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dns.resolver import sys import json as _json import argparse @@ -143,11 +144,11 @@ def main(argv: list[str]) -> int: args = parser.parse_args(argv) lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup - server = lookup(args.address) try: + server = lookup(args.address) return args.func(server) - except (socket.timeout, socket.gaierror, ValueError) as e: + except (socket.timeout, socket.gaierror, dns.resolver.NoNameservers) as e: # catch and hide traceback for expected user-facing errors print(f"Error: {e}", file=sys.stderr) return 1 From 890d378fdd53f3d7544a678e46398e8aa9196599 Mon Sep 17 00:00:00 2001 From: rina Date: Sat, 20 Jul 2024 11:51:30 +1000 Subject: [PATCH 17/36] review: fallback logic in CLI ping since this runs both ping() then status(), it can report precisely when one fails and the other succeeds. some kludgy logic to switch bedrock too. --- mcstatus/__main__.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 811a6aaf..e0c73302 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -5,6 +5,7 @@ import json as _json import argparse import socket +import warnings import dataclasses from typing import TYPE_CHECKING @@ -24,10 +25,33 @@ def _motd(motd: Motd) -> str: def ping(server: SupportedServers) -> int: - # this method supports both Java and Bedrock. - # only Java supports the `ping` packet, and even then not always: - # https://github.com/py-mine/mcstatus/issues/850 - print(f"{server.status().latency}") + notsup = "notsup" + + # handle java and bedrock differences, as well as non-conformant servers + # which require a 'status' packet. + + try: + ping_res = server.ping() if isinstance(server, JavaServer) else notsup + except Exception as e: + ping_res = e + + # at this point, ping_res is NOTSUP for Bedrock, otherwise it is either a float or an Exception. + + if isinstance(ping_res, (float, int)): + latency = ping_res + else: + latency = server.status().latency + + if ping_res != notsup: + addr = f"{server.address.host}:{server.address.port}" + warnings.warn( + f"contacting {addr} failed with a 'ping' packet but succeeded with a 'status' packet,\n " + f"this is likely a bug in the server-side implementation.\n " + f"for more details, see: https://mcstatus.readthedocs.io/en/stable/pages/faq/\n", + stacklevel=1, + ) + + print(f"{latency}") return 0 From 80079b3494d0a4b3c50284f71b2ed882e8ccd104 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Sat, 20 Jul 2024 23:49:32 +1000 Subject: [PATCH 18/36] review: use ip/port fields in CLI's JSON output in anticipation of #536 Co-authored-by: Perchun Pak --- mcstatus/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index e0c73302..6e0fa968 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -103,8 +103,8 @@ def json(server: SupportedServers) -> int: # TODO: QueryResponse is not (yet?) a dataclass data["query"] = qdata = {} - qdata["host_ip"] = query_res.raw["hostip"] - qdata["host_port"] = query_res.raw["hostport"] + qdata["ip"] = query_res.raw["hostip"] + qdata["port"] = query_res.raw["hostport"] qdata["map"] = query_res.map qdata["plugins"] = query_res.software.plugins qdata["raw"] = query_res.raw From 05d37a5d667b49a04f3f181bc79a8747916241be Mon Sep 17 00:00:00 2001 From: rina Date: Sat, 20 Jul 2024 23:48:08 +1000 Subject: [PATCH 19/36] review: avoid kind() classmethod --- mcstatus/__main__.py | 13 +++++++++++-- mcstatus/server.py | 14 +------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 6e0fa968..a1089ba7 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -24,6 +24,15 @@ def _motd(motd: Motd) -> str: 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(server: SupportedServers) -> int: notsup = "notsup" @@ -70,7 +79,7 @@ def status(server: SupportedServers) -> int: if player_sample: player_sample = " " + player_sample - print(f"version: {server.kind()} {response.version.name} (protocol {response.version.protocol})") + 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") @@ -78,7 +87,7 @@ def status(server: SupportedServers) -> int: def json(server: SupportedServers) -> int: - data = {"online": False, "kind": server.kind()} + data = {"online": False, "kind": _kind(server)} status_res = query_res = None try: diff --git a/mcstatus/server.py b/mcstatus/server.py index ad912035..7bb57b4c 100644 --- a/mcstatus/server.py +++ b/mcstatus/server.py @@ -1,6 +1,6 @@ from __future__ import annotations -from abc import ABC, abstractmethod +from abc import ABC from typing import TYPE_CHECKING from mcstatus.address import Address, async_minecraft_srv_address_lookup, minecraft_srv_address_lookup @@ -53,20 +53,12 @@ def lookup(cls, address: str, timeout: float = 3) -> Self: addr = Address.parse_address(address, default_port=cls.DEFAULT_PORT) return cls(addr.host, addr.port, timeout=timeout) - @classmethod - @abstractmethod - def kind(cls) -> str: ... - class JavaServer(MCServer): """Base class for a Minecraft Java Edition server.""" DEFAULT_PORT = 25565 - @classmethod - def kind(cls) -> str: - return "Java" - @classmethod def lookup(cls, address: str, timeout: float = 3) -> Self: """Mimics minecraft's server address field. @@ -196,10 +188,6 @@ class BedrockServer(MCServer): DEFAULT_PORT = 19132 - @classmethod - def kind(cls) -> str: - return "Bedrock" - @retry(tries=3) def status(self, **kwargs) -> BedrockStatusResponse: """Checks the status of a Minecraft Bedrock Edition server. From 61559801d0721cf086e8cf54f342b9af85c8a9a8 Mon Sep 17 00:00:00 2001 From: rina Date: Sat, 20 Jul 2024 23:50:46 +1000 Subject: [PATCH 20/36] review: clarify MOTD serialisation comment --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index a1089ba7..26a9b5ba 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -104,7 +104,7 @@ def json(server: SupportedServers) -> int: if status_res is not None: data["status"] = dataclasses.asdict(status_res) - # XXX: hack to fixup MOTD serialisation. should be implemented elsewhere. + # XXX: hack to fixup MOTD serialisation. proper JSON serialisation for the Motd class should be implemented elsewhere. assert "motd" in data["status"] data["status"]["motd"] = {"raw": status_res.motd.to_minecraft()} From b42b1b78776aa0df1462692058d049a82eb28552 Mon Sep 17 00:00:00 2001 From: rina Date: Sat, 20 Jul 2024 23:59:50 +1000 Subject: [PATCH 21/36] review: simplify ping fallback logic Co-authored-by: Perchun Pak --- mcstatus/__main__.py | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 26a9b5ba..77c6c0b7 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -33,34 +33,34 @@ def _kind(serv: SupportedServers) -> str: raise ValueError(f"unsupported server for kind: {serv}") -def ping(server: SupportedServers) -> int: - notsup = "notsup" - - # handle java and bedrock differences, as well as non-conformant servers - # which require a 'status' packet. +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: - ping_res = server.ping() if isinstance(server, JavaServer) else notsup + return server.ping(tries=1) except Exception as e: - ping_res = e + ping_exc = e - # at this point, ping_res is NOTSUP for Bedrock, otherwise it is either a float or an Exception. + latency = server.status().latency - if isinstance(ping_res, (float, int)): - latency = ping_res - else: - latency = server.status().latency - - if ping_res != notsup: - addr = f"{server.address.host}:{server.address.port}" - warnings.warn( - f"contacting {addr} failed with a 'ping' packet but succeeded with a 'status' packet,\n " - f"this is likely a bug in the server-side implementation.\n " - f"for more details, see: https://mcstatus.readthedocs.io/en/stable/pages/faq/\n", - stacklevel=1, - ) - - print(f"{latency}") + address = f"{server.address.host}:{server.address.port}" + warnings.warn( + f"contacting {address} failed with a 'ping' packet but succeeded with a 'status' packet,\n" + f" this is likely a bug in the server-side implementation.\n" + f' (note: ping packet failed due to "{ping_exc}")\n' + f" for more details, see: https://mcstatus.readthedocs.io/en/stable/pages/faq/\n", + stacklevel=1, + ) + + return latency + + +def ping(server: SupportedServers) -> int: + print(f"{_ping_with_fallback(server)}") return 0 From 6ae1dbfd89c0e8fe17cd0a8a1774fd8373af20da Mon Sep 17 00:00:00 2001 From: rina Date: Sun, 21 Jul 2024 10:46:00 +1000 Subject: [PATCH 22/36] make version consistent between status and query --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 77c6c0b7..e55fc596 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -140,7 +140,7 @@ def query(server: SupportedServers) -> int: return 1 print(f"host: {response.raw['hostip']}:{response.raw['hostport']}") - print(f"software: v{response.software.version} {response.software.brand}") + 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.names}") From ceb43ff8b1f7b7d9a99b27d810a2100c2a375980 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Mon, 22 Jul 2024 00:16:59 +1000 Subject: [PATCH 23/36] review: apply simplify() to motd in CLI JSON output Co-authored-by: Perchun Pak --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index e55fc596..117c3290 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -106,7 +106,7 @@ def json(server: SupportedServers) -> int: # XXX: hack to fixup MOTD serialisation. proper JSON serialisation for the Motd class should be implemented elsewhere. assert "motd" in data["status"] - data["status"]["motd"] = {"raw": status_res.motd.to_minecraft()} + data["status"]["motd"] = {"raw": status_res.motd.simplify().to_minecraft()} if query_res is not None: # TODO: QueryResponse is not (yet?) a dataclass From 1799c3d3ebce51fd1a2a7b0af885253abce1d6f3 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Mon, 22 Jul 2024 10:58:10 +1000 Subject: [PATCH 24/36] review: use separate JSON field for simplified MOTD --- mcstatus/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 117c3290..d17568eb 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -106,7 +106,10 @@ def json(server: SupportedServers) -> int: # XXX: hack to fixup MOTD serialisation. proper JSON serialisation for the Motd class should be implemented elsewhere. assert "motd" in data["status"] - data["status"]["motd"] = {"raw": status_res.motd.simplify().to_minecraft()} + data["status"]["motd"] = { + "raw": status_res.motd.to_minecraft(), + "simplified": status_res.motd.simplify().to_minecraft(), + } if query_res is not None: # TODO: QueryResponse is not (yet?) a dataclass From 50b825e5a63ca0386f49b2804e120d08692b15b9 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Mon, 22 Jul 2024 10:58:36 +1000 Subject: [PATCH 25/36] review: remove MOTD fixup comment --- mcstatus/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index d17568eb..90709e2b 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -104,7 +104,6 @@ def json(server: SupportedServers) -> int: if status_res is not None: data["status"] = dataclasses.asdict(status_res) - # XXX: hack to fixup MOTD serialisation. proper JSON serialisation for the Motd class should be implemented elsewhere. assert "motd" in data["status"] data["status"]["motd"] = { "raw": status_res.motd.to_minecraft(), From c23d40dcfa9ddf69cf03f66df5c9159634e4605c Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Tue, 23 Jul 2024 08:47:04 +1000 Subject: [PATCH 26/36] review: update README with new CLI --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f988433b..ee4a52f8 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,33 @@ See the [documentation](https://mcstatus.readthedocs.io) to find what you can do ### Command Line Interface -This only works with Java servers; Bedrock is not yet supported. Use `mcstatus -h` to see helpful information on how to use this script. +The mcstatus library includes a simple CLI. Once installed, it can be used through: +```console +$ python3 -m mcstatus --help +usage: mcstatus [-h] [--bedrock] address {ping,status,query,json} ... + +mcstatus provides an easy way to query Minecraft servers for any information they can +expose. It provides three modes of access: query, status, ping and json. + +positional arguments: + address The address of the server. + +options: + -h, --help show this help message and exit + --bedrock Specifies that 'address' is a Bedrock server (default: Java). + +commands: + Command to run, defaults to 'status'. + + {ping,status,query,json} + ping Ping server for latency. + status Prints server status. Supported by all Minecraft servers that + are version 1.7 or higher. + query Prints detailed server information. Must be enabled in servers' + server.properties file. + json Prints server status and query in json. Supported by all + Minecraft servers that are version 1.7 or higher. +``` ## License From 4ad7f6cf197a6ac7bb2ae08d77da269a60f46a79 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Wed, 24 Jul 2024 08:58:21 +1000 Subject: [PATCH 27/36] review: no raw motd --- mcstatus/__main__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 90709e2b..a40b75c5 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -105,10 +105,7 @@ def json(server: SupportedServers) -> int: data["status"] = dataclasses.asdict(status_res) assert "motd" in data["status"] - data["status"]["motd"] = { - "raw": status_res.motd.to_minecraft(), - "simplified": status_res.motd.simplify().to_minecraft(), - } + data["status"]["motd"] = status_res.motd.simplify().to_minecraft() if query_res is not None: # TODO: QueryResponse is not (yet?) a dataclass From 8d2967317a9ed944bb5f62c768b83bd2d900bfa9 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Wed, 24 Jul 2024 17:17:58 +1000 Subject: [PATCH 28/36] no --help output in readme --- README.md | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ee4a52f8..e7864ddb 100644 --- a/README.md +++ b/README.md @@ -68,31 +68,8 @@ See the [documentation](https://mcstatus.readthedocs.io) to find what you can do ### Command Line Interface The mcstatus library includes a simple CLI. Once installed, it can be used through: -```console -$ python3 -m mcstatus --help -usage: mcstatus [-h] [--bedrock] address {ping,status,query,json} ... - -mcstatus provides an easy way to query Minecraft servers for any information they can -expose. It provides three modes of access: query, status, ping and json. - -positional arguments: - address The address of the server. - -options: - -h, --help show this help message and exit - --bedrock Specifies that 'address' is a Bedrock server (default: Java). - -commands: - Command to run, defaults to 'status'. - - {ping,status,query,json} - ping Ping server for latency. - status Prints server status. Supported by all Minecraft servers that - are version 1.7 or higher. - query Prints detailed server information. Must be enabled in servers' - server.properties file. - json Prints server status and query in json. Supported by all - Minecraft servers that are version 1.7 or higher. +```bash +python3 -m mcstatus --help ``` ## License From 06e8b806b75fa0d6d89f9b400f4706919cee5938 Mon Sep 17 00:00:00 2001 From: rina Date: Thu, 25 Jul 2024 10:04:46 +1000 Subject: [PATCH 29/36] review: allow main() with no arguments --- mcstatus/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index a40b75c5..1094f5f5 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -146,7 +146,7 @@ def query(server: SupportedServers) -> int: return 0 -def main(argv: list[str]) -> int: +def main(argv: list[str] = sys.argv[1:]) -> int: parser = argparse.ArgumentParser( "mcstatus", description=""" @@ -187,4 +187,4 @@ def main(argv: list[str]) -> int: if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) + sys.exit(main()) From f82ae9e462e1a053973b5658a5a33f8cca361295 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Thu, 25 Jul 2024 22:41:50 +1000 Subject: [PATCH 30/36] Update mcstatus/__main__.py Co-authored-by: Kevin Tindall --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 1094f5f5..07cb447d 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -60,7 +60,7 @@ def _ping_with_fallback(server: SupportedServers) -> float: def ping(server: SupportedServers) -> int: - print(f"{_ping_with_fallback(server)}") + print(_ping_with_fallback(server)) return 0 From 80fb48b621f45d9862e15fd01ed9a52659842305 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Thu, 25 Jul 2024 22:50:07 +1000 Subject: [PATCH 31/36] avoid json collision --- mcstatus/__main__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 07cb447d..7eb10e76 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -2,7 +2,7 @@ import dns.resolver import sys -import json as _json +import json import argparse import socket import warnings @@ -59,12 +59,12 @@ def _ping_with_fallback(server: SupportedServers) -> float: return latency -def ping(server: SupportedServers) -> int: +def ping_cmd(server: SupportedServers) -> int: print(_ping_with_fallback(server)) return 0 -def status(server: SupportedServers) -> int: +def status_cmd(server: SupportedServers) -> int: response = server.status() java_res = response if isinstance(response, JavaStatusResponse) else None @@ -86,7 +86,7 @@ def status(server: SupportedServers) -> int: return 0 -def json(server: SupportedServers) -> int: +def json_cmd(server: SupportedServers) -> int: data = {"online": False, "kind": _kind(server)} status_res = query_res = None @@ -117,11 +117,11 @@ def json(server: SupportedServers) -> int: qdata["plugins"] = query_res.software.plugins qdata["raw"] = query_res.raw - _json.dump(data, sys.stdout) + json.dump(data, sys.stdout) return 0 -def query(server: SupportedServers) -> int: +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 @@ -160,19 +160,19 @@ def main(argv: list[str] = sys.argv[1:]) -> int: 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) + parser.set_defaults(func=status_cmd) subparsers.add_parser("ping", help="Ping server for latency.").set_defaults(func=ping) subparsers.add_parser( "status", help="Prints server status. Supported by all Minecraft servers that are version 1.7 or higher." - ).set_defaults(func=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) + ).set_defaults(func=query_cmd) subparsers.add_parser( "json", help="Prints server status and query in json. Supported by all Minecraft servers that are version 1.7 or higher.", - ).set_defaults(func=json) + ).set_defaults(func=json_cmd) args = parser.parse_args(argv) lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup From f4fdabd49758db774eca76530e0e4402c63a6a7a Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Thu, 25 Jul 2024 22:52:44 +1000 Subject: [PATCH 32/36] oops! good linter --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 7eb10e76..bfcdab0d 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -162,7 +162,7 @@ def main(argv: list[str] = sys.argv[1:]) -> int: 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) + subparsers.add_parser("ping", help="Ping server for latency.").set_defaults(func=ping_cmd) subparsers.add_parser( "status", help="Prints server status. Supported by all Minecraft servers that are version 1.7 or higher." ).set_defaults(func=status_cmd) From 25400892e13b4a1084c7ae2a4e58051fe6b0e2b2 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Fri, 26 Jul 2024 07:39:14 +1000 Subject: [PATCH 33/36] drike review --- mcstatus/__main__.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index bfcdab0d..f850c37f 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -48,12 +48,12 @@ def _ping_with_fallback(server: SupportedServers) -> float: latency = server.status().latency address = f"{server.address.host}:{server.address.port}" - warnings.warn( - f"contacting {address} failed with a 'ping' packet but succeeded with a 'status' packet,\n" - f" this is likely a bug in the server-side implementation.\n" - f' (note: ping packet failed due to "{ping_exc}")\n' - f" for more details, see: https://mcstatus.readthedocs.io/en/stable/pages/faq/\n", - stacklevel=1, + print( + f"warning: contacting {address} failed with a 'ping' packet but succeeded with a 'status' packet,\n" + f" this is likely a bug in the server-side implementation.\n" + f' (note: ping packet failed due to "{ping_exc}")\n' + f" for more details, see: https://mcstatus.readthedocs.io/en/stable/pages/faq/\n", + file=sys.stderr, ) return latency @@ -89,22 +89,30 @@ def status_cmd(server: SupportedServers) -> int: def json_cmd(server: SupportedServers) -> int: data = {"online": False, "kind": _kind(server)} - status_res = query_res = None + 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: - if status_res is None: - data["error"] = str(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) - assert "motd" in data["status"] + # 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: From 551caa4f0941eb41d18d50240bafd798a2767467 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Fri, 26 Jul 2024 07:41:36 +1000 Subject: [PATCH 34/36] good linter --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index f850c37f..e6498f61 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -100,7 +100,7 @@ def json_cmd(server: SupportedServers) -> int: 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) From 9189de7fff2ffaf812c5a665226bf0a3b9ed9103 Mon Sep 17 00:00:00 2001 From: Kait Lam Date: Fri, 26 Jul 2024 07:43:22 +1000 Subject: [PATCH 35/36] one more ci failure and i turn on the computer --- mcstatus/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index e6498f61..4dbbdb00 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -5,7 +5,6 @@ import json import argparse import socket -import warnings import dataclasses from typing import TYPE_CHECKING From d6907356b1b88cbf29e14ad574c209561d383d22 Mon Sep 17 00:00:00 2001 From: rina Date: Sat, 27 Jul 2024 11:10:27 +1000 Subject: [PATCH 36/36] also squash ConnectionError happens during server startup, for example --- mcstatus/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcstatus/__main__.py b/mcstatus/__main__.py index 4dbbdb00..1b4e5a5c 100644 --- a/mcstatus/__main__.py +++ b/mcstatus/__main__.py @@ -187,7 +187,7 @@ def main(argv: list[str] = sys.argv[1:]) -> int: try: server = lookup(args.address) return args.func(server) - except (socket.timeout, socket.gaierror, dns.resolver.NoNameservers) as e: + except (socket.timeout, socket.gaierror, dns.resolver.NoNameservers, ConnectionError) as e: # catch and hide traceback for expected user-facing errors print(f"Error: {e}", file=sys.stderr) return 1