diff --git a/klient/client.py b/klient/client.py index 820db5f..35b028e 100644 --- a/klient/client.py +++ b/klient/client.py @@ -4,6 +4,7 @@ import json import logging import requests import shutil +import sys import tempfile import time from subprocess import Popen, PIPE, TimeoutExpired @@ -36,12 +37,7 @@ def main(args: argparse.Namespace) -> None: min_round = 0 while True: - state, wait_time = get_state(min_round, args) - if state is None: - # retry later - time.sleep(wait_time) - continue - + state = get_state(min_round, args) round = state["round"] min_round = round + 1 # if requested, dump state to file instead @@ -59,16 +55,14 @@ def main(args: argparse.Namespace) -> None: turn = json.loads(turn_json) except json.JSONDecodeError: logger.error(f"Invalid JSON returned by strategy code.") - continue + sys.exit(1) - while not send_turn(turn, round, args): - # if there was a connection error, retry later - time.sleep(TIME_BEFORE_RETRY) + send_turn(turn, round, args) def run_subprocess( - program: str, state_json: str, timeout: Optional[float] - ) -> Optional[str]: + program: str, state_json: str, timeout: Optional[float] +) -> Optional[str]: """Run user program and return its output.""" proc = Popen([program], encoding="utf-8", @@ -83,82 +77,104 @@ def run_subprocess( logger.error(f"Strategy code stderr:\n{stderr}") if proc.returncode != 0: logger.error("Strategy code exited with non-zero exit code.") - return None + sys.exit(1) return stdout -def get_state(min_round: int, args) -> Tuple[Optional[dict], float]: - """Get game data from the server. +def get_state(min_round: int, args) -> dict: + """Iteratively try to get game data from the server.""" - Returns None and wait time if there was an error.""" + state, wait_time = get_state_once() + while state is None: + time.sleep(wait_time) + state, wait_time = get_state_once() - 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.") + def get_state_once() -> Tuple[Optional[dict], float]: + try: + res = requests.get(f"{args.server}/api/state", params={ + "game": args.game, + "token": args.token, + "min_round": min_round + }) + except requests.exceptions.RequestException as e: + # server might be down, retry later + logger.warning(f"Request error: {e}") + return None, TIME_BEFORE_RETRY + if not res.ok: + logger.error(f"Server error: {res.status_code} {res.reason}") + log_server_error(res) + sys.exit(1) + + state = res.json() + if state["status"] == "ok": + return state, 0 + + # retry after some time + if state["status"] == "waiting": + logger.info("Server is busy.") + 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. +def send_turn(turn: dict, round: int, args) -> None: + """Iteratively try to send turn to the server.""" - Returns True if the server received the request.""" + while not send_turn_once(): + time.sleep(TIME_BEFORE_RETRY) - 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 + def send_turn_once() -> bool: + """Returns True if server answered.""" + + try: + res = requests.post( + f"{args.server}/api/action", + params={ + "game": args.game, + "token": args.token, + "round": round + }, + json=turn + ) + except requests.exceptions.RequestException as e: + # server might be down, retry later + logger.warning(f"Request error: {e}") + return False + if not res.ok: + log_server_error(res) + sys.exit(1) + + # print errors + # because such errors are caused by the submitted turn, + # retrying will not help, so return True + response = res.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.warning( + "Member warnings:\n" + "\n".join(member_warns) + ) + + return True + + +def log_server_error(res: requests.Response) -> None: + logger.error( + f"Server error: {res.status_code} {res.reason}\n" + f"{res['description']}" + ) if __name__ == "__main__":