#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TODO: 3 stop command-line jobs too, via kshm api?

import sys
import cgi
import fcntl
import fnmatch
import logging.handlers
import logging
import os
import signal
import socket
import subprocess
import traceback
import time
import inspect

import pkg_resources
import yaml
from yandextank.core.consoleworker import TankWorker, Status, Lock
from yandextank.core.tankcore import LockError
from yandextank.validator.validator import ValidationError

try:
    import simplejson as json
except ImportError:
    import json

from pkg_resources import resource_filename
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse

from util import DictToXml, DirectOutput, NotFoundError, run_subprocess_and_wait

logger = logging.getLogger('tankapi')

MIN_LOG_DURATION = 1  # minimal duration for logging slow functions


def get_callstack():
    """
        Get call stack, clean wrapper functions from it and present
        in dotted notation form
    """
    stack = inspect.stack(context=0)
    cleaned = [frame[3] for frame in stack if frame[3] != 'wrapper']
    return '.'.join(cleaned[1:])


def timeit(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stack = get_callstack()
        duration = time.time() - start_time
        if duration > MIN_LOG_DURATION:
            logger.info('Slow call of {} (stack: {}), duration {}'.format(func.__name__, stack, duration))
        return result

    return wrapper


def for_all_methods(decorator, exclude=None):
    if exclude is None:
        exclude = []
    """
        Decorator for all methods in a class, 
        shamelessly stolen from https://stackoverflow.com/questions/6307761
    """

    def decorate(cls):
        for attr in cls.__dict__:  # there's propably a better way to do this
            if callable(getattr(cls, attr)) and attr not in exclude:
                setattr(cls, attr, decorator(getattr(cls, attr)))
        return cls

    return decorate


@for_all_methods(timeit, exclude=['serve_forever'])
class LunaparkAPIServer(BaseHTTPRequestHandler):
    """
        Class with generic handlers for web API
    """

    PATH_TESTS = '/api/v1/tests/'
    PATH_TANK = '/api/v1/tank/'
    PATH_ADB = '/api/v1/devices.json'
    # aggregating proxy
    PATH_PROXY_JOB = '/proxy/api/job/'
    PATH_PROXY_TASK = '/proxy/api/task/'
    PATH_PROXY_MONITORING = '/proxy/api/monitoring/receiver/'
    PATH_PROXY = '/proxy/'

    HELP_URLS = ('/', '/help', '/help/')

    def response_help_page(self):
        """
            Вернуть страницу помощи
        """
        logger.info('Show help page')
        self.send_response(200, 'OK')
        self.send_header('Content-type', 'text/html')
        self.end_headers()

        helpfile = resource_filename(__name__, 'data/tank_api_help.html')

        help_file = open(helpfile, 'r')
        self.wfile.write(help_file.read())
        help_file.close()

    def response_200(self, message, response_type):
        logger.debug('Show message type {}: {}'.format(response_type, message))
        self.send_response(200, 'OK')
        self.send_header('Content-Type', response_type)
        self.end_headers()
        self.wfile.write(message.encode('utf-8'))

    def response_400(self, message):
        self.send_response(400, 'Bad Request')
        self.end_headers()
        self.wfile.write(message.encode('utf-8'))

    def do_GET(self):
        """
            GET requests processor
            Calls method according to requested URL or shows API description
        """
        try:
            if self.path in self.HELP_URLS:
                self.response_help_page()
                return
            client = {
                'address': self.client_address[0],
                'port': self.client_address[1],
                'user-agent': self.headers.get('user-agent', '')
            }
            if self.path.startswith('/favicon.ico'):
                return self.response_200('', 'text/plain')
            elif self.path.startswith(self.PATH_TANK):
                res = self.server.handle_tank(self.path[len(self.PATH_TANK):])
            elif self.path.startswith(self.PATH_TESTS):
                res = self.server.handle_tests(self.path[len(
                    self.PATH_TESTS):], client)
            elif self.path.startswith(self.PATH_ADB):
                res = self.server.handle_devices(self.path[len(self.PATH_ADB):])
            elif self.path.startswith(self.PATH_PROXY_JOB):
                res = self.server.proxy.handle_job_get(self.path[len(
                    self.PATH_PROXY_JOB):])
                self.response_200(res, 'text/plain')
                return
            elif self.path.startswith(self.PATH_PROXY):
                res = self.server.proxy.handle_proxy_get(self.path[len(
                    self.PATH_PROXY):])
                self.response_200(res, 'text/plain')
                return
            else:
                raise NotFoundError()

            res['success'] = True
            res['error'] = ''

            parsed = urlparse(self.path)
            splitted = os.path.splitext(parsed.path)
            res = self.server.convert_message(res, splitted[1][1:])
            self.response_200(res[0], res[1])
        except NotFoundError as e:
            logger.error('Incorrect url. Page not found, url-path: {}'.format(self.path))
            self.send_error(404, str(e))
            return
        except DirectOutput as e:
            logger.info('Send file: {}'.format(e))
            self.send_response(200, 'OK')
            self.send_header('Content-type', e.mimetype)
            self.end_headers()
            self.wfile.write(e.get_content())
        except Exception as e:
            logger.error("GET error: {}".format(traceback.format_exc(e)))
            res = {'success': False, 'error': str(e)}
            parsed = urlparse(self.path)
            splitted = os.path.splitext(parsed.path)
            res = self.server.convert_message(res, splitted[1][1:])
            self.response_200(res[0], res[1])

    def do_POST(self):
        try:
            parsed = urlparse(self.path)
            splitted = os.path.splitext(parsed.path)
            mtype = splitted[1][1:]
            length = int(self.headers.get_all('content-length')[0])
            res = {}
            if self.path.startswith(self.PATH_PROXY_JOB):
                try:
                    res = self.server.proxy.handle_job_post(
                        self.path[len(self.PATH_PROXY_JOB):], self.headers,
                        self.rfile.read(length))
                except Exception as exc:
                    logger.warning("Interrupting test: {}".format(traceback.format_exc(exc)))

                    self.response_400("Interrupted by API")
            elif self.path.startswith(self.PATH_PROXY_MONITORING):
                res = self.server.proxy.handle_job_mon_post(
                    self.path[len(self.PATH_PROXY_MONITORING):], self.headers, self.rfile.read(length)
                )
                mtype = 'txt'
            else:
                res = self.server.handle_start_test(self.path, self.headers, self.rfile)
            res = self.server.convert_message(res, mtype)
            self.response_200(res[0], res[1])
        except NotFoundError as e:
            logger.error('Incorrect url. Page not found, url-path: {}'.format(self.path))
            self.send_error(404, str(e))
            return
        except Exception as e:
            logger.error("POST error: {}".format(traceback.format_exc(e)))
            res = {'success': False, 'error': str(e)}
            splitted = os.path.splitext(self.path)
            res = self.server.convert_message(res, splitted[1][1:])
            self.response_200(res[0], res[1])

    @staticmethod
    def serve_forever(port, ipv6, tests_dir, lock_dir):
        APIServer(('', port), LunaparkAPIServer, ipv6, tests_dir, lock_dir).serve_forever()


# ==============================================================================


def parse_cfg_field(field_item):
    if isinstance(field_item, list):
        return {os.path.basename(item.filename): item.file.read() for item in field_item}
    else:
        return {os.path.basename(field_item.filename): field_item.file.read()}


@for_all_methods(timeit)
class APIServer(HTTPServer):
    '''
    Class with application logic
    '''

    CONFIG_FIELD = 'load.conf'
    AMMO_FIELD = 'ammo'
    OPTIONS_FIELD = 'options'
    CFG_PATCH_FIELD = 'cfg_patch'

    API_TESTS_BASE = '/api/v1/tests/'
    LOCK_DIR = os.getenv('LOCK_DIR', '/var/lock')

    def __init__(self,
                 server_address,
                 handler_class,
                 ipv6,
                 tests_dir,
                 lock_dir,
                 bind_and_activate=True):
        if ipv6:
            APIServer.address_family = socket.AF_INET6
            if server_address[0]:
                server_address = ("::", server_address[1])
        HTTPServer.__init__(self,
                            server_address,
                            handler_class,
                            bind_and_activate=bind_and_activate)
        if not os.path.exists(tests_dir):
            os.makedirs(tests_dir)
        self.tests_dir = tests_dir
        self.lock_dir = lock_dir
        self.tank_worker = None
        logger.info("Logic object created")

    def __convert_to_json(self, message):
        logger.debug('Convert to json message: {}'.format(message))
        json_message = json.dumps(message, sort_keys=True, indent=4)
        return json_message

    def __convert_to_xml(self, message):
        logger.debug('Convert to xml message: {}'.format(message))
        xml_message = ''
        try:
            xml_message = DictToXml({'result': message}).display()
        except Exception as ex:
            logger.error(traceback.format_exc(ex))
        logger.debug('xml message {}'.format(xml_message))
        return xml_message

    def convert_message(self, message, message_type):
        """
            Convert message to requested format and set content-type header
        """
        if message_type == 'xml':
            content_type = 'text/xml'
            message = self.__convert_to_xml(message)
        elif message_type == 'json':
            content_type = 'application/json'
            message = self.__convert_to_json(message)
        elif message_type == 'txt':
            content_type = 'text/plain'
        elif message_type == 'html':
            content_type = 'text/html'
            result = ""
            for val in message.keys():
                if val != 'success':
                    result += message[val]
            message = result
        else:
            logger.warning("Unknown mime-type sent as-is: {}".format(message_type))
            content_type = message_type
        return message, content_type

    def handle_tank(self, path):
        splitted = os.path.splitext(path)
        if splitted[0] == 'status':
            return self.get_tank_status()
        else:
            raise NotFoundError()

    def handle_devices(self):
        """
        adb devices handler
        :returns: list of attached to tank host android devices
        """
        result = {}
        try:
            devices = []
            p = subprocess.Popen(['adb devices'], shell=True, stdout=subprocess.PIPE)
            adb_results = p.stdout.read().decode('utf-8').split('\n')[1:]
            for device in adb_results:
                dev_id = device.split('\t')[0]
                if dev_id:
                    devices.append(dev_id)
            result['devices'] = devices
        except Exception as exc:
            result['error'] = 'unable to check adb devices {}'.format(exc)
        return result

    def handle_tests(self, path, client):
        parts = path.split('/')
        basename = os.path.splitext(path)[0]
        if basename == 'stop':
            if self.tank_worker:
                logger.info('Client {} is stopping test'.format(client))
                return self.stop_test()
            else:
                raise RuntimeError("No test running")
        else:
            test_id = parts[0]
            test_id = self.__check_test_id(test_id)
            if os.path.splitext(parts[1])[0] == 'status':
                return self.get_test_status(test_id)
            elif parts[1] == 'logs':
                raise DirectOutput(self.get_test_log(test_id, parts[2]))
            elif os.path.splitext(parts[1])[0] == 'logs':
                return self.get_test_logs(test_id)
            else:
                raise NotFoundError()

    def save_cgi_field_item(self, item):
        path = os.path.join(self.tests_dir, os.path.basename(item.filename))
        with open(path, 'w') as f:
            f.write(item.file.read().decode('utf-8'))
        return path

    def save_configs(self, field_item):
        """
        :returns: list of paths
        """
        if isinstance(field_item, list):
            return [self.save_cgi_field_item(item) for item in field_item]
        else:
            return [self.save_cgi_field_item(field_item)]

    def handle_start_test(self, path, headers, rfile):
        logger.info('User-Agent: {}'.format(headers['User-Agent']))
        logger.debug('Headers: {}'.format(headers))
        ctype, pdict = cgi.parse_header(headers.get_all('content-type')[0])
        logger.debug('ctype {}, pdict {}'.format(ctype, pdict))
        if not ctype == 'multipart/form-data':
            raise Exception('Incorrect content_type {}.'.format(ctype))
        if not os.path.exists('/tmp'):
            os.mkdir('/tmp')
            logger.info("Directory /tmp created ")
        form_data = cgi.FieldStorage(
            fp=rfile,
            headers=headers,
            environ={'REQUEST_METHOD': 'POST',
                     'CONTENT_TYPE': headers['Content-Type'], })
        cfg_paths = None
        ammo_path = None
        options = []
        patches = []
        files = []
        for field in form_data.keys():
            field_item = form_data[field]
            if field == self.CONFIG_FIELD:
                cfg_paths = self.save_configs(field_item)
            elif field == self.AMMO_FIELD and field_item.filename:
                ammo_path = self.save_cgi_field_item(field_item)
            elif field == self.OPTIONS_FIELD:
                options = [f.file.read().decode('utf-8') for f in field_item] \
                    if isinstance(field_item, list) \
                    else [field_item.file.read()]
            elif field == self.CFG_PATCH_FIELD:
                patches = [f.file.read().decode('utf-8') for f in field_item] \
                    if isinstance(field_item, list) \
                    else [field_item.file.read()]
            elif field_item.filename:
                files.append(self.save_cgi_field_item(field_item))
        if cfg_paths is None:
            raise RuntimeError('Error: load.conf is empty')
        api_message = self.start_test(files, cfg_paths, ammo_path, options, patches)
        return api_message

    def start_test(self, files, cfg_paths, ammo_path, cfg_options, patches):
        if self.tank_worker and self.tank_worker.status != Status.TEST_FINISHED:
            return {
                'success': False,
                'error': "Another test is already running"}

        overwrite_options = {
            'core': {
                'artifacts_base_dir': self.tests_dir,
                'lock_dir': self.lock_dir
            },
            'phantom': {
                'cache_dir': os.path.join(self.tests_dir, 'stpd-cache')
            }
        }
        if ammo_path:
            overwrite_options['phantom'].update({
                'use_caching': False,
                'ammofile': os.path.basename(ammo_path)
            })
        patches = [yaml.dump(overwrite_options)] + patches

        logger.info('Starting test')
        try:
            self.tank_worker = TankWorker(cfg_paths, cfg_options, patches, files=files, ammo_file=ammo_path,
                                          api_start=True)
        except (ValidationError, LockError) as e:
            return {
                'success': False,
                'error': e.message}
        self.tank_worker.start()
        return {'success': True, 'id': self.tank_worker.test_id}

    def get_test_logs(self, test_id):
        """
            Get filenames for artifacts of specified test
        """
        test_dir = os.path.abspath(os.path.join(self.tests_dir, test_id))
        files_in_test_dir = []
        for filename in os.listdir(test_dir):
            if not os.path.isdir(os.path.join(test_dir, filename)):
                files_in_test_dir.append(filename)
        result = {'files': files_in_test_dir}
        return result

    def get_test_log(self, test_id, name):
        filename = os.path.join(self.tests_dir, test_id, name)
        if not os.path.exists(filename):
            raise NotFoundError("File not found: {}/{}".format(test_id, name))
        return filename

    def __check_test_id(self, test_id):
        """
            Check if test with supplied test_id exists
            If test is missing throws exception
        """
        if not os.path.exists(self.tests_dir):
            raise Exception('Tests directory {} does not exist.'.format(self.tests_dir))
        test_dir = os.path.join(self.tests_dir, str(test_id))
        if not os.path.exists(test_dir):
            raise NotFoundError('Test #{} does not exist. '.format(test_id))
        return str(test_id)

    def stop_test(self):
        """
            Force stop current test
        """
        res = {}
        if self.tank_worker:
            res['id'] = self.tank_worker.test_id
            self.tank_worker.stop()
        return res

    def _is_active_test(self, test_id):
        if self.tank_worker and self.tank_worker.is_alive() and self.tank_worker.test_id == test_id:
            return True
        return False

    def get_test_status(self, test_id):
        """ Get info about running or completed test """
        result = {'status_code': 'NO TEST',
                  'left_time': None,
                  'exit_code': None,
                  'lunapark_id': None,
                  'tank_msg': None,
                  'lunapark_url': None,
                  'luna_id': None,
                  'luna_url': None}
        test_dir = os.path.join(self.tests_dir, str(test_id))
        logger.debug("API test dir: {}".format(test_dir))

        if self._is_active_test(test_id):
            logger.debug("Active test detected")
            result = self.tank_worker.get_status()
        elif not os.path.exists(test_dir):
            result['status_code'] = Status.TEST_NOT_FOUND
        else:
            logger.debug("Finished test detected")
            finish_status_file = os.path.join(test_dir, TankWorker.FINISH_FILENAME)
            if os.path.exists(finish_status_file):
                with open(finish_status_file) as f:
                    result = yaml.load(f)
            else:
                msg = '{} file not found'.format(finish_status_file)
                logger.warning(msg)
                result['status_code'] = Status.TEST_FINISHED
                result['tank_msg'] = msg

        logger.info(result)
        return result

    def __get_tank_hostname(self):
        return socket.getfqdn()

    def __is_test_session_running(self):
        if self.tank_worker:
            return self.tank_worker.status != Status.TEST_FINISHED
        else:
            logger.info("Checking if tank is available")
            return bool(Lock.is_locked(self.LOCK_DIR))

    def __is_test_session_preparing(self):
        return (self.tank_worker and self.tank_worker.is_alive() and
                self.tank_worker.status == Status.TEST_PREPARING)

    def __get_fs_usage(self):
        result = {}
        process = run_subprocess_and_wait(
            'df -l -BG -x fuse -x tmpfs -x devtmpfs',
            shell=False)
        fs_info = process.stdout.read().decode('utf-8').split('\n')
        for fs_item in fs_info[1:]:
            if fs_item:
                # разделяем по пробелам и получаем нужные данные по файловой
                # системе
                fs_item_info = [i for i in fs_item.split(' ') if i.strip()]
                result[fs_item_info[0]] = {
                    'size': fs_item_info[1],
                    'used': fs_item_info[2],
                    'avail': fs_item_info[3],
                    'use_p': fs_item_info[4],
                    'mount': fs_item_info[5],
                }
        return result

    def __get_users_activity(self):
        """
            Get info about users logged in via ssh
        """
        result = []
        process = run_subprocess_and_wait("w -h | awk '{print $1, $5}'",
                                          shell=True)
        users_activity_info = process.stdout.read().decode('utf-8').split('\n')
        for item in users_activity_info:
            if item.strip():
                user_info = item.split(' ')
                if user_info[1] == '.':
                    result.append({user_info[0]: 'not'})
                else:
                    result.append({user_info[0]: user_info[1]})
        return result

    def __get_processes(self):
        """
            Get info about top CPU comsuming processes
        """
        try:
            result = []
            process = run_subprocess_and_wait(
                "top -n1 -d2 -b | grep -A 5 'PID' | "
                "tail -n6 | awk '{print $1, $2, $9, $12}'",
                shell=True)
            processes_info = process.stdout.readlines()
            processes_info.pop(0)
            for item in processes_info:
                if item:
                    command_info = item.strip().split(' ')
                    result.append({
                        'command': command_info[3],
                        'user': command_info[1],
                        'cpu': float(command_info[2].replace(',', '.')),
                        # FIXME replace is a workaround for trusty's top
                        'pid': int(command_info[0])
                    })
        except:
            return None
        return result

    def __get_lunapark_ids(self):
        jobs = []
        for filename in os.listdir(self.LOCK_DIR):
            if fnmatch.fnmatch(filename, 'lunapark_*.lock'):
                full_name = os.path.join(self.LOCK_DIR, filename)
                logger.debug("Getting job info from lock file: {}".format(full_name))

                try:
                    with open(full_name, 'r') as f:
                        info = yaml.load(f)
                    jobs.append(info.get('uploader', {}).get('meta', {}).get('jobno'))
                except Exception as exc:
                    logger.warning("Failed to load info from lock {}: {}".format(full_name, exc))
        return jobs

    def get_tank_status(self):
        """
            Returns tank status
            @return: dict with the following fields:
                "is_testing" - is there a running test on this tank True/False
                "is_preparing" - is there a test in prepairing stage
                                 on this tank True/False
                "left_time" - estimated completion time for running test,
                              in seconds
                "name": - tank hostname
                "users_activity" - dict with info about logged via ssh users
                                   format: { login: time_since_last_action }
                "processes" - dict with info about top CPU processes
                "fs_use" - dict with info about file system usage
        """
        is_testing = self.__is_test_session_running()
        if is_testing:
            if self.tank_worker:
                test_id = self.tank_worker.test_id
            else:
                test_id = Lock.running_ids()
        else:
            test_id = None
        result = {"is_testing": is_testing,
                  "current_test": test_id,
                  "is_preparing": self.__is_test_session_preparing(),
                  "name": self.__get_tank_hostname(),
                  "users_activity": self.__get_users_activity(),
                  "processes": self.__get_processes(),
                  "fs_use": self.__get_fs_usage(),
                  "lunapark_ids": self.__get_lunapark_ids(),
                  "meta_tests": [],
                  "version": 'YandexTank/{}'.format(pkg_resources.require('yandextank')[0].version)}
        return result


def start_lunapark_api_server(port, ipv6, tests_dir, lock_dir):
    logger.info("Run tank API server, port: {}".format(port))
    if ipv6:
        logger.info("Using IPv6")
    LunaparkAPIServer.serve_forever(int(port), ipv6, os.path.abspath(tests_dir),
                                    os.path.abspath(lock_dir))


def init_logger():
    """
        Initial logger setup
        Set logging level for stdout
    """
    logger.setLevel(logging.DEBUG)

    fmt_verbose = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s %(filename)s:%(lineno)d\t%(message)s")
    stderr_hdl = logging.StreamHandler(sys.stderr)
    stderr_hdl.setFormatter(fmt_verbose)
    stderr_hdl.setLevel(logging.WARN)

    logger.addHandler(stderr_hdl)

    logging.info("Logging stream done")
    logging.debug("Debug msg 0")


def add_file_handler_to_logger(log_path,
                               max_file_path=200,
                               log_chains_number=3):
    logger_message = "Add a file logger handler, file: {logging_path}," \
                     "max file chain size: {max_file_path}," \
                     "chains number: {log_chains_number}"
    logger.info(logger_message.format(**{
        'logging_path': log_path,
        'max_file_path': max_file_path,
        'log_chains_number': log_chains_number
    }))
    logs_file_handler = logging.handlers.RotatingFileHandler(
        log_path,
        maxBytes=1024 * 1024 * int(max_file_path),
        backupCount=log_chains_number)
    logs_file_handler.setLevel(logging.DEBUG if os.environ.get("DEBUG", 0) else
                               logging.INFO)
    formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s %(filename)s:%(lineno)d\t%(message)s")
    logs_file_handler.setFormatter(formatter)

    logger.addHandler(logs_file_handler)
    logger.info("Logging file done: {}".format(log_path))


def get_parameters():
    sys.path.append('/usr/share/tankapi')
    import server_defaults

    from optparse import OptionParser

    parser = OptionParser()
    parser.add_option('-p',
                      '--port',
                      type='int',
                      action='store',
                      default=server_defaults.port,
                      help='Port for tank API server.')
    parser.add_option('-w',
                      '--workdir',
                      type='string',
                      action='store',
                      default=server_defaults.workdir,
                      help='Path to tank API server workdir.')
    parser.add_option('-d',
                      '--daemonize',
                      action='store_true',
                      default=server_defaults.daemonize,
                      help='Run tank API server in background')

    parser.add_option('-6',
                      '--ipv6',
                      action='store_true',
                      default=server_defaults.ipv6,
                      help='use ipv6?')

    parser.add_option('-l',
                      '--lock-dir',
                      action='store',
                      default=server_defaults.lock_dir,
                      help='Directory to store lock file')

    script_options = parser.parse_args()[0]
    return (script_options.port, script_options.workdir,
            script_options.daemonize, script_options.ipv6, script_options.lock_dir)


class Daemon(object):
    def __init__(self,
                 pidfile_name,
                 stdin='/dev/null',
                 stdout='/dev/null',
                 stderr='/dev/null'):

        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile_name = pidfile_name
        self.pidfile = open(pidfile_name, 'a')

        def cleanup_handler(signum, frame):
            try:
                fcntl.lockf(self.pidfile, fcntl.LOCK_UN)
            except IOError as err:
                logger.error(err)
                os._exit(1)
            self.pidfile.close()
            os.remove(self.pidfile_name)
            os._exit(0)

        signal.signal(signal.SIGTERM, cleanup_handler)

    def daemonize(self, ch_to="/"):
        try:
            pid = os.fork()
            if pid > 0:
                # exit first parent
                os._exit(0)
        except OSError as err:
            sys.stderr.write("fork #1 failed: {errno} ({error})\n".format(**{
                'errno': err.errno,
                'error': err.strerror
            }))
            sys.exit(1)

        # decouple from parent environment
        os.chdir(ch_to)
        os.setsid()
        os.umask(0)

        # do second fork
        try:
            pid = os.fork()
            if pid > 0:
                # exit from second parent
                os._exit(0)
        except OSError as err:
            sys.stderr.write("fork #2 failed: {errno} ({error})\n".format(**{
                'errno': err.errno,
                'error': err.strerror
            }))

        # redirect standard file descriptors
        sys.stdout.flush()
        sys.stderr.flush()
        si = open(self.stdin, 'r')
        so = open(self.stdout, 'a+')
        se = open(self.stderr, 'a+', 0)
        os.dup2(si.fileno(), sys.stdin.fileno())
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())

        pid = str(os.getpid())
        try:
            fcntl.lockf(self.pidfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError:
            raise

        self.pidfile.truncate(0)
        self.pidfile.write("{}".format(pid))
        self.pidfile.flush()
        os.fsync(self.pidfile.fileno())


def detect_ipv6():
    """ detect if local machine has external ipv6 address
    returns: bool"""
    try:
        hostname = socket.getfqdn()
        lookup = socket.getaddrinfo(hostname, 80)
        # ipv6 from here: https://en.wikipedia.org/wiki/IPv6_address
        local_ips = ['::1', '127.0.0.1', '0:0:0:0:0:0:0:1', 'fe80::']
        for line in lookup:
            family, _, _, _, ip = line
            if family == socket.AF_INET6 and ip[0] not in local_ips:
                logger.info('Found external ipv6 on machine: {}'.format(ip[0]))
                return True
    except:
        logger.error('Unable to automatically find external ipv6, continue w/ defaults')
        return False
    logger.warning('ipv6 external address not found, continue w/ v4')
    return False


def main():
    init_logger()
    port, workdir, daemonize, ipv6, lock_dir = get_parameters()
    if not ipv6:
        ipv6 = detect_ipv6()

    signal.signal(signal.SIGINT, signal.SIG_DFL)
    if daemonize:
        pidfile_name = os.path.join(workdir, 'tank_api_server.pid')
        d = Daemon(pidfile_name)
        d.daemonize(workdir)
    log_file_path = os.path.join(workdir, 'server.log')
    add_file_handler_to_logger(log_file_path)
    tests_dir = os.path.join(workdir, 'tests')
    if not os.path.exists(tests_dir):
        logger.info("Tests folder does not exist. Trying to create it."
                    " Path: {}".format(tests_dir))
        os.makedirs(tests_dir)
    start_lunapark_api_server(port, ipv6, tests_dir, lock_dir)


if __name__ == "__main__":
    main()
