#!/usr/bin/env python3 import argparse import json import logging import requests import shutil import tempfile import time from subprocess import Popen, PIPE, TimeoutExpired from typing import List, Optional, Tuple parser = argparse.ArgumentParser() parser.add_argument("--token", type=str, required=True, help="API token.") parser.add_argument("--program", type=str, required=True, help="Program to run.") parser.add_argument("--server", type=str, default="http://localhost:5000", help="Server address.") parser.add_argument("--game", type=str, default="main", help="'main' or 'test_#'.") parser.add_argument("--copy", default=False, action="store_true", help="Save a copy of the program to ensure the same code is used all the time.") parser.add_argument("--log-level", type=str, default="warning", choices=["error", "warning", "info"]) parser.add_argument("--save-state", type=str, default=None, help="Save state to this file.") # parser.add_argument("program", nargs=argparse.REMAINDER, help="Program to run.") TIME_BEFORE_RETRY = 2.0 logging.basicConfig(level=logging.INFO) logger = logging.getLogger("client") def main(args: argparse.Namespace) -> None: logger.setLevel(args.log_level.upper()) program = args.program if args.copy: program = tempfile.mktemp() shutil.copyfile(args.program, program) program = "./" + program min_round = 0 while True: state, wait_time = get_state(min_round, args) if state is None: # retry later time.sleep(wait_time) continue round = state["round"] min_round = round + 1 # if requested, dump state to file instead if args.save_state is not None: with open(args.save_state, "w") as f: json.dump(state, f) return turn_json = run_subprocess( program, json.dumps(state), state["time_to_response"] ) if turn_json is None: continue try: turn = json.loads(turn_json) except json.JSONDecodeError: logger.error(f"Invalid JSON returned by strategy code.") continue while not send_turn(turn, round, args): # if there was a connection error, retry later time.sleep(TIME_BEFORE_RETRY) def run_subprocess( program: str, state_json: str, timeout: Optional[float] ) -> Optional[str]: """Run user program and return its output.""" proc = Popen([program], encoding="utf-8", stdin=PIPE, stdout=PIPE, stderr=PIPE) try: stdout, stderr = proc.communicate(input=state_json, timeout=timeout) except TimeoutExpired: proc.kill() logger.error("Strategy code took too long.") return None if stderr: logger.error(f"Strategy code stderr:\n{stderr}") if proc.returncode != 0: logger.error("Strategy code exited with non-zero exit code.") return None return stdout def get_state(min_round: int, args) -> Tuple[Optional[dict], float]: """Get game data from the server. Returns None and wait time if there was an error.""" try: r = requests.get(f"{args.server}/api/state", params={ "game": args.game, "token": args.token, "min_round": min_round }) # retry later if there was an error except requests.exceptions.RequestException as e: logger.warning(f"Request error: {e}") return None, TIME_BEFORE_RETRY if not r.ok: logger.warning(f"Server error: {r.status_code} {r.reason}") return None, TIME_BEFORE_RETRY state = r.json() # also retry if the server is not willing to give us the state yet if state["status"] == "waiting": logger.info("Server is busy.") return None, state["wait"] if state["status"] == "too_early": logger.info("Round didn't start yet.") return None, state["wait"] logger.info("Received new state.") return state, 0 def send_turn(turn: dict, round: int, args) -> bool: """Send turn to the server. Returns True if the server received the request.""" try: r = requests.post( f"{args.server}/api/action", params={ "game": args.game, "token": args.token, "round": round }, json=turn ) except requests.exceptions.RequestException as e: logger.warning(f"Request error: {e}") return False if not r.ok: logger.warning(f"Server error: {r.status_code} {r.reason}") return False # print errors # because such errors are caused by the submitted turn, # retrying will not help, so return True response = r.json() if response["status"] == "ok": logger.info("Turn accepted.") elif response["status"] == "too_late": logger.error("Turn submitted too late.") elif response["status"] == "error": logger.error(f"Turn error: {response['description']}") elif response["status"] == "warning": member_warns = [ f" {member['id']}: {member['description']}" for member in response["members"] ] logger.warn("Member warnings:\n" + "\n".join(member_warns)) return True if __name__ == "__main__": args = parser.parse_args() main(args)