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
Exemple #3
0
 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