async def receive_handler() -> AsyncGenerator[str, None]: while True: message: aiodocker.stream.Message = await cmd_stream.read_out() if message is None: break logger.debug(f"Container {container.id} --> '{message}'") yield bytes(message.data).decode()
async def run(gamemode: Gamemode, submission_hashes=None, options=None, turns=2 << 32, connections=None) -> ParsedResult: if submission_hashes is None: submission_hashes = [] submission_hashes = list(submission_hashes) if options is None: options = dict() options = {**gamemode.options, **options} if connections is None: connections = [] connections: List[Connection] turns = int(turns) logger.debug(f"Request for gamemode {gamemode.name}") if len(connections) + len(submission_hashes) != gamemode.player_count: raise RuntimeError("Invalid number of players total") # Create containers and recurse if len(submission_hashes) != 0: socket_awaitables = [_make_container_connection(gamemode, sub_hash) for sub_hash in submission_hashes] async with with_multiple(*socket_awaitables) as new_connections: for connection in new_connections: if isinstance(connection, ParsedResult): return connection if not isinstance(connection, Connection): raise RuntimeError(f"Unknown connection type: {connection}") connections.append(connection) return await run(gamemode, [], options, turns, connections) # Wrap all containers in timeouts timeout = (gamemode.player_count + 1) * int(options.get("turn_time", 10)) connections = [TimedConnection(connection, timeout) for connection in connections] # Set up linking through middleware logger.debug("Attaching middleware: ") middleware = Middleware(connections) # Run logger.debug("Running...") outcomes, result, moves, initial_board = await _run_loop(gamemode, middleware, options, turns) # Gather logger.debug("Completed game, shutting down containers...") await middleware.complete_all() prints = [] for i in range(gamemode.player_count): prints.append(middleware.get_player_prints(i)) results = [SingleResult(outcome, result == Result.ValidGame, name, result, prints) for outcome, name, prints in zip(outcomes, gamemode.players, prints)] parsed_result = ParsedResult(initial_board, moves, results) logger.debug(f"Done running gamemode {gamemode.name}! Result: {parsed_result}") return parsed_result
async def _timed(self, task: Coroutine): start = time.time() try: logger.debug( f"Connection {self._connection}: Time remaining: {self._time_remaining}, running {task}" ) yield await wait_for(task, self._time_remaining) except asyncio.TimeoutError: logger.debug(f"Connection {self._connection}: Timeout") while True: raise ConnectionTimedOutError() end = time.time() self._time_remaining -= (end - start)
async def _exec_root(container: aiodocker.docker.DockerContainer, command: str): logger.debug(f"Container {container.id}: running {command}") exec_ctxt = await container.exec(command, user='******', tty=True, stdout=True) exec_stream: Stream = exec_ctxt.start(timeout=30) output = b'' while True: message: aiodocker.stream.Message = await exec_stream.read_out() if message is None: break output += message.data logger.debug( f"Container {container.id}: result of command {command}: '{output.decode()}'" )
async def _copy_submission(container: aiodocker.docker.DockerContainer, submission_hash: str): submission_path = f"/home/subrunner/repositories/{submission_hash}.tar" # Ensure that submission is valid if not _is_submission_valid(submission_hash, submission_path): raise InvalidSubmissionError(submission_hash) # Make destination dest_path = "/home/sandbox/submission" await _exec_root(container, f"mkdir {dest_path}") # Make required init file for python init_path = os.path.join(dest_path, "__init__.py") await _exec_root(container, f"touch {init_path}") logger.debug( f"Container {container.id}: opening submission {submission_hash}") with open(submission_path, 'rb') as f: logger.debug( f"Container {container.id}: reading submission {submission_hash}") data = f.read() logger.debug( f"Container {container.id}: putting submission {submission_hash}") await container.put_archive(dest_path, data)
async def send_handler(m: str): logger.debug(f"Container {container.id} <-- '{m.encode()}'") await cmd_stream.write_in((m + "\n").encode())
async def run(submission_hash: str) -> AsyncIterator[Connection]: docker = None container = None try: # Attach to docker docker = aiodocker.Docker() # Create container logger.debug(f"Creating container for hash {submission_hash}") env_vars = _get_env_vars() try: container = await _make_sandbox_container(docker, env_vars) except DockerError: logger.error(traceback.format_exc()) raise # Copy information logger.debug(f"Container {container.id}: copying scripts") await _copy_sandbox_scripts(container) logger.debug(f"Container {container.id}: copying submission") await _copy_submission(container, submission_hash) logger.debug(f"Container {container.id}: locking down") await _lock_down(container) # Start script logger.debug(f"Container {container.id}: running script") run_t = int( config_file.get('submission_runner.sandbox_run_timeout_seconds')) run_script_cmd = f"./sandbox/run.sh 'play.py' {run_t}" cmd_exec = await container.exec(cmd=run_script_cmd, user='******', stdin=True, stdout=True, stderr=True, tty=False, environment=env_vars, workdir="/home/sandbox/") unrun_t = int( config_file.get('submission_runner.sandbox_unrun_timeout_seconds')) cmd_stream: Stream = cmd_exec.start(timeout=unrun_t) # Set up input to the container async def send_handler(m: str): logger.debug(f"Container {container.id} <-- '{m.encode()}'") await cmd_stream.write_in((m + "\n").encode()) # Set up output from the container async def receive_handler() -> AsyncGenerator[str, None]: while True: message: aiodocker.stream.Message = await cmd_stream.read_out() if message is None: break logger.debug(f"Container {container.id} --> '{message}'") yield bytes(message.data).decode() # Process output from the container logger.debug(f"Container {container.id}: setting up output processing") lines = _get_lines(receive_handler()) logger.debug(f"Container {container.id}: connecting") yield MessagePrintConnection(send_handler, lines, container.id) finally: # Clean everything up if container is not None: logger.debug(f"Container {container.id}: cleaning up") await container.delete(force=True) if docker is not None: await docker.close()
async def _run_loop(gamemode: Gamemode, middleware, options, turns) -> Tuple[List[Outcome], Result, List[str], str]: moves = [] time_remaining = [int(options["turn_time"])] * gamemode.player_count board = gamemode.setup(**options) initial_encoded_board = gamemode.encode_board(board) player_turn = 0 try: # Calculate latencies pings = 5 latency = [] for i in range(gamemode.player_count): tot = 0.0 for _ in range(pings): tot += await middleware.ping(i) tot /= pings tot = min(tot, 0.2) # Cap latency at 0.2s to prevent slow loris attack latency.append(tot) except (ConnectionNotActiveError, ConnectionTimedOutError): # Shouldn't crash, it's our fault if it does :( return [Outcome.Draw] * gamemode.player_count, Result.UnknownResultType, moves, initial_encoded_board latency = sum(latency) / len(latency) logger.debug(f"Latency for container communication: {latency}s") def make_win(winner): res = [Outcome.Loss] * gamemode.player_count res[winner] = Outcome.Win return res def make_loss(loser): res = [Outcome.Win] * gamemode.player_count res[loser] = Outcome.Loss return res for _ in range(turns): start_time = time.time_ns() try: move = await middleware.call(player_turn, "make_move", board=gamemode.filter_board(board, player_turn), time_remaining=time_remaining[player_turn]) except ConnectionNotActiveError: return make_loss(player_turn), Result.ProcessKilled, moves, initial_encoded_board except ConnectionTimedOutError: return make_loss(player_turn), Result.Timeout, moves, initial_encoded_board end_time = time.time_ns() t = (end_time - start_time) / 1e9 t -= latency time_remaining[player_turn] -= t if time_remaining[player_turn] <= 0: return make_loss(player_turn), Result.Timeout, moves, initial_encoded_board if isinstance(move, MissingFunctionError): return make_loss(player_turn), Result.BrokenEntryPoint, moves, initial_encoded_board if isinstance(move, ExceptionTraceback): return make_loss(player_turn), Result.Exception, moves, initial_encoded_board move = gamemode.parse_move(move) logger.debug(f"Got move {move}") if not gamemode.is_move_legal(board, move): logger.debug(f"Move is not legal {move}") return make_loss(player_turn), Result.IllegalMove, moves, initial_encoded_board moves.append(gamemode.encode_move(move, player_turn)) board = gamemode.apply_move(board, move) if gamemode.is_win(board, player_turn): return make_win(player_turn), Result.ValidGame, moves, initial_encoded_board if gamemode.is_loss(board, player_turn): return make_loss(player_turn), Result.ValidGame, moves, initial_encoded_board if gamemode.is_draw(board, player_turn): return [Outcome.Draw] * gamemode.player_count, Result.ValidGame, moves, initial_encoded_board player_turn += 1 player_turn %= gamemode.player_count return [Outcome.Draw] * gamemode.player_count, Result.GameUnfinished, moves, initial_encoded_board