59 lines
2.8 KiB
Python
59 lines
2.8 KiB
Python
import argparse
|
|
import readline
|
|
from importlib.util import spec_from_file_location, module_from_spec
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
prog="Minecraft RCON Client",
|
|
description="A simple RCON client for remotely controlling a Minecraft server. Can be used interactively or to run prespecified commands. Also supports running arbitrary Python scripts for more complex logic.")
|
|
|
|
parser.add_argument("-H", "--host", help="The hostname of the RCON server.")
|
|
parser.add_argument("-P", "--port", help="The port number of the RCON server. Default is 25575 if omitted and -H is specified. ", type=int)
|
|
parser.add_argument("-p", "--password", help="The password to the RCON server.")
|
|
|
|
execute_arg_group = parser.add_mutually_exclusive_group()
|
|
execute_arg_group.add_argument("-e", "--execute-command", help="Send a single command on the RCON server. Can be passed multiple times to run multiple commands. Does not abort on server-side command failures. Requires -H and -p to be specified. Mutually exclusive with -E.", metavar="COMMAND", action='append')
|
|
execute_arg_group.add_argument("-E", "--execute-file", help="Execute a Python file. Requires -H and -p to be specified. Mutually exclusive with -e.", metavar="FILEPATH")
|
|
|
|
parser.add_argument("--echo", help="Echo a command before sending it. Does nothing when neither -e nor -E is present.", action="store_true")
|
|
parser.add_argument("--socket-timeout", help="Set the timeout for the socket when waiting to receive data. Default 30 seconds.", type=float, metavar="TIMEOUT_SECONDS")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if (args.execute_command or args.execute_file) and not (args.host and args.password):
|
|
parser.error("-e/-E require -H and -p to be specified")
|
|
|
|
return args
|
|
|
|
|
|
def safe_input(prompt: str) -> str:
|
|
"""Like `input()`, but Ctrl-C prints a fresh prompt and Ctrl-D cleanly exits"""
|
|
while True:
|
|
try:
|
|
return input(prompt)
|
|
except KeyboardInterrupt:
|
|
print('^C')
|
|
except EOFError:
|
|
print()
|
|
exit(0)
|
|
|
|
|
|
def load_script(script_path: str):
|
|
# three stages to importing here: find the thing to import, initialize an empty module, and execute the top-level code in that module
|
|
# here, we're essentially deciding which tool to use to read the script
|
|
spec = spec_from_file_location("user_script", script_path)
|
|
if spec is None:
|
|
raise ValueError(f"{script_path} is not a valid Python file path")
|
|
|
|
# initialize an empty "box" that we'll populate with all of our code
|
|
module = module_from_spec(spec)
|
|
# execute the top-level code (function definitions, global variables, etc.)
|
|
spec.loader.exec_module(module) # pyright: ignore
|
|
|
|
if not hasattr(module, "run"):
|
|
raise ValueError(f"Provided script {script_path} must define a run(sock) function")
|
|
|
|
return module
|
|
|