%PDF- %PDF-
Direktori : /opt/imunify360/venv/lib/python3.8/site-packages/defence360agent/utils/ |
Current File : //opt/imunify360/venv/lib/python3.8/site-packages/defence360agent/utils/common.py |
""" This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. Copyright © 2019 Cloud Linux Software Inc. This software is also available under ImunifyAV commercial license, see <https://www.imunify360.com/legal/eula> """ 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 def coalesce_calls(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. """ call_time = float("-inf") delayed_call = None """ 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): nonlocal delayed_call 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.""" nonlocal delayed_call, call_time assert delayed_call logger.info("Schedule call %s", call_repr) call_time, delayed_call = loop.time(), 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 > (call_time + period): # call immediately if delayed_call is not None: # get string representation for logs # before cancelling the call old_delayed_call_repr = str(delayed_call) delayed_call.cancel() old_delayed_call, delayed_call = 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, call_time, now, ) logger.info( "Satisfy the call request soon: %s" ". No calls in more than %r seconds since the start", call_repr, period, ) delayed_call = loop.call_soon(call_delayed, coro, args, kwargs) elif call_time <= now <= (call_time + period): delay = (call_time + period) - now if 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 delayed_call is None logger.info( "Delay call request: %s for ~%.2f seconds" ". Enforcing one call per %r seconds limit", call_repr, delay, period, ) delayed_call = loop.call_at( 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, call_time, now, ) return wrapper return decorator 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