Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Bedrock support and misc improvements #849

Merged
merged 36 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2ee93a5
fix crash with one command-line argument
katrinafyi Jul 15, 2024
ec1a0f5
implement ping() on BedrockServer
katrinafyi Jul 15, 2024
60e3512
support Bedrock servers in CLI
katrinafyi Jul 15, 2024
a0e41a2
print server kind and tweak player sample printing
katrinafyi Jul 15, 2024
47ce944
JavaServer ping() doesn't work?
katrinafyi Jul 16, 2024
213fc2a
fix precommit warnings
katrinafyi Jul 17, 2024
848fc57
review: remove Bedrock ping()
katrinafyi Jul 17, 2024
f9577b5
review: change CLI ping comment to be more permanent
katrinafyi Jul 17, 2024
3a0ee8c
review: formalise hostip/hostport within QueryResponse
katrinafyi Jul 17, 2024
73a4543
review: only squash traceback in common errors
katrinafyi Jul 17, 2024
47f62fc
review: leading line break for multi-line motd
katrinafyi Jul 19, 2024
1130b99
Revert "review: formalise hostip/hostport within QueryResponse"
katrinafyi Jul 19, 2024
bb51ac5
review: use motd.to_minecraft() in json
katrinafyi Jul 19, 2024
b6a28fa
review amendment: factor out motd line breaking
katrinafyi Jul 19, 2024
de8be4d
review: refactor CLI json() to use dataclasses.asdict()
katrinafyi Jul 20, 2024
faf208b
amendment: add NoNameservers and remove ValueError from squashed errors
katrinafyi Jul 19, 2024
890d378
review: fallback logic in CLI ping
katrinafyi Jul 20, 2024
80079b3
review: use ip/port fields in CLI's JSON output
katrinafyi Jul 20, 2024
05d37a5
review: avoid kind() classmethod
katrinafyi Jul 20, 2024
6155980
review: clarify MOTD serialisation comment
katrinafyi Jul 20, 2024
b42b1b7
review: simplify ping fallback logic
katrinafyi Jul 20, 2024
6ae1dbf
make version consistent between status and query
katrinafyi Jul 21, 2024
ceb43ff
review: apply simplify() to motd in CLI JSON output
katrinafyi Jul 21, 2024
1799c3d
review: use separate JSON field for simplified MOTD
katrinafyi Jul 22, 2024
50b825e
review: remove MOTD fixup comment
katrinafyi Jul 22, 2024
c23d40d
review: update README with new CLI
katrinafyi Jul 22, 2024
4ad7f6c
review: no raw motd
katrinafyi Jul 23, 2024
8d29673
no --help output in readme
katrinafyi Jul 24, 2024
06e8b80
review: allow main() with no arguments
katrinafyi Jul 25, 2024
f82ae9e
Update mcstatus/__main__.py
katrinafyi Jul 25, 2024
80fb48b
avoid json collision
katrinafyi Jul 25, 2024
f4fdabd
oops! good linter
katrinafyi Jul 25, 2024
2540089
drike review
katrinafyi Jul 25, 2024
551caa4
good linter
katrinafyi Jul 25, 2024
9189de7
one more ci failure and i turn on the computer
katrinafyi Jul 25, 2024
d690735
also squash ConnectionError
katrinafyi Jul 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 69 additions & 28 deletions mcstatus/__main__.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,107 @@
from __future__ import annotations
PerchunPak marked this conversation as resolved.
Show resolved Hide resolved

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:
# 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

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 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})")
print(f'motd: "{response.motd}"')
print(f"players: {response.players.online}/{response.players.max} {player_sample}")
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()}")
PerchunPak marked this conversation as resolved.
Show resolved Hide resolved
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
data["kind"] = server.kind()
# 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
PerchunPak marked this conversation as resolved.
Show resolved Hide resolved
data["player_count"] = status_res.players.online
data["player_max"] = status_res.players.max
PerchunPak marked this conversation as resolved.
Show resolved Hide resolved
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.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
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:
print(
"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
print(f"host: {response.raw['hostip']}:{response.raw['hostport']}")
return 1

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}"')
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="""
Expand All @@ -80,8 +112,11 @@ def main() -> None:
)

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)

subparsers = parser.add_subparsers()
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."
Expand All @@ -94,11 +129,17 @@ 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 = parser.parse_args(argv)
lookup = JavaServer.lookup if not args.bedrock else BedrockServer.lookup
server = lookup(args.address)

args.func(server)
try:
return args.func(server)
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


if __name__ == "__main__":
main()
sys.exit(main(sys.argv[1:]))
8 changes: 6 additions & 2 deletions mcstatus/querier.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def __init__(self, version: str, plugins: str):
map: str
players: Players
software: Software
hostip: str
hostport: int
PerchunPak marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, raw: dict[str, str], players: list[str]):
try:
Expand All @@ -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:
Expand Down
14 changes: 13 additions & 1 deletion mcstatus/server.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -53,12 +53,20 @@ 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: ...
PerchunPak marked this conversation as resolved.
Show resolved Hide resolved


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.
Expand Down Expand Up @@ -188,6 +196,10 @@ 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.
Expand Down
Loading