PK!:!!psmon/__init__.pyfrom .main import ProcessMonitor PK!L!h]] psmon/base.pyclass Watcher(object): watched_attrs = [] def fallback(self, res): pass def register_root(self, pid): pass def update(self, processes_info): pass def get_stats(self, pid): pass def should_terminate(self, pid): return False def get_error(self, pid): return None, None PK!Jvvpsmon/limiters/__init__.pyfrom .cpu_times import CpuTimeLimiter from .wall_time import WallTimeLimiter from .max_memory import MaxMemoryLimiter PK!psmon/limiters/cpu_times.pyfrom collections import defaultdict, namedtuple from psmon.base import Watcher class CpuTimeLimiter(Watcher): watched_attrs = ["cpu_times"] def __init__(self, limit): self._limit = limit self._known_pids = set() self._root = None self._tree = defaultdict(list) self._cpu_times = {} def register_root(self, pid): self._root = pid def _update(self, root, by_pid): if root not in by_pid or by_pid[root]["cpu_times"] is None: return self._cpu_times[root] cpu_time = by_pid[root]["cpu_times"].user + by_pid[root]["cpu_times"].system for child in self._tree[root]: cpu_time += self._update(child, by_pid) self._cpu_times[root] = cpu_time return cpu_time def update(self, processes_info): by_pid = {} for pinfo in processes_info: pid = pinfo["pid"] by_pid[pid] = pinfo if pid not in self._known_pids: self._known_pids.add(pid) self._tree[pinfo["ppid"]].append(pid) self._cpu_times[pid] = 0 self._update(self._root, by_pid) def fallback(self, res): return res.ru_utime + res.ru_stime def get_stats(self, pid): return self._cpu_times[pid] def should_terminate(self, pid): return self.get_stats(pid) > self._limit def get_error(self, pid): return (TimeoutError, "CPU time limit exceeded!") PK!@psmon/limiters/max_memory.pyfrom collections import defaultdict, namedtuple from psmon.base import Watcher class MaxMemoryLimiter(Watcher): watched_attrs = ["memory_info"] def __init__(self, limit): self._limit = limit self._known_pids = set() self._root = None self._tree = defaultdict(list) self._max_memory = {} def register_root(self, pid): self._root = pid def _update(self, root, by_pid): if root not in by_pid or by_pid[root]["memory_info"] is None: return self._max_memory[root] max_mem = by_pid[root]["memory_info"].rss for child in self._tree[root]: max_mem += self._update(child, by_pid) self._max_memory[root] = max(self._max_memory[root], max_mem) return self._max_memory[root] def update(self, processes_info): by_pid = {} for pinfo in processes_info: pid = pinfo["pid"] by_pid[pid] = pinfo if pid not in self._known_pids: self._known_pids.add(pid) self._tree[pinfo["ppid"]].append(pid) self._max_memory[pid] = 0 self._update(self._root, by_pid) def fallback(self, res): return res.ru_maxrss def get_stats(self, pid): return self._max_memory[pid] def should_terminate(self, pid): return self.get_stats(pid) > self._limit def get_error(self, pid): return (MemoryError, "Max memory limit exceeded!") PK!apsmon/limiters/wall_time.pyimport time from psmon.base import Watcher class WallTimeLimiter(Watcher): watched_attrs = [] def __init__(self, limit): self._limit = limit def fallback(self, res): return time.monotonic() - self._start_time def register_root(self, pid): self._start_time = time.monotonic() def update(self, processes_info): pass def get_stats(self, pid): return time.monotonic() - self._start_time def should_terminate(self, pid): return time.monotonic() - self._start_time > self._limit def get_error(self, pid): return TimeoutError, "Wall time limit exceeded!" PK!7= psmon/main.pyimport os import time import atexit import resource import threading import psutil from queue import Queue from subprocess import Popen, PIPE from loguru import logger from psmon.utils import graceful_kill class Reader(threading.Thread): def __init__(self, fd, queue, text=False): super().__init__() self._fd = fd self._queue = queue self._text = text def run(self): for line in iter(self._fd.readline, b""): if self._text: line = line.decode().strip() self._queue.put(line) class ProcessMonitor: def __init__( self, *popenargs, input=None, capture_output=False, freq=10, text=False, **kwargs, ): if input is not None: if "stdin" in kwargs: raise ValueError("stdin and input arguments may not both be used.") kwargs["stdin"] = PIPE if capture_output: if ("stdout" in kwargs) or ("stderr" in kwargs): raise ValueError( "stdout and stderr arguments may not be used " "with capture_output." ) kwargs["stdout"] = PIPE kwargs["stderr"] = PIPE self.stdout_queue = Queue() self.stderr_queue = Queue() self.popenargs = popenargs self.input = input self.capture_output = capture_output self.text = text self.freq = freq self.kwargs = kwargs self.watchers = {} self.watched_attrs = dict(pid=1, ppid=1) self.root_process = None self.processes = set() def subscribe(self, watcher_id, watcher): self.watchers[watcher_id] = watcher for attr in watcher.watched_attrs: self.watch_attr(attr) def unsubscribe(self, watcher_id): watcher = self.watchers[watcher_id] for attr in watcher.watched_attrs: self.unwatch_attr(attr) del self.watchers[watcher_id] def watch_attr(self, attr): if attr in self.watched_attrs: self.watched_attrs[attr] += 1 else: self.watched_attrs[attr] = 1 def unwatch_attr(self, attr): self.watched_attrs[attr] -= 1 if self.watched_attrs[attr] == 0: del self.watched_attrs[attr] def update_tree(self): children = self.root_process.children(recursive=True) self.processes.update(set(children)) def try_get_process_info(self, process): try: return process.as_dict(list(self.watched_attrs.keys())) except psutil.NoSuchProcess: return None def get_processes_info(self): return list( filter(None.__ne__, [self.try_get_process_info(p) for p in self.processes]) ) def send_processes_stats(self, stats): for watcher in self.watchers.values(): watcher.update(stats) def is_root_process_running(self): return ( self.root_process.is_running and not self.root_process.status() == psutil.STATUS_ZOMBIE ) def stop(self): return graceful_kill(self.processes) def run(self): atexit.register(self.stop) stdout_reader = None stderr_reader = None with Popen(*self.popenargs, preexec_fn=os.setpgrp, **self.kwargs) as process: if self.capture_output: stdout_reader = Reader(process.stdout, self.stdout_queue, self.text) stderr_reader = Reader(process.stderr, self.stderr_queue, self.text) stdout_reader.start() stderr_reader.start() if self.input: process.stdin.write(self.input) error = None error_str = None is_premature_stop = False root_pid = process.pid for watcher in self.watchers.values(): watcher.register_root(root_pid) self.root_process = psutil.Process(root_pid) self.processes.add(self.root_process) processes_info = self.get_processes_info() if len(processes_info) == 0: is_premature_stop = True else: self.send_processes_stats(processes_info) should_terminate = False while self.is_root_process_running() and not should_terminate: try: self.update_tree() except psutil.NoSuchProcess: break self.send_processes_stats(self.get_processes_info()) for watcher in self.watchers.values(): if watcher.should_terminate(root_pid): error, error_str = watcher.get_error(root_pid) should_terminate = True break time.sleep(1.0 / self.freq) if is_premature_stop: pid, ret, res = os.wait4(root_pid, os.WNOHANG | os.WUNTRACED) assert pid == root_pid stats = { watcher_id: watcher.fallback(res) for watcher_id, watcher in self.watchers.items() } stats["return_code"] = ret else: stats = { watcher_id: watcher.get_stats(root_pid) for watcher_id, watcher in self.watchers.items() } return_codes = self.stop() stats["return_code"] = return_codes[root_pid] stats["error"] = error stats["error_str"] = error_str if self.capture_output: stdout_reader.join() stderr_reader.join() stdout = [] while not self.stdout_queue.empty(): stdout.append(self.stdout_queue.get()) stderr = [] while not self.stderr_queue.empty(): stderr.append(self.stderr_queue.get()) stats["stdout"] = stdout stats["stderr"] = stderr if stats["error"]: logger.warning(stats["error_str"]) atexit.unregister(self.stop) res = resource.getrusage(resource.RUSAGE_SELF) logger.info( f"Used approximately {res.ru_utime + res.ru_stime: .2f}s cpu time for monitoring" ) return stats PK!'psmon/utils.pyimport os import psutil def graceful_kill(processes, timeout=3): """ Terminate then kill pids after ${timeout} s. Also return returncode as dict([pid]: returncode) """ stopped = [] initially_gone = [p for p in processes if not p.is_running()] processes = [p for p in processes if p not in initially_gone] for proc in processes: proc.terminate() gone, alive = psutil.wait_procs(processes, timeout=timeout) stopped += gone if len(alive) > 0: for proc in alive: proc.kill() gone, alive = psutil.wait_procs(processes, timeout=timeout) assert len(alive) > 0 stopped += gone returncodes = {proc.pid: proc.returncode for proc in stopped} for proc in initially_gone: try: ret = os.waitpid(proc.pid, 0) returncodes[proc.pid] = ret except: continue return returncodes PK!XB33psmon-1.0.0.dist-info/LICENSEMIT License Copyright (c) 2019 Rakha Kanz Kautsar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!Hu)GTUpsmon-1.0.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!HbnYpsmon-1.0.0.dist-info/METADATAMO1MhvW4B$rJ^ql?l*z=e>X*B6ACM2)OKtcQ5)$?T#z'7cBf~FX']̛[e[cu ř۵!aIX-ܫelo6#reFc~kӥPcPK!HK?8psmon-1.0.0.dist-info/RECORDur@{ld Ba6>cTN\}=j$OÍ@UZs?mH‘v("UH +ޝF/(ڶ/.&5ݍ3gPNL%V@`ָri~2GT 8CGt4-|aJpM/JIC{UFcSybxӁgܥhD>#hlkm=lެo&+ylss)ݝbOo.)M̝1cS m`k ڈ.wSd1lҿǮ?6$TpCWYjc{=r e5|K"ӊRm[D{wl2<!? I?Qokɺ+gCU~hrGG:WqE+l&7ɭuSJOMoTSUX΅9fKf= l*A3l!|'/)/}2a`GJdWR)Jh'Z{oTOmO!PK!:!!psmon/__init__.pyPK!L!h]] Ppsmon/base.pyPK!Jvvpsmon/limiters/__init__.pyPK!psmon/limiters/cpu_times.pyPK!@psmon/limiters/max_memory.pyPK!apsmon/limiters/wall_time.pyPK!7= Gpsmon/main.pyPK!'*psmon/utils.pyPK!XB33M.psmon-1.0.0.dist-info/LICENSEPK!Hu)GTU2psmon-1.0.0.dist-info/WHEELPK!HbnYH3psmon-1.0.0.dist-info/METADATAPK!HK?84psmon-1.0.0.dist-info/RECORDPK ?O7