You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
9.1 KiB
Python
278 lines
9.1 KiB
Python
# 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
|