import os import psutil import subprocess import threading import sys from threading import Timer import select from player_abstract import AbstractPlayer class PlainPlayer(AbstractPlayer): def __init__(self, socket_file, working_dir, local_dir=None, player_key="", player_mem_limit=256, player_cpu=20): self.paused = False self.streaming = False self.process = None super().__init__(socket_file, working_dir, local_dir, None, None, player_key, player_mem_limit, player_cpu) def stream_logs(self, stdout=True, stderr=True, line_action=lambda line: print(line.decode())): assert not self.streaming self.streaming = True if stdout: threading.Thread(target=self._stream_logs, args=(self.process.stdout, line_action), daemon=True).start() if stderr: threading.Thread(target=self._stream_logs, args=(self.process.stderr, line_action), daemon=True).start() def _stream_logs(self, stream, line_action): for line in stream: if self.process is None: return line_action(line) def start(self): if sys.platform == 'win32': args = [os.path.join(self.working_dir, 'run.bat')] # things break otherwise env = dict(os.environ) else: args = ['sh', os.path.join(self.working_dir, 'run.sh')] # Path needs to be passed through, otherwise some compilers (e.g gcc) can get confused and not find things env = {'PATH': os.environ['PATH']} env['PLAYER_KEY'] = str(self.player_key) env['RUST_BACKTRACE'] = '1' env['BC_PLATFORM'] = self._detect_platform() if isinstance(self.socket_file, tuple): # tcp port env['TCP_PORT'] = str(self.socket_file[1]) else: env['SOCKET_FILE'] = self.socket_file cwd = self.working_dir self.process = psutil.Popen(args, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=-1) def guess_language(self): children = self.process.children(recursive=True) for c in children: name = c.exe() if "java" in name: return "jvm" elif "python" in name: return "python" elif "pypy" in name: return "pypy" elif "mono" in name: return "mono" return "c" def pause(self): # pausing too slow on windows if sys.platform == 'win32': return if not self.paused: self.paused = True suspend(self.process) def unpause(self, timeout=None): # pausing too slow on windows if sys.platform == 'win32': return if self.paused: resume(self.process) self.paused = False def destroy(self): if self.process is not None: tmp = self.process # This will signal to the log thread that everything is going to be shut down # and ignore any future messages. In particular bash may log something like 'Terminated: <PID>' # which would pollute the output of this script. self.process = None reap(tmp) self.process = None super().destroy() def reap(process, timeout=3): "Tries hard to terminate and ultimately kill all the children of this process." def on_terminate(proc): pass # print("process {} terminated with exit code {}".format(proc.pid, proc.returncode)) try: procs = process.children(recursive=True) # send SIGTERM for p in procs: p.terminate() gone, alive = psutil.wait_procs(procs, timeout=timeout, callback=on_terminate) if alive: # send SIGKILL for p in alive: p.kill() gone, alive = psutil.wait_procs(alive, timeout=timeout, callback=on_terminate) if alive: # give up for p in alive: print("process {} survived SIGKILL; giving up" % p.pid) process.kill() except: print("Killing failed; assuming process exited early.") def suspend(process): procs = process.children(recursive=False) # 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. for p in procs: try: p.suspend() except: pass try: p.suspend() except: pass def resume(process): procs = process.children(recursive=True) for p in procs: try: p.resume() except: pass try: p.resume() except: pass