diff --git a/battlecode-manager/battlecode_cli.py b/battlecode-manager/battlecode_cli.py index 7fc2239..a7bce97 100644 --- a/battlecode-manager/battlecode_cli.py +++ b/battlecode-manager/battlecode_cli.py @@ -12,6 +12,8 @@ from player_sandboxed import SandboxedPlayer import server import battlecode as bc +import threading + try: import ujson as json except: @@ -148,9 +150,12 @@ def cleanup(dockers, args, sock_file): ''' Clean up that needs to be done at the end of a game ''' - for player_key in dockers: - docker_inst = dockers[player_key] - docker_inst.destroy() + threads = [threading.Thread(target=dockers[key].destroy) for key in dockers] + for t in threads: + t.start() + + for t in threads: + t.join() if isinstance(sock_file, str) or isinstance(sock_file, bytes): # only unlink unix sockets diff --git a/battlecode-manager/player_abstract.py b/battlecode-manager/player_abstract.py index 30c90de..1a6e850 100644 --- a/battlecode-manager/player_abstract.py +++ b/battlecode-manager/player_abstract.py @@ -81,6 +81,9 @@ def _detect_platform(self): else: raise Exception("Unknown os: " + sys.platform) + def refreshProcessChildren(self): + pass + def guess_language(self): return "?" diff --git a/battlecode-manager/player_plain.py b/battlecode-manager/player_plain.py index 9b71778..e3f5bed 100644 --- a/battlecode-manager/player_plain.py +++ b/battlecode-manager/player_plain.py @@ -16,9 +16,15 @@ def __init__(self, socket_file, working_dir, local_dir=None, self.paused = False self.streaming = False self.process = None + self.recursiveProcs = [] + self.nonRecursiveProcs = [] super().__init__(socket_file, working_dir, local_dir, None, None, player_key, player_mem_limit, player_cpu) + def refreshProcessChildren(self): + self.recursiveProcs = self.process.children(recursive=True) + self.nonRecursiveProcs = self.process.children(recursive=False) + def stream_logs(self, stdout=True, stderr=True, line_action=lambda line: print(line.decode())): assert not self.streaming self.streaming = True @@ -72,16 +78,23 @@ def guess_language(self): def pause(self): # pausing too slow on windows - if sys.platform == 'win32': return + if sys.platform == 'win32': + return + if not self.paused: self.paused = True - suspend(self.process) + suspend(self.nonRecursiveProcs) def unpause(self, timeout=None): # pausing too slow on windows - if sys.platform == 'win32': return + if sys.platform == 'win32': + return + + # This assert should ideally be tested for Java and Python too, but I think it should hold unless the player goes out of its way to defeat the pausing mechanism + # assert(len(self.recursiveProcs) == len(self.process.children(recursive=True))) + if self.paused: - resume(self.process) + resume(self.recursiveProcs) self.paused = False def destroy(self): @@ -121,9 +134,7 @@ def on_terminate(proc): except: print("Killing failed; assuming process exited early.") -def suspend(process): - - procs = process.children(recursive=False) +def suspend(procs): # to enterprising players reading this code: # yes, it is possible to escape the pausing using e.g. `nohup` when running without docker. # however, that won't work while running inside docker. Sorry. @@ -137,8 +148,7 @@ def suspend(process): except: pass -def resume(process): - procs = process.children(recursive=True) +def resume(procs): for p in procs: try: p.resume() diff --git a/battlecode-manager/server.py b/battlecode-manager/server.py index 61011bf..9043fa8 100644 --- a/battlecode-manager/server.py +++ b/battlecode-manager/server.py @@ -246,7 +246,6 @@ def end_turn(self): # print(line) if self.extra_delay: - import time time.sleep(self.extra_delay / 1000.) # Increment to the next player @@ -340,6 +339,7 @@ def __init__(self, *args, **kwargs): self.error = "" self.logged_in = False self.is_unix_stream = is_unix_stream + super(ReceiveHandler, self).__init__(*args, **kwargs) def get_next_message(self) -> object: @@ -352,20 +352,16 @@ def get_next_message(self) -> object: loaded string ''' - recv_socket = self.request - game = self.game - - wrapped_socket = recv_socket.makefile('rwb', 1) logging.debug("Client %s: Waiting for next message", self.client_id) try: - data = next(wrapped_socket) + data = next(self.wrapped_socket) except (StopIteration, IOError): print("{} has not sent message for {} seconds, assuming they're dead".format( self.game.get_player(self.client_id)['player'], TIMEOUT )) - wrapped_socket.close() - recv_socket.close() + self.wrapped_socket.close() + self.request.close() if bc.Team.Red == self.game.get_player(self.client_id)['player'].team: self.game.winner = 'player2' elif bc.Team.Blue == self.game.get_player(self.client_id)['player'].team: @@ -377,8 +373,8 @@ def get_next_message(self) -> object: self.game.game_over = True raise TimeoutError() except KeyboardInterrupt: - wrapped_socket.close() - recv_socket.close() + self.wrapped_socket.close() + self.request.close() if bc.Team.Red == self.game.get_player(self.client_id)['player'].team: self.game.winner = 'player2' elif bc.Team.Blue == self.game.get_player(self.client_id)['player'].team: @@ -389,13 +385,8 @@ def get_next_message(self) -> object: self.game.disconnected = True self.game.game_over = True raise KeyboardInterrupt() - finally: - wrapped_socket.close() - data = data.decode("utf-8").strip() - return data - #unpacked_data = json.loads(data) - #return unpacked_data + return data.decode("utf-8").strip() def send_message(self, obj: object) -> None: ''' @@ -410,22 +401,21 @@ def send_message(self, obj: object) -> None: None ''' - - send_socket = self.request if isinstance(obj, bytes): - obj = obj.decode() + encoded_message = obj + encoded_message.append(b'\n') + else: + message = obj + "\n" + encoded_message = message.encode() - message = obj + "\n" - encoded_message = message.encode() logging.debug("Client %s: Sending message %s", self.client_id, encoded_message) - wrapped_socket = send_socket.makefile('rwb', 1) try: - wrapped_socket.write(encoded_message) + self.wrapped_socket.write(encoded_message) except IOError: - wrapped_socket.close() - send_socket.close() + self.wrapped_socket.close() + self.request.close() print("{} has not accepted message for {} seconds, assuming they're dead".format( [p for p in self.game.players if p['id'] == self.client_id][0]['player'], TIMEOUT @@ -441,8 +431,8 @@ def send_message(self, obj: object) -> None: self.game.game_over = True raise TimeoutError() except KeyboardInterrupt: - wrapped_socket.close() - send_socket.close() + self.wrapped_socket.close() + self.request.close() if bc.Team.Red == self.game.get_player(self.client_id)['player'].team: self.game.winner = 'player2' elif bc.Team.Blue ==self.game.get_player(self.client_id)['player'].team: @@ -453,8 +443,6 @@ def send_message(self, obj: object) -> None: self.game.disconnected = True self.game.game_over = True raise KeyboardInterrupt() - finally: - wrapped_socket.close() return def message(self, state_diff): @@ -487,6 +475,7 @@ def player_handler(self): self.logged_in = False logging.debug("Client connected to server") self.request.settimeout(TIMEOUT) + self.wrapped_socket = self.request.makefile('rwb', 1) TIMEDOUTLOG = False @@ -513,6 +502,7 @@ def player_handler(self): self.send_message(log_success) if self.game.game_over: + self.wrapped_socket.close() return logging.debug("Client %s: Spinning waiting for game to start", @@ -534,12 +524,14 @@ def player_handler(self): # This is the loop that the code will always remain in # Blocks until it this clients turn if not self.game.start_turn(self.client_id): + self.wrapped_socket.close() self.request.close() return if self.game.manager.is_over(): self.game.game_over = True self.game.end_turn() + self.wrapped_socket.close() self.request.close() return @@ -554,6 +546,8 @@ def player_handler(self): running_stats["bld"] = False if self.game.initialized <= 3: + # Refresh the process tree here, then assume it is going to stay the same for performance reasons + my_sandbox.refreshProcessChildren() my_sandbox.unpause() self.send_message(start_turn_msg) self.game.initialized += 1