| Index: third_party/logilab/common/proc.py
|
| diff --git a/third_party/logilab/common/proc.py b/third_party/logilab/common/proc.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c27356c6b88b286775998ab414de95541f7eb5f5
|
| --- /dev/null
|
| +++ b/third_party/logilab/common/proc.py
|
| @@ -0,0 +1,277 @@
|
| +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
|
| +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
| +#
|
| +# This file is part of logilab-common.
|
| +#
|
| +# logilab-common is free software: you can redistribute it and/or modify it under
|
| +# the terms of the GNU Lesser General Public License as published by the Free
|
| +# Software Foundation, either version 2.1 of the License, or (at your option) any
|
| +# later version.
|
| +#
|
| +# logilab-common is distributed in the hope that it will be useful, but WITHOUT
|
| +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
| +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
| +# details.
|
| +#
|
| +# You should have received a copy of the GNU Lesser General Public License along
|
| +# with logilab-common. If not, see <http://www.gnu.org/licenses/>.
|
| +"""module providing:
|
| +* process information (linux specific: rely on /proc)
|
| +* a class for resource control (memory / time / cpu time)
|
| +
|
| +This module doesn't work on windows platforms (only tested on linux)
|
| +
|
| +:organization: Logilab
|
| +
|
| +
|
| +
|
| +"""
|
| +__docformat__ = "restructuredtext en"
|
| +
|
| +import os
|
| +import stat
|
| +from resource import getrlimit, setrlimit, RLIMIT_CPU, RLIMIT_AS
|
| +from signal import signal, SIGXCPU, SIGKILL, SIGUSR2, SIGUSR1
|
| +from threading import Timer, currentThread, Thread, Event
|
| +from time import time
|
| +
|
| +from logilab.common.tree import Node
|
| +
|
| +class NoSuchProcess(Exception): pass
|
| +
|
| +def proc_exists(pid):
|
| + """check the a pid is registered in /proc
|
| + raise NoSuchProcess exception if not
|
| + """
|
| + if not os.path.exists('/proc/%s' % pid):
|
| + raise NoSuchProcess()
|
| +
|
| +PPID = 3
|
| +UTIME = 13
|
| +STIME = 14
|
| +CUTIME = 15
|
| +CSTIME = 16
|
| +VSIZE = 22
|
| +
|
| +class ProcInfo(Node):
|
| + """provide access to process information found in /proc"""
|
| +
|
| + def __init__(self, pid):
|
| + self.pid = int(pid)
|
| + Node.__init__(self, self.pid)
|
| + proc_exists(self.pid)
|
| + self.file = '/proc/%s/stat' % self.pid
|
| + self.ppid = int(self.status()[PPID])
|
| +
|
| + def memory_usage(self):
|
| + """return the memory usage of the process in Ko"""
|
| + try :
|
| + return int(self.status()[VSIZE])
|
| + except IOError:
|
| + return 0
|
| +
|
| + def lineage_memory_usage(self):
|
| + return self.memory_usage() + sum([child.lineage_memory_usage()
|
| + for child in self.children])
|
| +
|
| + def time(self, children=0):
|
| + """return the number of jiffies that this process has been scheduled
|
| + in user and kernel mode"""
|
| + status = self.status()
|
| + time = int(status[UTIME]) + int(status[STIME])
|
| + if children:
|
| + time += int(status[CUTIME]) + int(status[CSTIME])
|
| + return time
|
| +
|
| + def status(self):
|
| + """return the list of fields found in /proc/<pid>/stat"""
|
| + return open(self.file).read().split()
|
| +
|
| + def name(self):
|
| + """return the process name found in /proc/<pid>/stat
|
| + """
|
| + return self.status()[1].strip('()')
|
| +
|
| + def age(self):
|
| + """return the age of the process
|
| + """
|
| + return os.stat(self.file)[stat.ST_MTIME]
|
| +
|
| +class ProcInfoLoader:
|
| + """manage process information"""
|
| +
|
| + def __init__(self):
|
| + self._loaded = {}
|
| +
|
| + def list_pids(self):
|
| + """return a list of existent process ids"""
|
| + for subdir in os.listdir('/proc'):
|
| + if subdir.isdigit():
|
| + yield int(subdir)
|
| +
|
| + def load(self, pid):
|
| + """get a ProcInfo object for a given pid"""
|
| + pid = int(pid)
|
| + try:
|
| + return self._loaded[pid]
|
| + except KeyError:
|
| + procinfo = ProcInfo(pid)
|
| + procinfo.manager = self
|
| + self._loaded[pid] = procinfo
|
| + return procinfo
|
| +
|
| +
|
| + def load_all(self):
|
| + """load all processes information"""
|
| + for pid in self.list_pids():
|
| + try:
|
| + procinfo = self.load(pid)
|
| + if procinfo.parent is None and procinfo.ppid:
|
| + pprocinfo = self.load(procinfo.ppid)
|
| + pprocinfo.append(procinfo)
|
| + except NoSuchProcess:
|
| + pass
|
| +
|
| +
|
| +try:
|
| + class ResourceError(BaseException):
|
| + """Error raise when resource limit is reached"""
|
| + limit = "Unknown Resource Limit"
|
| +except NameError:
|
| + class ResourceError(Exception):
|
| + """Error raise when resource limit is reached"""
|
| + limit = "Unknown Resource Limit"
|
| +
|
| +
|
| +class XCPUError(ResourceError):
|
| + """Error raised when CPU Time limit is reached"""
|
| + limit = "CPU Time"
|
| +
|
| +class LineageMemoryError(ResourceError):
|
| + """Error raised when the total amount of memory used by a process and
|
| + it's child is reached"""
|
| + limit = "Lineage total Memory"
|
| +
|
| +class TimeoutError(ResourceError):
|
| + """Error raised when the process is running for to much time"""
|
| + limit = "Real Time"
|
| +
|
| +# Can't use subclass because the StandardError MemoryError raised
|
| +RESOURCE_LIMIT_EXCEPTION = (ResourceError, MemoryError)
|
| +
|
| +
|
| +class MemorySentinel(Thread):
|
| + """A class checking a process don't use too much memory in a separated
|
| + daemonic thread
|
| + """
|
| + def __init__(self, interval, memory_limit, gpid=os.getpid()):
|
| + Thread.__init__(self, target=self._run, name="Test.Sentinel")
|
| + self.memory_limit = memory_limit
|
| + self._stop = Event()
|
| + self.interval = interval
|
| + self.setDaemon(True)
|
| + self.gpid = gpid
|
| +
|
| + def stop(self):
|
| + """stop ap"""
|
| + self._stop.set()
|
| +
|
| + def _run(self):
|
| + pil = ProcInfoLoader()
|
| + while not self._stop.isSet():
|
| + if self.memory_limit <= pil.load(self.gpid).lineage_memory_usage():
|
| + os.killpg(self.gpid, SIGUSR1)
|
| + self._stop.wait(self.interval)
|
| +
|
| +
|
| +class ResourceController:
|
| +
|
| + def __init__(self, max_cpu_time=None, max_time=None, max_memory=None,
|
| + max_reprieve=60):
|
| + if SIGXCPU == -1:
|
| + raise RuntimeError("Unsupported platform")
|
| + self.max_time = max_time
|
| + self.max_memory = max_memory
|
| + self.max_cpu_time = max_cpu_time
|
| + self._reprieve = max_reprieve
|
| + self._timer = None
|
| + self._msentinel = None
|
| + self._old_max_memory = None
|
| + self._old_usr1_hdlr = None
|
| + self._old_max_cpu_time = None
|
| + self._old_usr2_hdlr = None
|
| + self._old_sigxcpu_hdlr = None
|
| + self._limit_set = 0
|
| + self._abort_try = 0
|
| + self._start_time = None
|
| + self._elapse_time = 0
|
| +
|
| + def _hangle_sig_timeout(self, sig, frame):
|
| + raise TimeoutError()
|
| +
|
| + def _hangle_sig_memory(self, sig, frame):
|
| + if self._abort_try < self._reprieve:
|
| + self._abort_try += 1
|
| + raise LineageMemoryError("Memory limit reached")
|
| + else:
|
| + os.killpg(os.getpid(), SIGKILL)
|
| +
|
| + def _handle_sigxcpu(self, sig, frame):
|
| + if self._abort_try < self._reprieve:
|
| + self._abort_try += 1
|
| + raise XCPUError("Soft CPU time limit reached")
|
| + else:
|
| + os.killpg(os.getpid(), SIGKILL)
|
| +
|
| + def _time_out(self):
|
| + if self._abort_try < self._reprieve:
|
| + self._abort_try += 1
|
| + os.killpg(os.getpid(), SIGUSR2)
|
| + if self._limit_set > 0:
|
| + self._timer = Timer(1, self._time_out)
|
| + self._timer.start()
|
| + else:
|
| + os.killpg(os.getpid(), SIGKILL)
|
| +
|
| + def setup_limit(self):
|
| + """set up the process limit"""
|
| + assert currentThread().getName() == 'MainThread'
|
| + os.setpgrp()
|
| + if self._limit_set <= 0:
|
| + if self.max_time is not None:
|
| + self._old_usr2_hdlr = signal(SIGUSR2, self._hangle_sig_timeout)
|
| + self._timer = Timer(max(1, int(self.max_time) - self._elapse_time),
|
| + self._time_out)
|
| + self._start_time = int(time())
|
| + self._timer.start()
|
| + if self.max_cpu_time is not None:
|
| + self._old_max_cpu_time = getrlimit(RLIMIT_CPU)
|
| + cpu_limit = (int(self.max_cpu_time), self._old_max_cpu_time[1])
|
| + self._old_sigxcpu_hdlr = signal(SIGXCPU, self._handle_sigxcpu)
|
| + setrlimit(RLIMIT_CPU, cpu_limit)
|
| + if self.max_memory is not None:
|
| + self._msentinel = MemorySentinel(1, int(self.max_memory) )
|
| + self._old_max_memory = getrlimit(RLIMIT_AS)
|
| + self._old_usr1_hdlr = signal(SIGUSR1, self._hangle_sig_memory)
|
| + as_limit = (int(self.max_memory), self._old_max_memory[1])
|
| + setrlimit(RLIMIT_AS, as_limit)
|
| + self._msentinel.start()
|
| + self._limit_set += 1
|
| +
|
| + def clean_limit(self):
|
| + """reinstall the old process limit"""
|
| + if self._limit_set > 0:
|
| + if self.max_time is not None:
|
| + self._timer.cancel()
|
| + self._elapse_time += int(time())-self._start_time
|
| + self._timer = None
|
| + signal(SIGUSR2, self._old_usr2_hdlr)
|
| + if self.max_cpu_time is not None:
|
| + setrlimit(RLIMIT_CPU, self._old_max_cpu_time)
|
| + signal(SIGXCPU, self._old_sigxcpu_hdlr)
|
| + if self.max_memory is not None:
|
| + self._msentinel.stop()
|
| + self._msentinel = None
|
| + setrlimit(RLIMIT_AS, self._old_max_memory)
|
| + signal(SIGUSR1, self._old_usr1_hdlr)
|
| + self._limit_set -= 1
|
|
|