Mini Shell
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
import io
import logging
import os
import signal
import sys
import time
import traceback
import psutil
from lvestats.lib.commons.func import reboot_lock
from lvestats.lib import config, dbengine
from lvestats.lib.commons.func import LVEVersionError, get_lve_version
from lvestats.lib.commons.logsetup import setup_logging
DEV_NULL = '/dev/null'
PIDFILE = ""
class EnvironmentException(Exception):
def __init__(self, message):
super().__init__()
self.message = message
def get_process_pid():
"""
Check if lvestats already running
:return int|None: None - if no process found; pid - if some lvestats-server found
"""
if PIDFILE and os.path.isfile(PIDFILE):
try:
with open(PIDFILE, 'r', encoding='utf-8') as f:
pid = int(f.read().strip())
os.kill(pid, 0) # try to send some
return pid
except (IOError, OSError):
return None # No pidfile or no process found
def stop_server():
exit_code = 0
def kill_process(_pid: int, _signal: signal.Signals):
"""Kill process by pid by sending a specific signal to it"""
try:
os.kill(_pid, _signal)
except (OSError, ProcessLookupError):
log.info("Process with pid '%d' is already dead", _pid)
def on_sigusr2(_proc: psutil.Process):
"""callback for psutil.wait_procs()"""
log.info("Signal 'SIGUSR2' sent to child process: %s", _proc)
def on_terminate(_proc: psutil.Process):
"""callback for psutil.wait_procs()"""
log.info("Signal 'SIGTERM' sent to child process: %s", _proc)
log = setup_logging({}, caller_name='stop_server')
pid = get_process_pid()
if pid is None:
exit_code = 1
else:
process = psutil.Process(pid)
childs = process.children(recursive=True)
with reboot_lock(timeout=60 * 10):
for child in childs:
# There may be stored absent childs, so we need to kill
# only existing processes
if psutil.pid_exists(child.pid):
kill_process(child.pid, signal.SIGUSR2)
_, alive = psutil.wait_procs(childs, timeout=3, callback=on_sigusr2)
for p in alive:
kill_process(p.pid, signal.SIGTERM)
_, alive = psutil.wait_procs(childs, timeout=3, callback=on_terminate)
kill_process(pid, signal.SIGTERM)
time.sleep(0.15)
for child in alive:
kill_process(child.pid, signal.SIGKILL)
return exit_code
def setup_default_exception_hook():
def hook(_type, _ex, _trace):
sio = io.StringIO()
traceback.print_tb(_trace, file=sio)
msg = f"Uncaught exception {_type}\nmessage='%s'\n:%s"
logging.error(msg, str(_ex), sio.getvalue())
sio.close()
sys.__excepthook__(_type, _ex, _trace)
sys.excepthook = hook
def run_main(cnf, singleprocess, plugins, profiling_log, times_):
from lvestats import main # pylint: disable=import-outside-toplevel,redefined-outer-name
main.main(cnf, singleprocess, plugins, profiling_log, times_)
def sigterm_handler(signum, frame):
log = logging.getLogger('sigterm_handler')
log.info('SIGTERM handler. Shutting Down.')
os._exit(0)
def _check_db_connection(cnf, log_):
"""
Check whether database connection can be
established and db schema is ok.
Raises exception if database is broken.
:type cnf: dict
:type log_: logging.Logger
:raises: EnvironmentException
"""
log_.debug('Check for running SQL server')
try:
engine = dbengine.make_db_engine(cnf)
engine.execute("SELECT 1;")
validation = dbengine.validate_database(engine)
if validation['column_error'] or validation['table_error']:
sys.exit(1)
except Exception as ex:
msg = "Error occurred during connecting to SQL server:"
log_.fatal(str(ex))
log_.exception(ex)
raise EnvironmentException(f"\n{msg}\n{ex}\n") from ex
def _check_valid_lve_version(log_):
"""
Check for possible misconfiguration
if cpu speed is reported as 0 with /proc/cpuinfo
and lve version <= 4. Raises exception, if so.
:type log_: logging.Logger
:raises: EnvironmentException
"""
log_.debug('Check for valid LVE version')
lve_version = get_lve_version()
if lve_version <= 4:
msg = "LVE version <= 4"
log_.fatal('LVE version <= 4. Please, update.')
raise EnvironmentException(f"\n{msg}\n")
def _check_running_process(log_):
"""
Check if another instance of lve-stats is
already running. Raises exception, if so.
:param log_: logging.Logger
:raises: EnvironmentException
"""
log_.debug('Check for running lvestats-server')
rc = get_process_pid()
if rc:
msg = f"Lvestats-server already running with pid {rc}. Exiting"
log_.warning(msg)
raise EnvironmentException(msg)
def _is_environment_ok(cnf, log_):
"""
Checks whether system environment works fine.
Return True if ok, False otherwise.
:type cnf: dict
:type log_: logging.Logger
:rtype: bool
"""
try:
_check_running_process(log_)
_check_db_connection(cnf, log_)
_check_valid_lve_version(log_)
except EnvironmentException as ex:
sys.stderr.write(ex.message)
sys.stderr.flush()
return False
return True
def daemonize(cnf, singleprocess, plugins, profiling_log, times):
def fork():
try:
return os.fork()
except OSError as e:
raise RuntimeError(f"{e.strerror} [{e.errno}]") from e
setup_logging(cnf, console_level=logging.CRITICAL)
setup_default_exception_hook()
log_ = logging.getLogger('server')
# check for issues with environment
if not _is_environment_ok(cnf, log_):
sys.exit(1)
log_.debug('Starting server')
pid = fork()
if pid:
log_.debug('First fork, pid=%d', pid)
time.sleep(0.2)
os._exit(0)
os.setsid()
signal.signal(signal.SIGTERM, sigterm_handler)
pid = fork()
if pid:
log_.debug('Second fork, pid=%d', pid)
if PIDFILE:
log_.debug('Writing pid to file %s', PIDFILE)
with open(PIDFILE, 'w', encoding='utf-8') as pidfile:
pidfile.write(str(pid))
# exit parent process
log_.debug('Child daemon fork ok')
os._exit(0)
os.nice(10)
os.setpgrp()
os.chdir('/')
previous_umask = os.umask(0)
sys.stdout.flush()
sys.stderr.flush()
# pylint: disable=consider-using-with,unspecified-encoding
si = open(DEV_NULL, 'r')
so = open(DEV_NULL, 'a+') # read + write allowed
se = open(DEV_NULL, 'a+') # read + write allowed
# pylint: enable=consider-using-with,unspecified-encoding
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
log_.info('Starting main() in daemon')
os.umask(previous_umask)
run_main(cnf, singleprocess, plugins, profiling_log, times)
def process_opts(cnf, _opts, _times):
if _opts.action == 'stop':
sys.exit(stop_server())
elif _opts.action == 'restart': # stop and start
stop_server()
daemonize(cnf, _opts.singleprocess, _opts.plugins, _opts.profiling_log, _times)
elif _opts.action == 'start':
if _opts.nodaemon:
debug_mode = logging.DEBUG if _opts.debug_mode else logging.INFO
setup_logging(cnf, console_level=debug_mode, file_level=debug_mode)
setup_default_exception_hook()
run_main(cnf, _opts.singleprocess, _opts.plugins, _opts.profiling_log, _times)
else:
daemonize(cnf, _opts.singleprocess, _opts.plugins, _opts.profiling_log, _times)
def main(opts):
if not opts.pidfile:
print("--pidfile should be specified")
sys.exit(1)
else:
global PIDFILE
PIDFILE = opts.pidfile
times = None
if opts.times:
times = int(opts.times)
cfg = None
try:
cfg = config.read_config()
except config.ConfigError as ce:
ce.log_and_exit()
try:
process_opts(cfg, opts, times)
except LVEVersionError as lve_error:
config.log.error(str(lve_error))
sys.exit(1)
Zerion Mini Shell 1.0