PK!ז=p psmon/main.pyimport os import multiprocessing import time import signal from subprocess import Popen, PIPE from collections import namedtuple import resource import subprocess import psutil from psmon.process import ProcessNode from psmon.utils import graceful_kill, human2bytes WALL_EXCEEDED_STR = "Wall time limit exceeded!" CPU_EXCEEDED_STR = "CPU time limit exceeded!" MEMORY_EXCEEDED_STR = "Memory usage limit exceeded!" class Watcher(multiprocessing.Process): def __init__( self, root_pid, period=0.1, wall_time_limit=None, cpu_time_limit=None, memory_limit=None, pipe=None, **kwargs, ): super().__init__(**kwargs) self.pipe = pipe self.root_pid = root_pid self.period = period self.wall_time_limit = wall_time_limit self.cpu_time_limit = cpu_time_limit self.memory_limit = memory_limit self.processes = {} def get_processes_info(self): return [ p.info for p in psutil.process_iter( attrs=["pid", "ppid", "cpu_times", "memory_info", "status"] ) if p.info["pid"] in self.processes or p.info["ppid"] in self.processes ] def stop_processes(self, error=None, error_str=None, timeout=3): if error_str: print("[!] {}".format(error_str)) processes_info = self.get_processes_info() running_pids = [ pinfo["pid"] for pinfo in processes_info if pinfo["status"] == psutil.STATUS_RUNNING ] graceful_kill(running_pids, [signal.SIGTERM, signal.SIGKILL], timeout) def run(self): self.processes[self.root_pid] = ProcessNode(self.root_pid) root_process = psutil.Process(self.root_pid) start_time = time.monotonic() if self.wall_time_limit: def alarm_handler(*args): self.stop_processes(error=TimeoutError, error_str=WALL_EXCEEDED_STR) signal.signal(signal.SIGALRM, alarm_handler) signal.alarm(self.wall_time_limit) while ( root_process.is_running() and not root_process.status() == psutil.STATUS_ZOMBIE ): processes_info = self.get_processes_info() for pinfo in processes_info: if pinfo["pid"] not in self.processes: self.processes[pinfo["pid"]] = ProcessNode(pinfo["pid"]) self.processes[pinfo["ppid"]].add_child( self.processes[pinfo["pid"]] ) if pinfo["cpu_times"] is None or pinfo["memory_info"] is None: continue cpu_time = pinfo["cpu_times"].user + pinfo["cpu_times"].system memory = pinfo["memory_info"].rss self.processes[pinfo["pid"]].update(cpu_time, memory) if ( self.cpu_time_limit and self.processes[self.root_pid].cpu_time > self.cpu_time_limit ): self.stop_processes(error=TimeoutError, error_str=CPU_EXCEEDED_STR) break if ( self.memory_limit and self.processes[self.root_pid].max_memory > self.memory_limit ): self.stop_processes(error=MemoryError, error_str=MEMORY_EXCEEDED_STR) break time.sleep(self.period) signal.alarm(0) self.stop_processes() self.pipe.send( dict( wall_time=time.monotonic() - start_time, cpu_time=self.processes[self.root_pid].cpu_time, max_memory=self.processes[self.root_pid].max_memory, ) ) def run( args, *popenargs, wall_time_limit=None, cpu_time_limit=None, memory_limit=None, **kwargs, ): process = Popen(args, *popenargs, stdout=PIPE, stderr=PIPE, preexec_fn=os.setpgrp) process.poll() if type(memory_limit) == str: memory_limit = human2bytes(memory_limit) recv_pipe, send_pipe = multiprocessing.Pipe(duplex=False) watcher_process = Watcher( process.pid, wall_time_limit=wall_time_limit, cpu_time_limit=cpu_time_limit, memory_limit=memory_limit, pipe=send_pipe, daemon=True, **kwargs, ) watcher_process.start() outs, errs = process.communicate() # process.wait() stats = recv_pipe.recv() print(stats) watcher_process.join() return stats PK!heepsmon/process.pyclass ProcessNode: def __init__(self, pid): self.pid = pid self.parent = None self.children = [] self.max_memory = 0 self.cpu_time = 0 self.finished = False def add_child(self, child): self.children.append(child) child.parent = self def update(self, cpu_time, memory): self.max_memory = max(self.max_memory, memory) self.cpu_time = cpu_time if self.parent: self.parent.update( self.parent.cpu_time + cpu_time, self.parent.max_memory + self.max_memory, ) PK!G>* * psmon/utils.pyimport os import time import psutil def _is_running(pid): try: return os.waitpid(pid, os.WNOHANG) == (0, 0) except ChildProcessError: try: return psutil.Process(pid).is_running() except: return False def graceful_kill(pids, signals, timeout=3): for signal in signals: for pid in pids: try: os.kill(pid, signal) except: pass end_time = time.monotonic() + timeout while time.monotonic() < end_time: running_pids = [pid for pid in pids if _is_running(pid)] if len(running_pids) > 0: time.sleep(0.1) pids = running_pids else: return ## {{{ http://code.activestate.com/recipes/578019/ (r15) """ Bytes-to-human / human-to-bytes converter. Based on: http://goo.gl/kTQMs Working with Python 2.x and 3.x. Author: Giampaolo Rodola' License: MIT """ # see: http://goo.gl/kTQMs SYMBOLS = { "customary": ("B", "K", "M", "G", "T", "P", "E", "Z", "Y"), "customary_ext": ( "byte", "kilo", "mega", "giga", "tera", "peta", "exa", "zetta", "iotta", ), "iec": ("Bi", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"), "iec_ext": ("byte", "kibi", "mebi", "gibi", "tebi", "pebi", "exbi", "zebi", "yobi"), } def human2bytes(s): """ Attempts to guess the string format based on default symbols set and return the corresponding bytes as an integer. When unable to recognize the format ValueError is raised. >>> human2bytes('0 B') 0 >>> human2bytes('1 K') 1024 >>> human2bytes('1 M') 1048576 >>> human2bytes('1 Gi') 1073741824 >>> human2bytes('1 tera') 1099511627776 >>> human2bytes('0.5kilo') 512 >>> human2bytes('0.1 byte') 0 >>> human2bytes('1 k') # k is an alias for K 1024 >>> human2bytes('12 foo') Traceback (most recent call last): ... ValueError: can't interpret '12 foo' """ init = s num = "" while s and s[0:1].isdigit() or s[0:1] == ".": num += s[0] s = s[1:] num = float(num) letter = s.strip() for name, sset in SYMBOLS.items(): if letter in sset: break else: if letter == "k": # treat 'k' as an alias for 'K' as per: http://goo.gl/kTQMs sset = SYMBOLS["customary"] letter = letter.upper() else: raise ValueError("can't interpret %r" % init) prefix = {sset[0]: 1} for i, s in enumerate(sset[1:]): prefix[s] = 1 << (i + 1) * 10 return int(num * prefix[letter]) ## end of http://code.activestate.com/recipes/578019/ }}} PK!XB33psmon-0.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-0.1.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!HHZpsmon-0.1.1.dist-info/METADATARMO1W4 0iH znYY1zKy3K@I<[Ap5tAZkwM!N+k!r2$¢~S_ }^L re _˲FZ-r72[Kbi3nt۩-oT웁L5 W}FCes,"fOޘʞ6@:cd PK!ז=p psmon/main.pyPK!heepsmon/process.pyPK!G>* * psmon/utils.pyPK!XB33psmon-0.1.1.dist-info/LICENSEPK!Hu)GTUY$psmon-0.1.1.dist-info/WHEELPK!HHZ$psmon-0.1.1.dist-info/METADATAPK!HP΀Pj&psmon-0.1.1.dist-info/RECORDPK'