%PDF-
%PDF-
Mini Shell
Mini Shell
"""
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
Zerion Mini Shell 1.0