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