GIF89a;
Direktori : /opt/imunify360/venv/lib64/python3.11/site-packages/defence360agent/utils/ |
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 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