PK!:!!psmon/__init__.pyfrom .main import ProcessMonitor PK!mSxx 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 @classmethod def get_error(self, pid): raise NotImplementedError() PK!Jvvpsmon/limiters/__init__.pyfrom .cpu_times import CpuTimeLimiter from .wall_time import WallTimeLimiter from .max_memory import MaxMemoryLimiter PK!^ mmpsmon/limiters/base.pyfrom collections import defaultdict from psmon.base import Watcher class CommonResourceLimiter(Watcher): watched_attrs = [] def __init__(self, limit=None): self._limit = limit self._known_pids = set() self._root = None self._tree = defaultdict(list) self._resource_usage = {} def register_root(self, pid): self._root = pid self._resource_usage[pid] = None def _get_resource_usage(self, stats, pid): raise NotImplementedError() def _update(self, root, by_pid): if root not in by_pid: return self._resource_usage[root] resource = self._get_resource_usage(by_pid[root], root) for child in self._tree[root]: resource += self._update(child, by_pid) self._resource_usage[root] = self._get_max_usage( self._resource_usage[root], resource ) return self._resource_usage[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._resource_usage[pid] = None self._update(self._root, by_pid) def get_stats(self, pid): return self._resource_usage[pid] def should_terminate(self, pid): if not self._limit: return False return self.get_stats(pid) > self._limit @classmethod def fallback(cls, res): raise NotImplementedError() @classmethod def _get_max_usage(cls, previous, current): if previous is None: return current return max(previous, current) @classmethod def get_error(cls, pid): return (ResourceWarning, "Resource usage limit exceeded!") PK!Uiipsmon/limiters/cpu_times.pyfrom psmon.limiters.base import CommonResourceLimiter class CpuTimeLimiter(CommonResourceLimiter): watched_attrs = ["cpu_times"] def _get_resource_usage(self, stats, pid): if stats["cpu_times"] is None: return self._resource_usage[pid] return stats["cpu_times"].user + stats["cpu_times"].system @classmethod def fallback(cls, res): return res.ru_utime + res.ru_stime @classmethod def _get_max_usage(cls, previous, current): return current @classmethod def get_error(cls, pid): return (TimeoutError, "CPU time limit exceeded!") PK! psmon/limiters/max_memory.pyfrom psmon.limiters.base import CommonResourceLimiter class MaxMemoryLimiter(CommonResourceLimiter): watched_attrs = ["memory_info"] def _get_resource_usage(self, stats, pid): if stats["memory_info"] is None: return self._resource_usage[pid] return stats["memory_info"].rss @classmethod def fallback(cls, res): return res.ru_maxrss @classmethod def get_error(cls, pid): return (MemoryError, "Max memory limit exceeded!") PK!Cyȡpsmon/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.perf_counter() - self._start_time def register_root(self, pid): self._start_time = time.perf_counter() def update(self, processes_info): pass def get_stats(self, pid): return time.perf_counter() - self._start_time def should_terminate(self, pid): return time.perf_counter() - self._start_time > self._limit @classmethod def get_error(cls, pid): return TimeoutError, "Wall time limit exceeded!" PK!pp psmon/main.pyimport atexit import os import resource import time from queue import Queue from subprocess import PIPE, Popen import psutil from loguru import logger from psmon.utils import FileReader, graceful_kill, first_true, extract_file_queue class ProcessMonitor: def __init__(self, *popenargs, input=None, capture_output=False, freq=10, **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.freq = freq self.kwargs = kwargs self.watchers = {} self.watched_attrs = dict(pid=1, ppid=1, status=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: stats = process.as_dict(list(self.watched_attrs.keys())) if stats["status"] == psutil.STATUS_ZOMBIE: return None return stats 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: 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() self.send_processes_stats(processes_info) if len(processes_info) == 0: is_premature_stop = True if self.capture_output: stdout_reader = FileReader(process.stdout, self.stdout_queue) stderr_reader = FileReader(process.stderr, self.stderr_queue) stdout_reader.start() stderr_reader.start() if self.input: process.stdin.write(self.input) process.stdin.close() 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()) terminating_watcher = first_true( lambda watcher: watcher.should_terminate(root_pid), self.watchers.values(), ) if terminating_watcher is not None: error, error_str = terminating_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) 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() stats["stdout"] = extract_file_queue(self.stdout_queue) stats["stderr"] = extract_file_queue(self.stderr_queue) if stats["error"]: logger.warning(stats["error_str"]) atexit.unregister(self.stop) res = resource.getrusage(resource.RUSAGE_SELF) own_cpu_time = res.ru_utime + res.ru_stime stats["psmon_cpu_time"] = own_cpu_time logger.info(stats) return stats PK!psmon/utils.pyimport os import threading import psutil class FileReader(threading.Thread): def __init__(self, fd, queue): super().__init__() self._fd = fd self._queue = queue def run(self): for line in iter(self._fd.readline, b""): self._queue.put(line) def extract_file_queue(queue): result = b"" while not queue.empty(): result += queue.get() return result def first_true(pred, iterable, default=None): """Returns the first true value in the iterable. If no true value is found, returns *default* source: https://docs.python.org/3/library/itertools.html """ # first_true([a,b,c], x) --> a or b or c or x # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x return next(filter(pred, iterable), default) 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: try: proc.terminate() except psutil.NoSuchProcess: continue 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) 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 ChildProcessError: continue return returncodes PK!XB33psmon-1.1.1.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.1.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!H psmon-1.1.1.dist-info/METADATAݎ0FH6Nw]. *PnbR6I!<=j"(QsΌgBIA[q>R%Ai -\ :ش*K넏M'F-UdN DI\r)+FQ=T>\HAtYӛHR ߩË&Gh\J iE3$tvL*0,2(M^QaoL]_z_@&y+޺učz]ib*EntR>Oi:W_=[i5 FQP˜fbٟaO/f^4VPlmmmt XhUd qMg\R}.v0!݋Ʈv.k6o3.%*8yE贁-zB79[Q.aSu-<7.w"_" *<-2İ&.,`~i{{c6c\SͿ>[QPK!H(`psmon-1.1.1.dist-info/RECORDuɒ@{? t9R, ^db)~q:ØAa G /y\>Lj!9Rk3D-@b_Od XvNq-/9eMC׽rlm݀8$XQA*X>r@؂R2 L5`Qb|Y`g&M/^ŗ|sQSd7W48F057Iy6A/)!a'b'#Mn¹ߣFePs8 ' -u4uZ74BlCdworFȽ%I@|\-WUw'Ҳ9QUzjKkric:LjS|ܷ zBP 'cU:ZN Mվ|qnGRw#E(C9A[RZ+[P8Ksş~$o{wCt|#|S%֑!\,BVĈJ/+0^J{(JYe4"|^^X&ڊ'$Oz 2uTl\1V yI R.~UvEoPK!:!!psmon/__init__.pyPK!mSxx Ppsmon/base.pyPK!Jvvpsmon/limiters/__init__.pyPK!^ mmpsmon/limiters/base.pyPK!UiiB psmon/limiters/cpu_times.pyPK!  psmon/limiters/max_memory.pyPK!Cyȡ psmon/limiters/wall_time.pyPK!pp psmon/main.pyPK!)psmon/utils.pyPK!XB330psmon-1.1.1.dist-info/LICENSEPK!Hu)GTU 5psmon-1.1.1.dist-info/WHEELPK!H 5psmon-1.1.1.dist-info/METADATAPK!H(`7psmon-1.1.1.dist-info/RECORDPK :