#!/usr/libexec/platform-python
#
# Copyright (c) 2021, Oracle and/or its affiliates.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.
#
# This code 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
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, see <https://www.gnu.org/licenses/>.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.

""" Prints the stack of top cpu consumer processes """
# pylint: disable=W0703,E1101,C0103,C0302

__author__ = "Cesar Roque"
__copyright__ = ""
__credits__ = ["Jeffery Yoder", "Cesar Roque"]
__license__ = ""
__version__ = ""
__maintainer__ = "Cesar Roque"
__email__ = "cesar.roque@oracle.com"
__status__ = "Development"


import os
import sys
import logging as stdlogging
import sqlite3
import fcntl

from optparse import OptionParser, OptionGroup, TitledHelpFormatter
from datetime import datetime, timedelta
from multiprocessing import Process
from time import sleep
from socket import gethostname
from subprocess import Popen, PIPE
from string import printable
from glob import glob
from platform import uname


class Object(object):
    """ Dummy object """
    # pylint: disable=R0903
    pass


# noinspection PyBroadException
class FileLock(object):
    """ Implements a lock file """

    def __init__(self):

        self.file_path = None
        self.lock_file = None
        self.locked = False
        self.directory = os.path.join("/run/lock", os.path.split(sys.argv[0])[1])
        self.file_path = os.path.join(self.directory, "lock")

    def __exit__(self, exception_type, exception_value, traceback):
        self.clean()

    def set_lock(self):
        """ Creates the directory and lock file """
        if not os.path.isdir(self.directory):
            try:
                os.makedirs(self.directory, mode=0o700)
            except Exception as error:
                raise Error("Unable to create the directory: %s:\n%s" % (self.directory, error))

        self.lock_file = open(self.file_path, "w")
        ops = fcntl.LOCK_EX
        ops |= fcntl.LOCK_NB
        try:
            fcntl.flock(self.lock_file, ops)
            self.locked = True
        except (IOError, OSError) as e:
            raise Error("Another instace of topstack is already running")

    def is_locked(self):
        """ Verifies if already exists a lock """

        if not os.path.exists(self.file_path):
            return False

        self.set_lock()
        self.clean()

        return True

    def clean(self):
        """ Removes the file """
        # pylint: disable= W0703
        if self.locked is True:
            self.lock_file.close()
            try:
                os.remove(self.file_path)
            except Exception:
                pass


class Database(object):
    """ Class to handle the sqlite DB"""

    def __init__(self):
        self.connection = None
        self.cursor = None
        self.__create_db()

    def __create_db(self):
        # Creating in memory DB
        try:
            self.connection = sqlite3.connect(":memory:", check_same_thread=True)
            self.cursor = self.connection.cursor()
            self.cursor.execute(
                '''create table if not exists top_data (
                time timestamp,
                pid int,
                cpu real,
                cpu_num int,
                sys real,
                usr real,
                stack text,
                pstack text)''')
            self.cursor.execute(
                '''create table if not exists commands (pid int UNIQUE, command text)''')

        except Exception as error:
            raise Error("Unable to create DB:\n%s" % str(error))

    def run_query(self, query):
        """ Runs the query passed as parameter and returns, the query result """

        logging.trace("Running query: %s" % query)

        try:
            return self.cursor.execute(query).fetchall()
        except Exception as error:
            raise Error("Error on query run: %s\n%s" % (query, error))

    def delete_records(self, table, where=None):
        """ Deletes the records from a table """

        logging.debug("Deleting records on table: %s" % table)

        delete = "DELETE FROM %s" % table
        if where is not None:
            query = "%s WHERE %s" % where
        else:
            query = delete

        logging.trace("Running query: %s" % query)

        try:
            self.cursor.execute(query)
        except Exception as error:
            raise Error("Error on query run: %s\n%s" % (query, error))

    def garbage_collector(self):
        """ Deletes the old data from the DB """

        logging.trace("Running garbage collector ...")

        # Deletes the old records from DB
        self.delete_records("top_data")
        self.delete_records("commands")
        self.commit()

    def commit(self):
        """ Runs a commit statement on the DB """

        self.connection.commit()


class Error(Exception):
    """ Exception for general error """
    # pylint: disable=W0231

    def __init__(self, message, trace=None):
        logging.error(str(message))
        if trace is not None:
            logging.error(trace)
        sys.exit(1)


class Cmd(object):
    """ Runs an OS command """
    # pylint: disable=too-few-public-methods

    def __init__(self):
        self.out = None
        self.error = None
        self.code = None
        self.pid = None

    def run(self, command):
        """ Runs an OS command """
        # pylint: disable=W0110

        logging.trace(command)
        if ">" in command:
            log_file = command.split(">")[1].strip()
            command = command.split(">")[0].strip()
            log = open(log_file, 'w')
            log.flush()
            # noinspection PyUnusedLocal
            result = Popen(command.split(),
                           stdout=log,
                           stderr=log)
        else:
            result = Popen(command.split(),
                           stdout=PIPE,
                           stderr=PIPE)

            (out, error) = result.communicate()
            self.code = result.returncode
            self.pid = result.pid
            self.out = "".join(filter(lambda x: x in printable, out.decode('utf-8')))
            self.error = "".join(error.decode('utf-8'))


class Rotate(object):
    """ Class to handle the rotate of the output files """
    # pylint: disable=R0903, R0913, W0703

    def __init__(self, file_name, max_size, max_files, every=None, compress=True):
        from tempfile import NamedTemporaryFile

        self.file_name = file_name
        # If max size is 0 will disable this function
        self.max_size = max_size
        self.max_files = max_files
        self.compress = compress
        self.file_name = file_name
        self.every = every
        self.rotate_config = NamedTemporaryFile(delete=False)

        self.__prepare()
        self.rotate(True)

    def __prepare(self):
        """ Prepares the logrotate file and output directory """

        from os import makedirs, path as opath

        # Create directory if doesn't exist
        directory = opath.split(self.file_name)[0]
        if not opath.exists(directory):
            logging.debug("Creating directory")
            try:
                makedirs(directory)
            except Exception as error:
                Error("Unable to create directory: %s\n%s" % (directory, error))
        elif not opath.isdir(directory):
            Error("Path exists, but it's not a directory: %s" % directory)

        # Create logrotate file
        with open(self.rotate_config.name, "w") as rfile:
            rfile.write("\"%s\" {\n" % self.file_name)
            if not "el6" in uname()[2]:
                rfile.write("   su root root\n")
            if self.max_size > 0:
                rfile.write("   maxsize %sM\n" % self.max_size)
            rfile.write("   create 600 root root\n")
            rfile.write("   rotate %s\n" % self.max_files)
            rfile.write("   compress\n")
            rfile.write("   dateext\n")
            rfile.write("   dateformat -%s\n")
            rfile.write("   }\n")

    def rotate(self, force=False):
        """ Calls the logrotate binary """
        # pylint: disable=W0621, W0404

        from os import system, path
        from datetime import datetime

        if force and path.exists(self.file_name):
            system("logrotate -f %s" % self.rotate_config.name)
        elif path.exists(self.file_name):
            if self.every is None:
                # Running the logrorate binary passing the configuration file
                system("logrotate %s" % self.rotate_config.name)
            else:
                # Runs logrotate every X mins
                module = int(datetime.now().strftime("%M")) % self.every
                if module == 0:
                    # Running the logrorate binary passing the configuration file
                    system("logrotate %s" % self.rotate_config.name)


class Logging(object):
    """ Class to get logger """

    # pylint: disable=R0902

    def __init__(self, level=None):
        """ Init definition """

        self.trace_level = 5
        self.__logger = None
        self.__logging_enabled = False
        self.to_file = None
        self.to_console = None
        self.log_format = None
        self.date_format = None
        self.file_level = None
        self.console_level = None
        self.log_file = None
        self.level = level

        self.__get_logger()

    def __log_level(self, level):
        """ Returns the log level """
        # pylint: disable=R1705

        if level is None:
            return stdlogging.INFO
        else:
            if level == 0:
                return stdlogging.INFO
            elif level == 1:
                return stdlogging.DEBUG
            else:
                return self.trace_level

    def __get_params(self):
        """ Gets the information from conf module """
        # pylint: disable=W0703

        self.to_file = False
        self.to_console = True
        self.file_level = stdlogging.DEBUG
        self.console_level = stdlogging.INFO
        self.log_file = "/var/log/topstack.log"

        self.log_format = "%(asctime)s.%(msecs)d %(levelname)-8s\t%(message)s"
        self.date_format = "%Y-%m-%d %H:%M:%S"

    def __get_logger(self):
        """ Checks if logger is already setup """

        if not stdlogging.getLogger('').handlers:
            self.__get_params()
            if self.to_file or self.to_console:
                self.__start_logging()
                self.__logging_enabled = True
        else:
            self.__logger = stdlogging.getLogger('')
            self.__logging_enabled = True

    def __start_logging(self):
        """ Starts logging """

        stdlogging.addLevelName(self.trace_level, 'TRACE')
        log_formatter = stdlogging.Formatter(self.log_format,
                                             datefmt=self.date_format)
        self.__logger = stdlogging.getLogger()
        self.__logger.setLevel(self.trace_level)
        self.__logger.propagate = False

        if self.to_file:
            file_handler = stdlogging.FileHandler(self.log_file)
            file_handler.setFormatter(log_formatter)
            file_handler.setLevel(self.file_level)
            self.__logger.addHandler(file_handler)

        if self.to_console:
            console_handler = stdlogging.StreamHandler()
            console_handler.setFormatter(log_formatter)

            if self.level is not None:
                console_handler.setLevel(self.__log_level(self.level))
            else:
                console_handler.setLevel(self.console_level)
            self.__logger.addHandler(console_handler)

    def trace(self, mess):
        """ Prints trace message """
        self.__logger.log(5, str(mess))

    def info(self, mess):
        """ Prints info message """
        self.__logger.info(str(mess))

    def warning(self, mess):
        """ Prints warning message """
        self.__logger.warning(str(mess))

    def error(self, mess):
        """ Prints error message """
        self.__logger.error(str(mess))

    def debug(self, mess):
        """ Prints debug message """
        self.__logger.debug(str(mess))


# noinspection PyUnresolvedReferences
class Parser(object):
    """ Class to parse program arguments """

    # pylint: disable=too-few-public-methods, E1101

    def __init__(self):
        """ Init definition """
        self.__options = None
        self.verbose = None
        self.debug = None
        self.__parser = OptionParser(usage="%prog ",
                                     description="",
                                     formatter=TitledHelpFormatter())
        self.__parse_options()
        self.__print_debug()

    ######################################################
    def __add_options(self):
        """ Adds the required options to the parser """

        utilization_group = OptionGroup(self.__parser, "UTILIZATION OPTIONS")
        background_group = OptionGroup(self.__parser, "BACKGROUND OPTIONS")
        perf_group = OptionGroup(self.__parser, "PERF OPTIONS")

        self.__parser.add_option("--max-space",
                                 dest="max_space",
                                 type="int",
                                 default=85,
                                 help="Maximum disk space percentage used on log directory")

        self.__parser.add_option("--add",
                                 dest="additional",
                                 action="append",
                                 help="Additional file(s) (contents) to be "
                                      "added to the output file. "
                                      "Can be specified more than once.")

        self.__parser.add_option("--pp",
                                 dest="pidfiles",
                                 action="append",
                                 help="Additional file(s) (contents) to be "
                                      "added to the output file. Will dump the files from "
                                      "/proc/<PID>/<PIDFILE> where PID are the offending PIDs. "
                                      "Can be specified more than once.")

        self.__parser.add_option("--pstack",
                                 dest="pstack",
                                 action="store_true",
                                 default=False,
                                 help="Captures pstack per PID")

        self.__parser.add_option("--trace",
                                 dest="trace",
                                 action="store_true",
                                 default=False,
                                 help="Set logging level to trace")

        utilization_group.add_option("-c",
                                     dest="cpu",
                                     type="int",
                                     default=90,
                                     help="The threshold of % CPU utilization "
                                          "of a process. If a process is at or "
                                          "above this threshold, the stack(s) "
                                          "will be dumped.  The default value "
                                          "is 90.")

        utilization_group.add_option("-s",
                                     dest="sys",
                                     type="int",
                                     default=0,
                                     help="The threshold of % system utilization. "
                                          "If the process is at or above this "
                                          "threshold, the stack(s) will be dumped. "
                                          "The default value is 0.")

        utilization_group.add_option("-u",
                                     dest="usr",
                                     type="int",
                                     default=0,
                                     help="The threshold of % usr utilization. "
                                          "If the process is at or above this "
                                          "threshold, the stack(s) will be dumped. "
                                          "The default value is 0.")

        utilization_group.add_option("-e",
                                     dest="time",
                                     metavar="SECONDS",
                                     type="int",
                                     default=30,
                                     help="Elapsed time while the process is consuming high "
                                          "CPU resources. It's the # of seconds for each iteration. "
                                          "The default is 30 seconds.")

        perf_group.add_option("-p",
                              dest="perf",
                              action="store_true",
                              default=False,
                              help="Runs perf command if a match is found")

        perf_group.add_option("-f",
                              dest="perf_files",
                              type="int",
                              default=10,
                              help="Amount of perf.data files to keep (Default: 10)")

        perf_group.add_option("-r",
                              dest="perf_report",
                              action="store_true",
                              default=False,
                              help="Create the perf report using the existing "
                                   "perf data files on the log directory")

        background_group.add_option("-b",
                                    action="store_true",
                                    dest="background",
                                    default=False,
                                    help="Places topstack in background mode.  "
                                         "Data will be written to /var/oled/topstack_"
                                         "HOSTNAME.out.")

        background_group.add_option("-d",
                                    dest="directory_name",
                                    metavar="DIRECTORY",
                                    type="str",
                                    default="/var/oled/topstack",
                                    help="Specify the output directory other than the default "
                                         "/var/oled/topstack.")

        background_group.add_option("-t",
                                    dest="minutes",
                                    type="int",
                                    default=30,
                                    help="The number of minutes that "
                                         "topstack should run when in background mode. "
                                         "The default is 30.")

        background_group.add_option("--hs",
                                    dest="hardstop",
                                    type="int",
                                    default=0,
                                    help="Hard stop, the topstack process will end "
                                         "in these minutes after printing the first process "
                                         "matching the threshold ")

        background_group.add_option("-m",
                                    dest="max_size",
                                    type="int",
                                    default=1,
                                    help="The number maximum file size (MB) of the "
                                         "output file when in background mode. "
                                         "The default is 1Mb.")

        background_group.add_option("-n",
                                    dest="files_number",
                                    metavar="NUMBER_OF_FILES",
                                    type="int",
                                    default=5,
                                    help="How many files to retain when in background mode. "
                                         "The default is 5.")

        self.__parser.add_option_group(utilization_group)
        self.__parser.add_option_group(background_group)
        self.__parser.add_option_group(perf_group)

    def __parse_options(self):
        """ Parses the command line options """
        # pylint: disable=C0103, W0612, C0325

        self.__add_options()
        self.__options, args = self.__parser.parse_args()
        for name, value in self.__options.__dict__.items():
            vars(self)[name] = value

        if self.debug:
            self.verbose = 1

        if self.trace:
            self.verbose = 2

        self.file_name = os.path.realpath(
            os.path.join(self.directory_name, "topstack_%s.out" % gethostname()))
        self.perf_file_name = os.path.realpath(
            os.path.join(self.directory_name, "topstack-perf.data"))

        if self.minutes*60 < self.time:
            print("Elapsed time in seconds (-e) can't "
                  "be bigger than number of minutes (-t)")
            sys.exit(1)

    # noinspection PyShadowingNames
    def __print_debug(self):
        """ Print selected options """
        # pylint: disable=redefined-outer-name

        logging = Logging(self.verbose)

        logging.debug("Program options:")
        logging.debug("==============================")
        for name, value in self.__options.__dict__.items():
            logging.debug("Option: %-*s\tValue: %s" % (15, name, value))
        logging.debug("==============================")


# noinspection Pylint,PyBroadException,PyUnresolvedReferences
class TopStack(object):
    """
    Gets the stack of the top CPU consuming processes
    within additional troubleshooting data
    """
    # pylint: disable=E1101

    def __init__(self):
        self.cmd = Cmd()
        self.cmd_list = {}
        self.perf_ran = False
        self.perf_child = None
        self.main_limit = None
        self.pidstat_samples = 0
        self.perf_bin = None
        self.__create_processes()

    @staticmethod
    def __read_stack(pid):
        """
        Returns the stack for a process
        """

        try:
            with open(os.path.join("/proc", str(pid), "stack")) as stack:
                stack_data = stack.read()
        except Exception:
            return None

        return stack_data

    @staticmethod
    def __read_cmdline(pid):
        """
        Returns the contents of the cmdline for a process
        """
        # pylint: disable=W0110

        try:
            with open(os.path.join("/proc", str(pid), "cmdline")) as cmdline:
                cmdline_data = cmdline.read()
        except Exception:
            return None

        with open(os.path.join("/proc", str(pid), "cmdline")) as cmdline:
            cmdline_data = cmdline.read()

        return "".join(filter(lambda x: x in printable, cmdline_data))

    def __read_pstack(self, pid):
        """
        Returns the stack for a process
        """

        try:
            self.cmd.run('/usr/bin/pstack %s' % pid)
            if self.cmd.code != 0:
                return "Process is not present any longer"
        except Exception:
            return ""

        return self.cmd.out

    def run_perf(self, limit):
        """ Run perf command and save output """

        def timedelta_total_seconds(delta):
            """ Converts a delta to seconds """

            return (delta.microseconds + 0.0 +
                    (delta.seconds + delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6

        perf_limit = timedelta_total_seconds(limit - datetime.now())
        logging.trace("perf will run for %s seconds" % perf_limit)
        if perf_limit > 0:
            logging.debug("Running perf ...")
            perf_command = "%s record --output=%s -a -g sleep %s &> /dev/null" \
                           % (self.perf_bin, options.perf_file_name, perf_limit)
            # Starts perf collection
            os.system(perf_command)
            rotate_perf.rotate(True)

        logging.trace("Exiting from perf collector process")

    def perf_report(self):
        """ Runs perf report on the existing perf reports """

        logging.info("Generating perf reports")
        directory = os.path.split(options.perf_file_name)[0]
        perf_files = glob("%s/top*perf*gz" % directory)
        cmd = Cmd()
        for perf_file in perf_files:
            cmd.run("/usr/bin/gunzip %s" % perf_file)
            if cmd.code != 0:
                raise Error("Unable to uncompress file: %s\n%s" % (perf_file, cmd.error))

            uncompressed = perf_file.rsplit('.', 1)[0]
            cmd.run("%s report -i %s > %s.out" % (self.perf_bin, uncompressed, uncompressed))
            if cmd.code != 0:
                raise Error("Unable to generate report for file: %s:\n%s"
                            % (uncompressed, cmd.error))

            cmd.run("/usr/bin/gzip %s" % uncompressed)
            if cmd.code != 0:
                raise Error("Unable to compress file: %s\n%s" % (uncompressed, cmd.error))

    def __get_top_consumers(self, limit):
        """ Returns a list of processes  """

        def insert_values():
            """ Gathers needed info and saves it to the DB"""

            logging.trace("Will insert the values onto top_data table")

            # Calling perf if enabled
            if options.perf is True and \
                    self.perf_ran is False:
                # Creates a thread that will be run on background,
                # it will run perf
                self.perf_child = Process(target=self.run_perf, args=(limit,))
                self.perf_child.start()
                self.perf_ran = True

            # Calling function to read the process stack
            # If the the stack can't be retrieved the data
            # is not inserted

            stack = self.__read_stack(pid)
            if stack is None:
                return

            pstack = ""
            if options.pstack is True:
                pstack = self.__read_pstack(pid)

            # Saving data into DB
            # Fields: TIMESTAMP, PID, CPU%, COMMAND, STACK
            sql_ins = "insert into top_data(time, pid, cpu, cpu_num, sys, usr, stack, pstack) " \
                      "values ('%s', %s, %s, '%s',%s, '%s', '%s', '%s')" \
                      % (datetime.now(), pid, cpu,
                         int(fields[position.cpu_num]),
                         float(fields[position.sys]),
                         float(fields[position.usr]),
                         stack.replace("'", "''"),
                         pstack.replace("'", "''"))
            logging.trace(sql_ins)
            db.run_query(sql_ins)

        def decode_fields(line):
            """ Reads the pid header line and returns which field is in which position """
            # pylint: disable=W0201, C1801

            try:
                decode = Object()
                decode.pid = line.index("PID") - 1
                decode.usr = line.index("%usr") - 1
                decode.sys = line.index("%system") - 1
                decode.cpu = line.index("%CPU") - 1
                decode.cpu_num = line.index("CPU") - 1
                decode.cmd = line.index("Command") - 1
            except Exception as error:
                raise Error("Unable to read pidstat header: %s" % error)

            return decode

        # Main code

        position = None
        pids = []
        self.pidstat_samples += 1
        # Running pidstat command on command line
        logging.trace("Reading pidstat")

        if "el8" in uname()[2]:
            self.cmd.run('/usr/bin/pidstat -Hh 1 1')
            if self.cmd.code != 0:
                raise Error("%s" % self.cmd.error)
        else:
            self.cmd.run('/usr/bin/pidstat -h 1 1')
            if self.cmd.code != 0:
                raise Error("%s" % self.cmd.error)

        pid_output = self.cmd.out.splitlines()

        # Reading existing PIDs on DB
        if len(pid_output) > 3:
            pids = [col[0] for col in db.run_query("SELECT DISTINCT pid FROM top_data")]

        # Looping per every line from top output
        for process in pid_output:
            logging.trace("Processing line: %s" % process)
            fields = process.split()
            if process.startswith("#"):
                position = decode_fields(fields)

            if fields and \
                    process.strip()[0].isdigit():

                pid = int(fields[position.pid])
                cpu = float(fields[position.cpu])
                # Check if CPU value is above the threshold
                if cpu >= options.cpu:
                    # If pid is not in the DB, get the cmdline
                    if pid not in pids:
                        cmd = self.__read_cmdline(pid)
                        # If the cmdline was not retrieved, get the value
                        # from pidstat
                        if cmd is None or not len(cmd):
                            cmd = fields[position.cmd]
                        sql = "INSERT OR IGNORE INTO commands(pid, command) VALUES (%s, '%s')" \
                              % (pid, cmd.replace("'", "''"))
                        db.run_query(sql)
                    # Insert values on DB
                    insert_values()
                # if pid is already on the DB, if yes the value will be inserted
                elif pid in pids:
                    insert_values()

        db.commit()

    def __report_data(self, zzz):
        """ Deletes old data from the DB """

        def myprint(msg, out=None):
            """ Prints the string to the required output """

            if out is None:
                print(msg)
            else:
                out.write(msg)

        logging.debug("Printing output for iteration")

        if options.background:
            check_space()
            try:
                fd = os.open(options.file_name, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
                output = os.fdopen(fd, 'a')
            except Exception as error:
                raise Error("Unable to write to file: %s\nError: %s" % (options.file_name, error))
        else:
            output = None

        logging.debug("Reading DB to get the registers")

        # Getting the list of PIDs that should be reported, based on CPU consumption and time
        sql = "SELECT pid, sum(cpu) / %s, sum(sys) / %s, sum(usr) / %s FROM top_data GROUP BY pid ORDER BY pid ASC" % \
              (self.pidstat_samples, self.pidstat_samples, self.pidstat_samples)
        pids = []
        pid_info = {}
        hit = False

        result = db.run_query(sql)
        logging.trace("Query output:\n %s" % result)

        for row in result:
            if row[1] >= options.cpu:
                sql = "SELECT command FROM commands WHERE pid=%s LIMIT 1" % row[0]
                cmd = db.run_query(sql)[0]
                pids.append(row[0])
                pid_info[row[0]] = (row[1], row[2], row[3], cmd[0])

        # Print date header onto output
        myprint("\nzzz <%s> - Samples: %s\n" % (zzz.strftime("%m/%d/%Y %H:%M:%S"), self.pidstat_samples), output)

        hit_pids = []
        # Printing the report per every PID
        for pid in pids:
            if (options.sys > 0 and pid_info[pid][1] < options.sys) or \
                    (options.usr > 0 and pid_info[pid][2] < options.usr):
                continue

            hit = True
            hit_pids.append(pid)
            # Print PID header
            myprint("\nPID: %s CPU AVG(%%): %s SYS AVG(%%): %s  USR AVG(%%): %s COMMAND: %s\n"
                    % (str(pid).rjust(6),
                       round(pid_info[pid][0], 2),
                       round(pid_info[pid][1], 2),
                       round(pid_info[pid][2], 2),
                       pid_info[pid][3]),
                    output)

            # Query the data per PID, and prints every entry in DB
            sql = "SELECT cpu_num, cpu, sys, usr, stack, pstack " \
                  "FROM top_data WHERE pid=%s ORDER BY time ASC" % pid
            for row in db.run_query(sql):
                myprint("CPU: %s "
                        "CPU(%%): %s "
                        "SYS(%%): %s "
                        "USR(%%): %s\n"
                        "KERNEL STACK:\n%s"
                        % (str(row[0]).rjust(3),
                           round(row[1], 2),
                           round(row[2], 2),
                           round(row[3], 2),
                           row[4]),
                        output)

                if options.pstack is True:
                    myprint("USERSPACE STACK:\n%s" % row[5], output)

        if hit is True:
            # Checking if hardstop is set and setting the new time limit
            myprint("\n\n", output)
            if options.hardstop > 0:
                self.main_limit = datetime.now() + timedelta(minutes=options.hardstop)
                logging.info("Hard stop set to: %s" % self.main_limit)
                myprint("\nHard stop set to: %s" % self.main_limit, output)
                options.hardstop = 0

            # Handling additional required files
            if options.additional:
                additional_files = list(dict.fromkeys(options.additional))
                logging.trace("The next additional files need to be handled: %s"
                              % ",".join(additional_files))
            else:
                additional_files = []

            if options.pidfiles:
                additional_pids = list(dict.fromkeys(options.pidfiles))
                for additional in additional_pids:
                    for pid in pids:
                        additional_files.append(os.path.join("/proc", str(pid), additional))

            # Reads the files that were provided on the parameters
            # and writes its output to the output file
            for add_file in additional_files:
                if not os.path.exists(add_file):
                    add_info = "File is not present any longer"
                else:
                    logging.debug("Reading: %s" % add_file)
                    with open(add_file, 'r') as input_file:
                        add_info = input_file.read()

                myprint("%s:\n" % add_file, output)
                myprint(add_info, output)
                myprint("\n\n", output)
        else:
            logging.debug("Nothing to print for iteration")

        if options.background:
            output.close()

    def __create_processes(self):
        """ Creates the background processes """

        def collect():
            """ Control the data collection cycle """

            # The limit is defined by the parameter -i
            time_limit = datetime.now() + timedelta(seconds=options.time)
            self.pidstat_samples = 0

            while True:
                # Calling procedure to collect the data
                self.__get_top_consumers(time_limit)
                sleep(1)

                # Checking if the time for current interval has expired
                if datetime.now() > time_limit:
                    break

        def run_operations():
            """ Handles data gathering, delete old data, reports"""

            # Calculate the time when the script will end processing
            if options.background:
                # Fork is not 0 for the current parent process
                if os.fork() != 0:
                    return
                # Set lock to avoid other topstack processes to run
                lock.set_lock()
                # The value comes from -t option
                logging.info("This script will run for %s minutes" % options.minutes)
                logging.info("Will finish on: %s" % self.main_limit)
                logging.info("The data will be written to: %s" % options.directory_name)

            # Loop until time limit is reached
            while True:
                # Getting time that will be used during the report
                zzz = datetime.now()
                self.perf_ran = False
                # Running data collection
                collect()
                # Printing the data out
                self.__report_data(zzz)
                # Check if perf ran and wait for it
                if self.perf_ran is True:
                    self.perf_child.join()
                # Checking if time has ended
                if options.background:
                    # Calls the procedure to rotate the data files
                    rotate_stk.rotate()
                    if datetime.now() >= self.main_limit:
                        if options.perf:
                            self.perf_report()
                        logging.info("topstack finished processing")
                        break
                    # Deleting unneeded data from DB
                    db.garbage_collector()
                else:
                    break

            lock.clean()

        # Main code for create_processes

        # Setting time when the script should finish
        self.main_limit = datetime.now() + timedelta(minutes=options.minutes)

        if options.perf is True or options.perf_report is True:
            if "uek" in uname()[2]:
                self.perf_bin = "/usr/sbin/perf"
            else:
                self.perf_bin = "/usr/bin/perf"

        if options.perf_report is True:
            self.perf_report()
            return

        if options.background:
            try:
                # Creates a thread that will be run on background
                top_child = Process(target=run_operations)
                top_child.start()
                top_child.join()
            except Exception as error:
                raise Error("Unknown error has ocurred:\n%s" % error)
        else:
            # Run in foreground
            logging.info("Collecting data")
            run_operations()


def check_uid():
    """ Check if the script is run as root """
    # pylint: disable=C0325

    # Verify if the uid for the process owner is 0 (root)
    if os.geteuid() != 0:
        print("This script must be run as root")
        sys.exit(1)


def check_perf():
    """ Check if perf rpm is installed """

    # Verify if the perf rpm is installed
    if options.perf:
        if "uek" in uname()[2]:
            perf_bin = "/usr/sbin/perf"
        else:
            perf_bin = "/usr/bin/perf"
        cmd = Cmd()
        cmd.run("rpm -qf %s" % perf_bin)
        if cmd.code != 0:
            raise Error("%s is not installed, please "
                        "install it or run without -p option" % perf_bin)


def check_rpms():
    """ Check if the required rpms are installed """

    def check_rpm(rpm):
        """ Check if an rpm passed as arg is installed """

        # Verify if the perf rpm is installed
        cmd = Cmd()
        cmd.run("rpm -q %s" % rpm)
        if cmd.code != 0:
            raise Error("rpm not installed: %s" % rpm)

    check_rpm("sysstat")
    if options.perf:
        check_rpm("perf")

    if options.pstack:
        check_rpm("gdb")


def check_space():
    """ Check if the log directory fs has available space """
    # pylint: disable=E1101

    # noinspection PyBroadException
    def used(path):
        """ Checks the disk space usage on filesystem """

        cmd = Cmd()
        command = "/usr/bin/df -Ph %s" % path
        position = None
        try:
            cmd.run(command)
        except Exception:
            command = "/bin/df -Ph %s" % path
            try:
                cmd.run(command)
            except Exception as error_msg:
                raise Error("Unable to check disk usage: %s" % error_msg)

        if cmd.code != 0:
            raise Error("Unable to check disk usage: %s" % cmd.error)

        for line in cmd.out.splitlines():
            if "Use" in line:
                position = line.split().index("Use%")
                continue
            return int(line.split()[position].replace("%", ""))

    if options.background:
        logging.trace("Checking free space on filesystem")
        if not os.path.exists(options.directory_name):
            try:
                os.makedirs(options.directory_name)
            except Exception as error:
                raise Error("Unable to create directory: %s\n%s" %
                            (options.directory_name, error))

        used_space = used(options.directory_name)
        logging.trace("Used: %s, Threshold: %s" % (used_space, options.max_space))
        if used_space >= options.max_space:
            Error("Maximum filesystem space reached. Used: %s, "
                  "Threshold: %s" % (used_space, options.max_space))


if __name__ == '__main__':

    # Check if running with root user
    check_uid()

    # Reading parameters
    options = Parser()

    # Creating logger
    logging = Logging()

    # Check if there is another instance of topstack running
    # and setting lock for foreground processes
    lock = FileLock()
    if not lock.is_locked() and options.background is False:
        lock.set_lock()

    # Check rpms
    check_rpms()

    # Check file system space
    check_space()

    if options.perf_report is False:
        # Creating in memory DB
        db = Database()

        # Setting up logrotate for stack files
        if options.background:
            # noinspection PyUnresolvedReferences,PyUnresolvedReferences
            rotate_stk = Rotate(options.file_name, options.max_size, options.files_number, 5)

        # Setting up logrotate for perf files
        if options.perf:
            rotate_perf = Rotate(options.perf_file_name, 0, options.perf_files)

    # Running topstack operations
    topstack = TopStack()
