Current File : //opt/imunify360/venv/lib64/python3.11/site-packages/defence360agent/utils/common.py |
import asyncio
import datetime
import functools
import logging
import socket
import time
import re
import os
import sys
MINUTE = datetime.timedelta(minutes=1).total_seconds()
HOUR = datetime.timedelta(hours=1).total_seconds()
DAY = datetime.timedelta(days=1).total_seconds()
WEEK = datetime.timedelta(weeks=1).total_seconds()
logger = logging.getLogger(__name__)
class ServiceBase(object):
"""Base service class."""
def __init__(self, loop):
self._loop = loop
self._should_stop = False
self._main_task = None
self._state = self.StoppedState(self)
def start(self):
return self._state.start()
def should_stop(self):
return self._state.should_stop()
async def wait(self):
return await self._state.wait()
def is_running(self):
return self._state.is_running()
async def _run(self):
raise NotImplementedError
class State(object):
def __init__(self, obj):
""":type obj: ServiceBase"""
self._obj = obj
def start(self):
pass
def should_stop(self):
pass
async def wait(self):
task = self._obj._main_task
if task:
await task
def is_running(self):
return False
class StoppedState(State):
def _on_stop(self, future):
self._obj._state = ServiceBase.StoppedState(self._obj)
self._obj._should_stop = False
def start(self):
obj = self._obj
obj._main_task = obj._loop.create_task(obj._run())
obj._main_task.add_done_callback(self._on_stop)
obj._state = ServiceBase.RunningState(obj)
class RunningState(State):
def should_stop(self):
obj = self._obj
obj._should_stop = True
obj._main_task.cancel()
obj._state = ServiceBase.StoppingState(obj)
def is_running(self):
return True
class StoppingState(State):
def start(self):
raise ProgrammingError(
"Cannot start stopping service. Please wait while it stop."
)
class ProgrammingError(Exception):
pass
class RateLimit:
"""Decorator to limit function calls to one per *period* seconds.
If less than *period* seconds have passed since the last call,
then the request to call the function is replace with an *on_drop*
call with the same arguments.
If *on_drop* is None [default] then the call is just dropped
"""
def __init__(self, period, timer=time.monotonic, *, on_drop=None):
self._next_call_time = None
self._period = period
self._timer = timer
self._on_drop = on_drop
@property
def should_be_called(self):
return (
self._next_call_time is None
or self._next_call_time <= self._timer()
)
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if self.should_be_called:
self._next_call_time = self._timer() + self._period
return func(*args, **kwargs)
elif self._on_drop is not None:
return self._on_drop(*args, **kwargs)
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
if self.should_be_called:
self._next_call_time = self._timer() + self._period
return await func(*args, **kwargs)
elif self._on_drop is not None:
return self._on_drop(*args, **kwargs)
return async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
rate_limit = RateLimit
class CoalesceCalls:
def __init__(self):
self.call_time = float("-inf")
self.delayed_call = None
def coalesce_calls(self, period, *, done_callback=None):
"""
Decorator to coalesce coroutine calls to one per *period* seconds.
Requests for a coroutine call in a given time period are coalesced:
If t is the time of the last call, then N call requests in the [t,
t+period) time interval results in a single call at the
t+period time iff N>0 i.e.,
if less than *period* seconds have passed since the last call,
then the calls are coalesced: (N-1) requests are dropped, Nth
requests is performed in *period* seconds.
It is unspecified which exact call is made if arguments differ.
If the call is not dropped then *done_callback* is attached
to the task when the coroutine is scheduled with the event loop.
Given `c` is the time of the last [actual] call (`loop.create_task()`)
And `T` is the coalesce time period
When a call request arrives at `t` time
Then
| call pending? | t>c+T | c<=t<=c+T | t<c |
|----------------+--------------------------------+------------+------|
| no p. call | call soon | call at c+T| warn |
| p. call at c+T | cancel the call/warn, call soon| drop call | warn |
"""
def decorator(coro):
@functools.wraps(coro)
async def wrapper(*args, **kwargs):
loop = kwargs.get("loop")
if loop is None:
loop = asyncio.get_event_loop()
if args or kwargs:
args_repr = "*%r, **%r" % (args, kwargs)
else: # special case no args case
args_repr = ""
call_repr = "%s(%s)" % (coro.__name__, args_repr)
def log_exception(task):
"""Log task's error
if any with event's loop exception handler.
CancelledError is not logged.
"""
if not task.cancelled() and task.exception() is not None:
loop.call_exception_handler(
{
"message": "Unhandled exception during "
+ call_repr,
"exception": task.exception(),
"task": task,
}
)
def call_delayed(coro, args, kwargs):
"""Call & schedule the delayed coroutine now."""
logger.info("Schedule call %s", call_repr)
self.call_time = loop.time()
self.delayed_call = None
task = loop.create_task(coro(*args, **kwargs))
task.add_done_callback(
log_exception
if done_callback is None
else done_callback
)
now = loop.time()
if now > (self.call_time + period): # call immediately
if self.delayed_call is not None:
# get string representation for logs
# before cancelling the call
old_delayed_call_repr = str(self.delayed_call)
self.delayed_call.cancel()
self.delayed_call = None
logger.warning(
"There was a scheduled call (%s)"
" but more than period (%r) seconds passed"
" since the last call (%r, now=%r)",
old_delayed_call_repr,
period,
self.call_time,
now,
)
logger.info(
"Satisfy the call request soon: %s. No calls in"
" more than %r seconds since the start",
call_repr,
period,
)
self.delayed_call = loop.call_soon(
call_delayed, coro, args, kwargs
)
elif self.call_time <= now <= (self.call_time + period):
delay = (self.call_time + period) - now
if self.delayed_call is not None: # drop call request
logger.info(
"Drop call request for %s"
", enforcing one call per %r seconds limit"
". Next call is in ~%.2f seconds",
call_repr,
period,
delay,
)
else: # schedule call request
assert self.delayed_call is None
logger.info(
"Delay call request: %s for ~%.2f seconds"
". Enforcing one call per %r seconds limit",
call_repr,
delay,
period,
)
self.delayed_call = loop.call_at(
self.call_time + period,
call_delayed,
coro,
args,
kwargs,
)
else: # now < call_time
logger.warning(
"Drop call request for %s, reason: last call time"
" (%r, now=%r) is in the future",
call_repr,
self.call_time,
now,
)
return wrapper
return decorator
webserver_gracefull_restart = CoalesceCalls()
def get_hostname():
"""Returns readable name of the server.
It is sent to CLN and allows user to sort out his servers.
"""
hostname = socket.getfqdn()
if hostname is None or hostname.lower().startswith("localhost"):
return socket.gethostname()
return hostname
# Everything from there is copied from setuptools package
# Copied from setuptools/_distutils/version.py
class Version:
"""Abstract base class for version numbering classes. Just provides
constructor (__init__) and reproducer (__repr__), because those
seem to be the same for all version numbering classes; and route
rich comparisons to _cmp.
"""
def __init__(self, vstring=None):
if vstring:
self.parse(vstring)
def __repr__(self):
return "{} ('{}')".format(self.__class__.__name__, str(self))
def __eq__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c == 0
def __lt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c < 0
def __le__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c <= 0
def __gt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c > 0
def __ge__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c >= 0
# Copied from setuptools/_distutils/version.py
class LooseVersion(Version):
"""Version numbering for anarchists and software realists.
Implements the standard interface for version number classes as
described above. A version number consists of a series of numbers,
separated by either periods or strings of letters. When comparing
version numbers, the numeric components will be compared
numerically, and the alphabetic components lexically. The following
are all valid version numbers, in no particular order:
1.5.1
1.5.2b2
161
3.10a
8.02
3.4j
1996.07.12
3.2.pl0
3.1.1.6
2g6
11g
0.960923
2.2beta29
1.13++
5.5.kw
2.0b1pl0
In fact, there is no such thing as an invalid version number under
this scheme; the rules for comparison are simple and predictable,
but may not always give the results you want (for some definition
of "want").
"""
component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE)
def parse(self, vstring):
# I've given up on thinking I can reconstruct the version string
# from the parsed tuple -- so I just store the string here for
# use by __str__
self.vstring = vstring
components = [
x for x in self.component_re.split(vstring) if x and x != "."
]
for i, obj in enumerate(components):
try:
components[i] = int(obj)
except ValueError:
pass
self.version = components
def __str__(self):
return self.vstring
def __repr__(self):
return "LooseVersion ('%s')" % str(self)
def _cmp(self, other):
if isinstance(other, str):
other = LooseVersion(other)
elif not isinstance(other, LooseVersion):
return NotImplemented
if self.version == other.version:
return 0
if self.version < other.version:
return -1
if self.version > other.version:
return 1
# Copied from setuptools/_distutils/spawn.py
def find_executable(executable, path=None):
"""Tries to find 'executable' in the directories listed in 'path'.
A string listing directories separated by 'os.pathsep'; defaults to
os.environ['PATH']. Returns the complete filename or None if not found.
"""
_, ext = os.path.splitext(executable)
if (sys.platform == "win32") and (ext != ".exe"):
executable = executable + ".exe"
if os.path.isfile(executable):
return executable
if path is None:
path = os.environ.get("PATH", None)
if path is None:
try:
path = os.confstr("CS_PATH")
except (AttributeError, ValueError):
# os.confstr() or CS_PATH is not available
path = os.defpath
# bpo-35755: Don't use os.defpath if the PATH environment variable is
# set to an empty string
# PATH='' doesn't match, whereas PATH=':' looks in the current directory
if not path:
return None
paths = path.split(os.pathsep)
for p in paths:
f = os.path.join(p, executable)
if os.path.isfile(f):
# the file exists, we have a shot at spawn working
return f
return None