#!/usr/bin/env python2

import time
import logging
import os
import subprocess
import sys
import resource

import targetutils


GLOBAL_SLEEP = {
    # how long to wait after server start
    # can be high as it is not happening so often
    "sleep_after_server_start": 0.5,
}


# https://stackoverflow.com/questions/1689505/python-ulimit-and-nice-for-subprocess-call-subprocess-popen
# Not needed, kept for later
def preexec_fn():
    resource.setrlimit(resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))


class ServerManager(object):
    """
        Manages the server (the fuzzing target) process.
        This includes:
            - handling the process (start, stop)
            - getting some crash information
    """
    def __init__(self,
                 config,
                 threadId,
                 targetPort,
                 prependCmdline=None,
                 hideChildOutput=True):
        self.config = config
        self.process = None
        self.targetPort = targetPort
        self.isDisabled = False
        self.hideChildOutput = hideChildOutput

        targetutils.setupEnvironment(self.config)

        popenArg = targetutils.getInvokeTargetArgs(self.config, self.targetPort)
        if prependCmdline is None:
            self.popenArg = popenArg
        else:
            self.popenArg = []
            self.popenArg.extend(prependCmdline)
            self.popenArg.extend(popenArg)


    def start(self):
        """Start the server process."""
        if self.isDisabled:
            return

        if not os.path.isfile(self.config["target_bin"]):
            logging.error("Could not find target file: " +
                          str(self.config["target_bin"]))
            sys.exit(1)

        self._runTarget()

        if self.process is None:
            return False
        else:
            logging.info("Start server PID: " + str(self.process.pid))
            return True


    def stop(self):
        """Stop the server."""
        if self.isDisabled or self.process is None:
            return

        logging.info("Stop server PID: " + str(self.process.pid))
        try:
            self.process.terminate()
        except:
            logging.info("Could not terminate server - already crashed?")


    def restart(self):
        if self.isDisabled:
            return

        self.stop()
        time.sleep(GLOBAL_SLEEP["sleep_after_server_start"])
        self.start()
        time.sleep(GLOBAL_SLEEP["sleep_after_server_start"])


    def dis(self):
        self.isDisabled = True


    def _runTarget(self):
        """
        Actually start the server.
        """
        global GLOBAL_SLEEP
        logging.info("Starting server with args: " + str(self.popenArg))

        if self.hideChildOutput:
            # create devnull so we can us it to surpress output of the server
            # (2.7 specific)
            DEVNULL = open(os.devnull, 'wb')
            p = subprocess.Popen(
                self.popenArg,
                stdin=DEVNULL,
                stdout=DEVNULL,
                stderr=DEVNULL)
        else:
            # we want to see stdout / stderr
            p = subprocess.Popen(self.popenArg)

        # wait a bit so we are sure server is really started
        time.sleep( GLOBAL_SLEEP["sleep_after_server_start"] )
        logging.info("  Pid: " + str(p.pid) )

        # check if process is really alive (check exit code)
        returnCode = p.poll()
        logging.info("  Return code: " + str(returnCode))
        if returnCode is not None:
            # if return code is set (e.g. 1), the process exited
            return False

        self.process = p

        return True


    def getCrashInformation(self, crashData):
        """
        Return the data of the crash
        or None if it has not crashed (should not happen)
        """
        if self.isDisabled or self.process is None:
            msg = "Could not get crash information, process doesnt exist "
            msg += "(not started?): " + str(self.process)
            logging.warn(msg)
            return None

        try:
            if self.process.poll():
                logging.info("getCrashData(): get data, but server alive?!")
            else:
                logging.info("getCrashData(): ok, server is really crashed")
        except Exception as e:
            logging.warn("Could not poll process, strange: " + str(e))

        crashData.setCrashInformation(
            asanOutput=targetutils.getAsanOutput(self.config, self.process.pid),
            signum=0,
            exitcode=0,
            reallydead=self.process.poll(),
            serverpid=self.process.pid,
        )

        return crashData