diff --git a/.gitignore b/.gitignore
index feffe32..621d21f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
 # App files
 pykms_logserver.log*
+pykms_logclient.log*
+pykms_newlines.txt*
+pykms_config.pickle*
+etrigan.log*
 
 # Byte-compiled / optimized / DLL files
 __pycache__/
diff --git a/py-kms/Etrigan.py b/py-kms/Etrigan.py
new file mode 100644
index 0000000..1d90a53
--- /dev/null
+++ b/py-kms/Etrigan.py
@@ -0,0 +1,610 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import atexit
+import errno
+import os
+import sys
+import time
+import signal
+import logging
+import argparse
+from collections import Sequence
+
+__version__             = "0.1"
+__license__             = "MIT License"
+__author__              = u"Matteo ℱan <SystemRage@protonmail.com>"
+__copyright__           = "© Copyright 2020"
+__url__                 = "https://github.com/SystemRage/Etrigan"
+__description__         = "Etrigan: a python daemonizer that rocks."
+
+path = os.path.dirname(os.path.abspath(__file__))
+
+class Etrigan(object):
+        """
+        Daemonizer based on double-fork method
+        --------------------------------------
+        Each option can be passed as a keyword argument or modified by assigning
+        to an attribute on the instance:
+        
+        jasonblood = Etrigan(pidfile,
+                             argument_example_1 = foo,
+                             argument_example_2 = bar)
+        
+        that is equivalent to:
+        
+        jasonblood = Etrigan(pidfile)
+        jasonblood.argument_example_1 = foo
+        jasonblood.argument_example_2 = bar
+
+        Object constructor expects always `pidfile` argument.
+        `pidfile`
+                Path to the pidfile.
+        
+        The following other options are defined:
+        `stdin`
+        `stdout`
+        `stderr`
+                :Default: `os.devnull`
+                        File objects used as the new file for the standard I/O streams
+                        `sys.stdin`, `sys.stdout`, and `sys.stderr` respectively.
+                        
+        `funcs_to_daemonize`
+                :Default: `[]`
+                        Define a list of your custom functions
+                        which will be executed after daemonization.
+                        If None, you have to subclass Etrigan `run` method.
+                        Note that these functions can return elements that will be
+                        added to Etrigan object (`etrigan_add` list) so the other subsequent
+                        ones can reuse them for further processing.
+                        You only have to provide indexes of `etrigan_add` list,
+                        (an int (example: 2) for single index or a string (example: '1:4') for slices)
+                        as first returning element.
+
+        `want_quit`
+                :Default: `False`
+                        If `True`, runs Etrigan `quit_on_start` or `quit_on_stop`
+                        lists of your custom functions at the end of `start` or `stop` operations.
+                        These can return elements as `funcs_to_daemonize`.
+
+        `logfile`
+                :Default: `None`
+                        Path to the output log file.
+
+        `loglevel`
+                :Default: `None`
+                        Set the log level of logging messages.
+
+        `mute`
+                :Default: `False`
+                        Disable all stdout and stderr messages (before double forking).
+
+        `pause_loop`
+                :Default: `None`
+                        Seconds of pause between the calling, in an infinite loop,
+                        of every function in `funcs_to_daemonize` list.
+                        If `-1`, no pause between the calling, in an infinite loop,
+                        of every function in `funcs_to_daemonize` list.                   
+                        If `None`, only one run (no infinite loop) of functions in
+                        `funcs_to_daemonize` list, without pause.       
+        """
+        
+        def __init__(self, pidfile,
+                     stdin = os.devnull, stdout = os.devnull, stderr = os.devnull,
+                     funcs_to_daemonize = [], want_quit = False,
+                     logfile = None, loglevel = None,
+                     mute = False, pause_loop = None):
+
+                self.pidfile = pidfile                
+                self.funcs_to_daemonize = funcs_to_daemonize
+                self.stdin = stdin
+                self.stdout = stdout
+                self.stderr = stderr
+                self.logfile = logfile
+                self.loglevel = loglevel
+                self.mute = mute
+                self.want_quit = want_quit
+                self.pause_loop = pause_loop
+                # internal only.               
+                self.homedir = '/'
+                self.umask = 0o22
+                self.etrigan_restart, self.etrigan_reload = (False for _ in range(2))
+                self.etrigan_alive = True
+                self.etrigan_add = []
+                self.etrigan_index = None
+                # seconds of pause between stop and start during the restart of the daemon.
+                self.pause_restart = 5
+                # when terminate a process, seconds to wait until kill the process with signal.
+                # self.pause_kill = 3
+                
+                # create logfile.
+                self.setup_files()
+
+        def handle_terminate(self, signum, frame):
+                if os.path.exists(self.pidfile):
+                        self.etrigan_alive = False
+                        # eventually run quit (on stop) function/s.
+                        if self.want_quit:
+                                if not isinstance(self.quit_on_stop, (list, tuple)):
+                                        self.quit_on_stop = [self.quit_on_stop]
+                                self.execute(self.quit_on_stop)
+                        # then always run quit standard.
+                        self.quit_standard()
+                else:
+                        self.view(self.logdaemon.error, self.emit_error, "Failed to stop the daemon process: can't find PIDFILE '%s'" %self.pidfile)
+                sys.exit(0)
+
+        def handle_reload(self, signum, frame):
+                self.etrigan_reload = True
+
+        def setup_files(self):
+                self.pidfile = os.path.abspath(self.pidfile)
+                
+                if self.logfile is not None:                     
+                        self.logdaemon = logging.getLogger('logdaemon')
+                        self.logdaemon.setLevel(self.loglevel)
+                        
+                        filehandler = logging.FileHandler(self.logfile)
+                        filehandler.setLevel(self.loglevel)
+                        formatter = logging.Formatter(fmt = '[%(asctime)s] [%(levelname)8s] --- %(message)s',
+                                                      datefmt = '%Y-%m-%d %H:%M:%S')
+                        filehandler.setFormatter(formatter)
+                        self.logdaemon.addHandler(filehandler)
+                else:
+                        nullhandler = logging.NullHandler()
+                        self.logdaemon.addHandler(nullhandler)
+
+        def emit_error(self, message, to_exit = True):
+                """ Print an error message to STDERR. """
+                if not self.mute:
+                        sys.stderr.write(message + '\n')
+                        sys.stderr.flush()
+                if to_exit:
+                        sys.exit(1)
+
+        def emit_message(self, message, to_exit = False):
+                """ Print a message to STDOUT. """
+                if not self.mute:
+                        sys.stdout.write(message + '\n')
+                        sys.stdout.flush()
+                if to_exit:
+                        sys.exit(0)
+
+        def view(self, logobj, emitobj, msg, **kwargs):
+                options = {'to_exit' : False,
+                           'silent' : False
+                           }
+                options.update(kwargs)
+
+                if logobj:
+                        logobj(msg)
+                if emitobj:                        
+                        if not options['silent']:
+                                emitobj(msg, to_exit = options['to_exit'])
+
+        def daemonize(self):
+                """
+                Double-forks the process to daemonize the script.
+                see Stevens' "Advanced Programming in the UNIX Environment" for details (ISBN 0201563177)
+                http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
+                """
+                self.view(self.logdaemon.debug, None, "Attempting to daemonize the process...")
+
+                # First fork.
+                self.fork(msg = "First fork")
+                # Decouple from parent environment.
+                self.detach()                
+                # Second fork.
+                self.fork(msg = "Second fork")
+                # Write the PID file.
+                self.create_pidfile()
+                self.view(self.logdaemon.info, self.emit_message, "The daemon process has started.")
+                # Redirect standard file descriptors.
+                sys.stdout.flush()
+                sys.stderr.flush()
+                self.attach('stdin', mode = 'r')
+                self.attach('stdout', mode = 'a+')
+
+                try:
+                        self.attach('stderr', mode = 'a+', buffering = 0)
+                except ValueError:
+                        # Python 3 can't have unbuffered text I/O.
+                        self.attach('stderr', mode = 'a+', buffering = 1)
+
+                # Handle signals.                
+                signal.signal(signal.SIGINT, self.handle_terminate)
+                signal.signal(signal.SIGTERM, self.handle_terminate) 
+                signal.signal(signal.SIGHUP, self.handle_reload)
+                #signal.signal(signal.SIGKILL....)
+
+        def fork(self, msg):
+                try:
+                        pid = os.fork()
+                        if pid > 0:                                
+                                self.view(self.logdaemon.debug, None, msg + " success with PID %d." %pid)
+                                # Exit from parent.
+                                sys.exit(0)
+                except Exception as e:
+                        msg += " failed: %s." %str(e)
+                        self.view(self.logdaemon.error, self.emit_error, msg)
+                        
+        def detach(self):
+                # cd to root for a guarenteed working dir.
+                try:
+                        os.chdir(self.homedir)
+                except Exception as e:
+                        msg = "Unable to change working directory: %s." %str(e)
+                        self.view(self.logdaemon.error, self.emit_error, msg)
+                        
+                # clear the session id to clear the controlling tty.
+                pid = os.setsid()
+                if pid == -1:
+                        sys.exit(1)
+                
+                # set the umask so we have access to all files created by the daemon.
+                try:
+                        os.umask(self.umask)
+                except Exception as e:
+                        msg = "Unable to change file creation mask: %s." %str(e)
+                        self.view(self.logdaemon.error, self.emit_error, msg)
+
+        def attach(self, name, mode, buffering = -1):
+                with open(getattr(self, name), mode, buffering) as stream:
+                        os.dup2(stream.fileno(), getattr(sys, name).fileno())
+
+        def checkfile(self, path, typearg, typefile):
+                filename = os.path.basename(path)
+                pathname = os.path.dirname(path)
+                if not os.path.isdir(pathname):
+                        msg = "argument %s: invalid directory: '%s'. Exiting..." %(typearg, pathname)
+                        self.view(self.logdaemon.error, self.emit_error, msg)
+                elif not filename.lower().endswith(typefile):
+                        msg = "argument %s: not a %s file, invalid extension: '%s'. Exiting..." %(typearg, typefile, filename)
+                        self.view(self.logdaemon.error, self.emit_error, msg)
+
+        def create_pidfile(self):
+                atexit.register(self.delete_pidfile)
+                pid = os.getpid()
+                try:
+                        with open(self.pidfile, 'w+') as pf:
+                             pf.write("%s\n" %pid)
+                        self.view(self.logdaemon.debug, None, "PID %d written to '%s'." %(pid, self.pidfile))
+                except Exception as e:
+                        msg = "Unable to write PID to PIDFILE '%s': %s" %(self.pidfile, str(e))
+                        self.view(self.logdaemon.error, self.emit_error, msg)
+
+        def delete_pidfile(self, pid):
+                # Remove the PID file.
+                try:
+                        os.remove(self.pidfile)
+                        self.view(self.logdaemon.debug, None, "Removing PIDFILE '%s' with PID %d." %(self.pidfile, pid))
+                except Exception as e:
+                        if e.errno != errno.ENOENT:
+                                self.view(self.logdaemon.error, self.emit_error, str(e))
+
+        def get_pidfile(self):
+                # Get the PID from the PID file.
+                if self.pidfile is None:
+                        return None
+                if not os.path.isfile(self.pidfile):
+                        return None
+
+                try:
+                        with open(self.pidfile, 'r') as pf:
+                                pid = int(pf.read().strip())
+                        self.view(self.logdaemon.debug, None, "Found PID %d in PIDFILE '%s'" %(pid, self.pidfile))
+                except Exception as e:
+                        self.view(self.logdaemon.warning, None, "Empty or broken PIDFILE")
+                        pid = None
+
+                def pid_exists(pid):
+                        # psutil _psposix.py.
+                        if pid == 0:
+                                return True
+                        try:
+                                os.kill(pid, 0)
+                        except OSError as e:
+                                if e.errno == errno.ESRCH:
+                                        return False
+                                elif e.errno == errno.EPERM:
+                                        return True
+                                else:
+                                        self.view(self.logdaemon.error, self.emit_error, str(e))
+                        else:
+                                return True
+
+                if pid is not None and pid_exists(pid):
+                        return pid
+                else:
+                        # Remove the stale PID file.
+                        self.delete_pidfile(pid)
+                        return None
+
+        def start(self):
+                """ Start the daemon. """
+                self.view(self.logdaemon.info, self.emit_message, "Starting the daemon process...", silent = self.etrigan_restart)
+                
+                # Check for a PID file to see if the Daemon is already running.
+                pid = self.get_pidfile()
+                if pid is not None:
+                        msg = "A previous daemon process with PIDFILE '%s' already exists. Daemon already running ?" %self.pidfile
+                        self.view(self.logdaemon.warning, self.emit_error, msg, to_exit = False)
+                        return
+
+                # Daemonize the main process.
+                self.daemonize()
+                # Start a infinitive loop that periodically runs `funcs_to_daemonize`.
+                self.loop()
+                # eventualy run quit (on start) function/s.
+                if self.want_quit:
+                        if not isinstance(self.quit_on_start, (list, tuple)):
+                                self.quit_on_start = [self.quit_on_start]
+                        self.execute(self.quit_on_start)
+
+        def stop(self):
+                """ Stop the daemon. """
+                self.view(None, self.emit_message, "Stopping the daemon process...", silent = self.etrigan_restart)
+                
+                self.logdaemon.disabled = True
+                pid = self.get_pidfile()
+                self.logdaemon.disabled = False
+                if not pid:
+                        # Just to be sure. A ValueError might occur
+                        # if the PIDFILE is empty but does actually exist.
+                        if os.path.exists(self.pidfile):
+                                self.delete_pidfile(pid)
+
+                        msg = "Can't find the daemon process with PIDFILE '%s'. Daemon not running ?" %self.pidfile
+                        self.view(self.logdaemon.warning, self.emit_error, msg, to_exit = False)
+                        return
+
+                # Try to kill the daemon process.
+                try:
+                        while True:
+                                os.kill(pid, signal.SIGTERM)
+                                time.sleep(0.1)
+                except Exception as e:
+                        if (e.errno != errno.ESRCH):
+                                self.view(self.logdaemon.error, self.emit_error, "Failed to stop the daemon process: %s" %str(e))
+                        else:
+                                self.view(None, self.emit_message, "The daemon process has ended correctly.", silent = self.etrigan_restart)
+
+        def restart(self):
+                """ Restart the daemon. """
+                self.view(self.logdaemon.info, self.emit_message, "Restarting the daemon process...")
+                self.etrigan_restart = True
+                self.stop()
+                if self.pause_restart:
+                        time.sleep(self.pause_restart)
+                        self.etrigan_alive = True
+                self.start()
+
+        def reload(self):
+                pass
+
+        def status(self):
+                """ Get status of the daemon. """
+                self.view(self.logdaemon.info, self.emit_message, "Viewing the daemon process status...")
+
+                if self.pidfile is None:
+                        self.view(self.logdaemon.error, self.emit_error, "Cannot get the status of daemon without PIDFILE.")
+           
+                pid = self.get_pidfile()
+                if pid is None:
+                        self.view(self.logdaemon.info, self.emit_message, "The daemon process is not running.", to_exit = True)
+                else:
+                        try: 
+                                with open("/proc/%d/status" %pid, 'r') as pf:
+                                        pass
+                                self.view(self.logdaemon.info, self.emit_message, "The daemon process is running.", to_exit = True)
+                        except Exception as e:
+                                msg = "There is not a process with the PIDFILE '%s': %s" %(self.pidfile, str(e))
+                                self.view(self.logdaemon.error, self.emit_error, msg)
+
+        def flatten(self, alistoflists, ltypes = Sequence):
+                # https://stackoverflow.com/questions/2158395/flatten-an-irregular-list-of-lists/2158532#2158532
+                alistoflists = list(alistoflists)
+                while alistoflists:
+                        while alistoflists and isinstance(alistoflists[0], ltypes):
+                                alistoflists[0:1] = alistoflists[0]
+                        if alistoflists: yield alistoflists.pop(0)
+
+        def exclude(self, func):
+                from inspect import getargspec
+                args = getargspec(func)
+                if callable(func):
+                        try:
+                                args[0].pop(0)
+                        except IndexError:
+                                pass
+                        return args
+                else:
+                        self.view(self.logdaemon.error, self.emit_error, "Not a function.")
+                        return
+
+        def execute(self, some_functions):
+                returned = None
+                if isinstance(some_functions, (list, tuple)):
+                        for func in some_functions: 
+                                l_req = len(self.exclude(func)[0])
+                                
+                                if l_req == 0:
+                                        returned = func()
+                                else:
+                                        l_add = len(self.etrigan_add)
+                                        if l_req > l_add:
+                                                self.view(self.logdaemon.error, self.emit_error,
+                                                          "Can't evaluate function: given %s, required %s." %(l_add, l_req))
+                                                return
+                                        else:
+                                                arguments = self.etrigan_add[self.etrigan_index]
+                                                l_args = (len(arguments) if isinstance(arguments, list) else 1)
+                                                if (l_args > l_req) or (l_args < l_req):
+                                                        self.view(self.logdaemon.error, self.emit_error,
+                                                                  "Can't evaluate function: given %s, required %s." %(l_args, l_req))
+                                                        return
+                                                else:
+                                                        if isinstance(arguments, list):
+                                                                returned = func(*arguments)
+                                                        else:
+                                                                returned = func(arguments)
+
+                                if returned:
+                                        if isinstance(returned, (list, tuple)):
+                                                if isinstance(returned[0], int):
+                                                        self.etrigan_index = returned[0]
+                                                else:
+                                                        self.etrigan_index = slice(*map(int, returned[0].split(':')))
+                                                if returned[1:] != []:
+                                                        self.etrigan_add.append(returned[1:])
+                                                        self.etrigan_add = list(self.flatten(self.etrigan_add))
+                                        else:
+                                                self.view(self.logdaemon.error, self.emit_error, "Function should return list or tuple.")
+                                        returned = None
+                else:
+                        if some_functions is None:
+                                self.run()
+
+        def loop(self):
+                try:
+                        if self.pause_loop is None:
+                                # one-shot.
+                                self.execute(self.funcs_to_daemonize)
+                        else:
+                                if self.pause_loop >= 0:
+                                        # infinite with pause.
+                                        time.sleep(self.pause_loop)
+                                        while self.etrigan_alive:
+                                                self.execute(self.funcs_to_daemonize)
+                                                time.sleep(self.pause_loop)
+                                elif self.pause_loop == -1:
+                                        # infinite without pause.
+                                        while self.etrigan_alive:
+                                                self.execute(self.funcs_to_daemonize)
+                except Exception as e:
+                        msg = "The daemon process start method failed: %s" %str(e)
+                        self.view(self.logdaemon.error, self.emit_error, msg)
+                        
+        def quit_standard(self):
+                self.view(self.logdaemon.info, None, "Stopping the daemon process...")
+                self.delete_pidfile(self.get_pidfile())
+                self.view(self.logdaemon.info, None, "The daemon process has ended correctly.")
+
+        def quit_on_start(self):
+                """
+                Override this method when you subclass Daemon.
+                """
+                self.quit_standard()
+                
+        def quit_on_stop(self):
+                """
+                Override this method when you subclass Daemon.
+                """
+                pass
+
+        def run(self):
+                """
+                Override this method when you subclass Daemon.
+                It will be called after the process has been
+                daemonized by start() or restart().
+                """
+                pass
+                
+#-----------------------------------------------------------------------------------------------------------------------------------------------------------
+
+class JasonBlood(Etrigan):
+        def run(self):
+                jasonblood_func()
+
+def jasonblood_func():  
+        with open(os.path.join(path, 'etrigan_test.txt'), 'a') as file:
+                file.write("Yarva Demonicus Etrigan " + time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()) + '\n')
+
+def Etrigan_parser(parser = None):
+        if parser is None:
+                # create a new parser.
+                parser = argparse.ArgumentParser(description = __description__, epilog = __version__)
+        if not parser.add_help:
+                # create help argument.
+                parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit")
+        
+        # attach to an existent parser.
+        parser.add_argument("operation", action = "store", choices = ["start", "stop", "restart", "status", "reload"],
+                            help = "Select an operation for daemon.", type = str)
+        parser.add_argument("--etrigan-pid",
+                            action = "store", dest = "etriganpid", default = "/tmp/etrigan.pid",
+                            help = "Choose a pidfile path. Default is \"/tmp/etrigan.pid\".", type = str) #'/var/run/etrigan.pid'
+        parser.add_argument("--etrigan-log",
+                            action = "store", dest = "etriganlog", default = os.path.join(path, "etrigan.log"),
+                            help = "Use this option to choose an output log file; for not logging don't select it. Default is \"etrigan.log\".", type = str)
+        parser.add_argument("--etrigan-lev",
+                            action = "store", dest = "etriganlev", default = "DEBUG",
+                            choices = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
+                            help = "Use this option to set a log level. Default is \"DEBUG\".", type = str)
+        parser.add_argument("--etrigan-mute",
+                            action = "store_const", dest = 'etriganmute', const = True, default = False,
+                            help = "Disable all stdout and stderr messages.")
+        return parser
+
+class Etrigan_check(object):
+        def emit_opt_err(self, msg):
+                print(msg)
+                sys.exit(1)
+
+        def checkfile(self, path, typearg, typefile):
+                filename, extension = os.path.splitext(path)
+                pathname = os.path.dirname(path)
+                if not os.path.isdir(pathname):
+                        msg = "argument `%s`: invalid directory: '%s'. Exiting..." %(typearg, pathname)
+                        self.emit_opt_err(msg)
+                elif not extension == typefile:
+                        msg = "argument `%s`: not a %s file, invalid extension: '%s'. Exiting..." %(typearg, typefile, extension)
+                        self.emit_opt_err(msg)
+
+        def checkfunction(self, funcs, booleans):
+                if not isinstance(funcs, (list, tuple)):
+                        if funcs is not None:
+                                msg = "argument `funcs_to_daemonize`: provide list, tuple or None"
+                                self.emit_opt_err(msg)
+                                        
+                for elem in booleans:
+                        if not type(elem) == bool:
+                                msg = "argument `want_quit`: not a boolean."
+                                self.emit_opt_err(msg)
+        
+def Etrigan_job(type_oper, daemon_obj):
+        Etrigan_check().checkfunction(daemon_obj.funcs_to_daemonize,
+                                      [daemon_obj.want_quit])
+        if type_oper == "start":
+                daemon_obj.start()
+        elif type_oper == "stop":
+                daemon_obj.stop()
+        elif type_oper == "restart":
+                daemon_obj.restart()
+        elif type_oper == "status":
+                daemon_obj.status()
+        elif type_oper == "reload":
+                daemon_obj.reload()
+        sys.exit(0)
+
+def main():
+        # Parse arguments.
+        parser = Etrigan_parser()
+        args = vars(parser.parse_args())
+        # Check arguments.
+        Etrigan_check().checkfile(args['etriganpid'], 'pidfile', '.pid')
+        Etrigan_check().checkfile(args['etriganlog'], 'pidfile', '.log')
+
+        # Setup daemon.
+        jasonblood_1 = Etrigan(pidfile = args['etriganpid'], logfile = args['etriganlog'], loglevel = args['etriganlev'],
+                               mute = args['etriganmute'],
+                               funcs_to_daemonize = [jasonblood_func], pause_loop = 5)
+
+##        jasonblood_2 = JasonBlood(pidfile = args['etriganpid'], logfile = args['etriganlog'], loglevel = args['etriganlev'],
+##                                  mute = args['etriganmute'],
+##                                  funcs_to_daemonize = None, pause_loop = 5)
+        # Do job.
+        Etrigan_job(args['operation'], jasonblood_1)
+        
+if __name__ == '__main__':
+        main()
diff --git a/py-kms/pykms_Client.py b/py-kms/pykms_Client.py
index 6ed5a27..01ab761 100644
--- a/py-kms/pykms_Client.py
+++ b/py-kms/pykms_Client.py
@@ -25,8 +25,11 @@ from pykms_Misc import logger_create, check_logfile
 from pykms_Misc import KmsParser, KmsException
 from pykms_Format import justify, byterize, enco, deco, ShellMessage, pretty_printer
 
-clt_description = 'KMS Client Emulator written in Python'
-clt_version = 'py-kms_2019-05-15'
+clt_version             = "py-kms_2020-02-02"
+__license__             = "The Unlicense"
+__author__              = u"Matteo ℱan <SystemRage@protonmail.com>"
+__url__                 = "https://github.com/SystemRage/py-kms"
+clt_description         = "py-kms: KMS Client Emulator written in Python"
 clt_config = {}
 
 #---------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -58,7 +61,7 @@ will be generated.', 'def' : None, 'des' : "machineName"},
                     'choi' : ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "MINI"]},
         'lfile' : {'help' : 'Use this option to set an output log file. The default is \"pykms_logclient.log\". Type \"STDOUT\" to view \
 log info on stdout. Type \"FILESTDOUT\" to combine previous actions.',
-                   'def' : os.path.dirname(os.path.abspath( __file__ )) + "/pykms_logclient.log", 'des' : "logfile"},
+                   'def' : os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pykms_logclient.log'), 'des' : "logfile"},
         'lsize' : {'help' : 'Use this flag to set a maximum size (in MB) to the output log file. Desactivated by default.', 'def' : 0, 'des': "logsize"},
         }
 
diff --git a/py-kms/pykms_Format.py b/py-kms/pykms_Format.py
index f7a8a75..080609b 100644
--- a/py-kms/pykms_Format.py
+++ b/py-kms/pykms_Format.py
@@ -215,7 +215,7 @@ class ShellMessage(object):
             self.put_text = put_text
             self.where = where
             self.plaintext = []
-            self.path = os.path.dirname(os.path.abspath( __file__ )) + '/newlines.txt'
+            self.path = os.path.dirname(os.path.abspath( __file__ )) + '/pykms_newlines.txt'
             self.print_queue = Queue.Queue()
 
         def formatter(self, msgtofrmt):
diff --git a/py-kms/pykms_GuiBase.py b/py-kms/pykms_GuiBase.py
index 85c413d..2430602 100644
--- a/py-kms/pykms_GuiBase.py
+++ b/py-kms/pykms_GuiBase.py
@@ -20,12 +20,15 @@ except ImportError:
         from tkinter import filedialog
         import tkinter.font as tkFont
         
-from pykms_Server import srv_options, srv_version, srv_config, srv_terminate, serverqueue, serverthread
+from pykms_Server import srv_options, srv_version, srv_config, server_terminate, serverqueue, serverthread
 from pykms_GuiMisc import ToolTip, TextDoubleScroll, TextRedirect, custom_background
 from pykms_Client import clt_options, clt_version, clt_config, client_thread
 
-gui_description = 'py-kms GUI'
-gui_version = 'v1.0'
+gui_version             = "py-kms_gui_v2.0"
+__license__             = "The Unlicense"
+__author__              = u"Matteo ℱan <SystemRage@protonmail.com>"
+__url__                 = "https://github.com/SystemRage/py-kms"
+gui_description         = "A GUI for py-kms."
 
 ##---------------------------------------------------------------------------------------------------------------------------------------------------------
 def get_ip_address():
@@ -452,7 +455,7 @@ class KmsGui(tk.Tk):
         def srv_actions_stop(self):
                 if serverthread.is_running_server:
                         if serverthread.server is not None:
-                                srv_terminate(exit_server = True)
+                                server_terminate(serverthread, exit_server = True)
                                 # wait for switch.
                                 while serverthread.is_running_server:
                                         pass
@@ -522,10 +525,10 @@ class KmsGui(tk.Tk):
         def on_exit(self):
                 if serverthread.is_running_server:
                         if serverthread.server is not None:
-                                srv_terminate(exit_server = True)
+                                server_terminate(serverthread, exit_server = True)
                         else:
                                 serverthread.is_running_server = False
-                srv_terminate(exit_thread = True)
+                server_terminate(serverthread, exit_thread = True)
                 self.destroy()
 
         def on_clear(self, widgetlist):
diff --git a/py-kms/pykms_Misc.py b/py-kms/pykms_Misc.py
index d6cb7b2..4dabb67 100644
--- a/py-kms/pykms_Misc.py
+++ b/py-kms/pykms_Misc.py
@@ -221,6 +221,31 @@ class KmsParser(argparse.ArgumentParser):
         def error(self, message):
                 raise KmsException(message)
 
+class KmsHelper(object):
+        def replace(self, parser):
+                text = parser.format_help().splitlines()
+                help_list = []
+                for line in text:
+                        if line == parser.description:
+                                continue
+                        if line == parser.epilog:
+                                line = 80 * '*' + '\n'
+                        help_list.append(line)
+                return help_list
+
+        def print(self, parsers):
+                parser_base, parser_adj, parser_sub = parsers
+                print('\n' + parser_base.description)
+                print(len(parser_base.description) * '-' + '\n')
+                for line in self.replace(parser_base):
+                        print(line)
+                print(parser_adj.description + '\n')
+                for line in self.replace(parser_sub):
+                        print(line)
+                print('\n' + len(parser_base.epilog) * '-')
+                print(parser_base.epilog + '\n')
+                parser_base.exit()
+
 #----------------------------------------------------------------------------------------------------------------------------------------------------------
 
 # http://joshpoley.blogspot.com/2011/09/hresults-user-0x004.html  (slerror.h)
diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py
index 7176ac7..27b3387 100755
--- a/py-kms/pykms_Server.py
+++ b/py-kms/pykms_Server.py
@@ -9,6 +9,7 @@ import uuid
 import logging
 import os
 import threading
+import pickle
 
 try:
         # Python 2 import.
@@ -27,11 +28,15 @@ import pykms_RpcBind, pykms_RpcRequest
 from pykms_RpcBase import rpcBase
 from pykms_Dcerpc import MSRPCHeader
 from pykms_Misc import logger_create, check_logfile, check_lcid
-from pykms_Misc import KmsParser, KmsException
+from pykms_Misc import KmsParser, KmsException, KmsHelper
 from pykms_Format import enco, deco, ShellMessage, pretty_printer
+from Etrigan import Etrigan, Etrigan_parser, Etrigan_check, Etrigan_job
 
-srv_description = 'KMS Server Emulator written in Python'
-srv_version = 'py-kms_2019-05-15'
+srv_version             = "py-kms_2020-02-02"
+__license__             = "The Unlicense"
+__author__              = u"Matteo ℱan <SystemRage@protonmail.com>"
+__url__                 = "https://github.com/SystemRage/py-kms"
+srv_description         = "py-kms: KMS Server Emulator written in Python"
 srv_config = {}
 
 ##---------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -110,12 +115,14 @@ class server_thread(threading.Thread):
                 self.name = name
                 self.queue = queue
                 self.server = None
-                self.is_running_server, self.with_gui = [False for _ in range(2)]
+                self.is_running_server, self.with_gui, self.checked = [False for _ in range(3)]
                 self.is_running_thread = threading.Event()
 
         def terminate_serve(self):
                 self.server.shutdown()
                 self.server.server_close()
+                self.server = None
+                self.is_running_server = False
 
         def terminate_thread(self):
                 self.is_running_thread.set()
@@ -136,15 +143,11 @@ class server_thread(threading.Thread):
                                                 self.eject = False
                                                 self.is_running_server = True
                                                 # Check options.
-                                                server_check()
+                                                if not self.checked:
+                                                        server_check()
                                                 # Create and run server.
                                                 self.server = server_create()
                                                 self.server.pykms_serve()
-                                        elif item == 'stop':
-                                                self.server = None
-                                                self.is_running_server = False
-                                        elif item == 'exit':
-                                                self.terminate_thread()
                                 except SystemExit as e:
                                         self.eject = True
                                         if not self.with_gui:
@@ -179,46 +182,126 @@ The default is \"364F463A8863D35F\" or type \"RANDOM\" to auto generate the HWID
                     'choi' : ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "MINI"]},
         'lfile' : {'help' : 'Use this option to set an output log file. The default is \"pykms_logserver.log\". Type \"STDOUT\" to view \
 log info on stdout. Type \"FILESTDOUT\" to combine previous actions.',
-                   'def' : os.path.dirname(os.path.abspath( __file__ )) + "/pykms_logserver.log", 'des' : "logfile"},
+                   'def' : os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pykms_logserver.log'), 'des' : "logfile"},
         'lsize' : {'help' : 'Use this flag to set a maximum size (in MB) to the output log file. Desactivated by default.', 'def' : 0, 'des': "logsize"},
         }
 
 def server_options():
-        parser = KmsParser(description = srv_description, epilog = 'version: ' + srv_version)
-        parser.add_argument("ip", nargs = "?", action = "store", default = srv_options['ip']['def'], help = srv_options['ip']['help'], type = str)
-        parser.add_argument("port", nargs = "?", action = "store", default = srv_options['port']['def'], help = srv_options['port']['help'], type = int)
-        parser.add_argument("-e", "--epid", dest = srv_options['epid']['des'], default = srv_options['epid']['def'], help = srv_options['epid']['help'], type = str)
-        parser.add_argument("-l", "--lcid", dest = srv_options['lcid']['des'], default = srv_options['lcid']['def'], help = srv_options['lcid']['help'], type = int)
-        parser.add_argument("-c", "--client-count", dest = srv_options['count']['des'] , default = srv_options['count']['def'],
-                            help = srv_options['count']['help'], type = int)
-        parser.add_argument("-a", "--activation-interval", dest = srv_options['activation']['des'], default = srv_options['activation']['def'],
-                            help = srv_options['activation']['help'], type = int)
-        parser.add_argument("-r", "--renewal-interval", dest = srv_options['renewal']['des'], default = srv_options['renewal']['def'],
-                            help = srv_options['renewal']['help'], type = int)
-        parser.add_argument("-s", "--sqlite", dest = srv_options['sql']['des'], action = "store_const", const = True, default = srv_options['sql']['def'],
-                            help = srv_options['sql']['help'])
-        parser.add_argument("-w", "--hwid", dest = srv_options['hwid']['des'], action = "store", default = srv_options['hwid']['def'],
-                            help = srv_options['hwid']['help'], type = str)
-        parser.add_argument("-t", "--timeout", dest = srv_options['time']['des'], action = "store", default = srv_options['time']['def'],
-                            help = srv_options['time']['help'], type = int)
-        parser.add_argument("-V", "--loglevel", dest = srv_options['llevel']['des'], action = "store", choices = srv_options['llevel']['choi'],
-                            default = srv_options['llevel']['def'], help = srv_options['llevel']['help'], type = str)
-        parser.add_argument("-F", "--logfile", nargs = "+", action = "store", dest = srv_options['lfile']['des'], default = srv_options['lfile']['def'],
-                            help = srv_options['lfile']['help'], type = str)
-        parser.add_argument("-S", "--logsize", dest = srv_options['lsize']['des'], action = "store", default = srv_options['lsize']['def'],
-                            help = srv_options['lsize']['help'], type = float)
+        main_parser = KmsParser(description = srv_description, epilog = 'version: ' + srv_version, add_help = False, allow_abbrev = False)
+        main_parser.add_argument("ip", nargs = "?", action = "store", default = srv_options['ip']['def'], help = srv_options['ip']['help'], type = str)
+        main_parser.add_argument("port", nargs = "?", action = "store", default = srv_options['port']['def'], help = srv_options['port']['help'], type = int)
+        main_parser.add_argument("-e", "--epid", action = "store", dest = srv_options['epid']['des'], default = srv_options['epid']['def'],
+                                 help = srv_options['epid']['help'], type = str)
+        main_parser.add_argument("-l", "--lcid", action = "store", dest = srv_options['lcid']['des'], default = srv_options['lcid']['def'],
+                                 help = srv_options['lcid']['help'], type = int)
+        main_parser.add_argument("-c", "--client-count", action = "store", dest = srv_options['count']['des'] , default = srv_options['count']['def'],
+                                 help = srv_options['count']['help'], type = int)
+        main_parser.add_argument("-a", "--activation-interval", action = "store", dest = srv_options['activation']['des'],
+                                 default = srv_options['activation']['def'], help = srv_options['activation']['help'], type = int)
+        main_parser.add_argument("-r", "--renewal-interval", action = "store", dest = srv_options['renewal']['des'], default = srv_options['renewal']['def'],
+                                 help = srv_options['renewal']['help'], type = int)
+        main_parser.add_argument("-s", "--sqlite", action = "store_const", dest = srv_options['sql']['des'], const = True, default = srv_options['sql']['def'],
+                                 help = srv_options['sql']['help'])
+        main_parser.add_argument("-w", "--hwid", action = "store", dest = srv_options['hwid']['des'], default = srv_options['hwid']['def'],
+                                 help = srv_options['hwid']['help'], type = str)
+        main_parser.add_argument("-t", "--timeout", action = "store", dest = srv_options['time']['des'], default = srv_options['time']['def'],
+                                 help = srv_options['time']['help'], type = int)
+        main_parser.add_argument("-V", "--loglevel", action = "store", dest = srv_options['llevel']['des'], choices = srv_options['llevel']['choi'],
+                                 default = srv_options['llevel']['def'], help = srv_options['llevel']['help'], type = str)
+        main_parser.add_argument("-F", "--logfile", nargs = "+", action = "store", dest = srv_options['lfile']['des'], default = srv_options['lfile']['def'],
+                                 help = srv_options['lfile']['help'], type = str)
+        main_parser.add_argument("-S", "--logsize", action = "store", dest = srv_options['lsize']['des'], default = srv_options['lsize']['def'],
+                                 help = srv_options['lsize']['help'], type = float)
+        main_parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit")
+
+        daemon_parser = KmsParser(description = "daemon options inherited from Etrigan", add_help = False, allow_abbrev = False)
+        daemon_subparser = daemon_parser.add_subparsers(dest = "mode")
+        etrigan_parser = daemon_subparser.add_parser("etrigan", add_help = False, allow_abbrev = False)
+        etrigan_parser.add_argument("-g", "--gui", action = "store_const", dest = 'gui', const = True, default = False,
+                                    help = "Enable py-kms GUI usage.")
+        etrigan_parser = Etrigan_parser(parser = etrigan_parser)
 
         try:
-                srv_config.update(vars(parser.parse_args()))
-                # Check logfile.
-                srv_config['logfile'] = check_logfile(srv_config['logfile'], srv_options['lfile']['def'], where = "srv")
+                if "-h" in sys.argv[1:]:
+                        KmsHelper().print(parsers = [main_parser, daemon_parser, etrigan_parser])
+
+                # Set defaults for config.
+                # case: python3 pykms_Server.py
+                srv_config.update(vars(main_parser.parse_args([])))
+                # Eventually set daemon values for config.
+                if 'etrigan' in sys.argv[1:]:
+                        if 'etrigan' == sys.argv[1]:
+                                # case: python3 pykms_Server.py etrigan start --daemon_optionals
+                                srv_config.update(vars(daemon_parser.parse_args(sys.argv[1:])))
+                        elif 'etrigan' == sys.argv[2]:
+                                # case: python3 pykms_Server.py 1.2.3.4 etrigan start --daemon_optionals
+                                srv_config['ip'] = sys.argv[1]
+                                srv_config.update(vars(daemon_parser.parse_args(sys.argv[2:])))
+                        else:
+                                # case: python3 pykms_Server.py 1.2.3.4 1234 --main_optionals etrigan start --daemon_optionals
+                                knw_args, knw_extras = main_parser.parse_known_args()
+                                # fix for logfile option (at the end) that catchs etrigan parser options.
+                                if 'etrigan' in knw_args.logfile:
+                                        indx = knw_args.logfile.index('etrigan')
+                                        for num, elem in enumerate(knw_args.logfile[indx:]):
+                                                knw_extras.insert(num, elem)
+                                        knw_args.logfile = knw_args.logfile[:indx]
+
+                                # continue parsing.
+                                if len(knw_extras) > 0 and knw_extras[0] in ['etrigan']:
+                                        daemon_parser.parse_args(knw_extras, namespace = knw_args)
+                                srv_config.update(vars(knw_args))
+                else:
+                        # Update dict config.
+                        # case: python3 pykms_Server.py 1.2.3.4 1234 --main_optionals
+                        knw_args, knw_extras = main_parser.parse_known_args()
+                        if knw_extras != []:
+                                raise KmsException("unrecognized arguments: %s" %' '.join(knw_extras))
+                        else:
+                                srv_config.update(vars(knw_args))
+
         except KmsException as e:
                 pretty_printer(put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e), to_exit = True)
 
+
+def server_daemon():
+        if 'etrigan' in srv_config.values():
+                path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pykms_config.pickle')
+
+                if srv_config['operation'] in ['stop', 'restart', 'status'] and len(sys.argv[1:]) > 2:
+                        pretty_printer(put_text = "{reverse}{red}{bold}too much arguments. Exiting...{end}", to_exit = True)
+
+                if srv_config['gui']:
+                        pass
+                else:
+                        if srv_config['operation'] == 'start':
+                                with open(path, 'wb') as file:
+                                        pickle.dump(srv_config, file, protocol = pickle.HIGHEST_PROTOCOL)
+                        elif srv_config['operation'] in ['stop', 'status', 'restart']:
+                                with open(path, 'rb') as file:
+                                        old_srv_config = pickle.load(file)
+                                old_srv_config = {x: old_srv_config[x] for x in old_srv_config if x not in ['operation']}
+                                srv_config.update(old_srv_config)
+
+                serverdaemon = Etrigan(srv_config['etriganpid'],
+                                       logfile = srv_config['etriganlog'], loglevel = srv_config['etriganlev'],
+                                       mute = srv_config['etriganmute'], pause_loop = None)
+
+                if srv_config['operation'] == 'start':
+                        serverdaemon.want_quit = True
+                        if srv_config['gui']:
+                                serverdaemon.funcs_to_daemonize = [server_with_gui]
+                        else:
+                                server_without_gui = ServerWithoutGui()
+                                serverdaemon.funcs_to_daemonize = [server_without_gui.start, server_without_gui.join]
+                                indx_for_clean = lambda: (0, )
+                                serverdaemon.quit_on_stop = [indx_for_clean, server_without_gui.clean]
+
+                Etrigan_job(srv_config['operation'], serverdaemon)
+
 def server_check():
-        # Check logfile (only for GUI).
-        if serverthread.with_gui:
-                srv_config['logfile'] = check_logfile(srv_config['logfile'], srv_options['lfile']['def'], where = "srv")
+        # Check logfile.
+        srv_config['logfile'] = check_logfile(srv_config['logfile'], srv_options['lfile']['def'], where = "srv")
 
         # Setup hidden or not messages.
         ShellMessage.view = ( False if any(i in ['STDOUT', 'FILESTDOUT'] for i in srv_config['logfile']) else True )
@@ -279,28 +362,59 @@ def server_create():
         loggersrv.info("HWID: %s" % deco(binascii.b2a_hex(srv_config['hwid']), 'utf-8').upper())
         return server
 
-def srv_terminate(exit_server = False, exit_thread = False):
+def server_terminate(generic_srv, exit_server = False, exit_thread = False):
         if exit_server:
-                serverthread.terminate_serve()
-                serverqueue.put('stop')
+                generic_srv.terminate_serve()
         if exit_thread:
-                serverqueue.put('exit')
+                generic_srv.terminate_thread()
 
-def srv_main_without_gui():
+class ServerWithoutGui(object):
+        def start(self):
+                import queue as Queue
+                daemon_queue = Queue.Queue(maxsize = 0)
+                daemon_serverthread = server_thread(daemon_queue, name = "Thread-Srv-Daemon")
+                daemon_serverthread.setDaemon(True)
+                # options already checked in `server_main_terminal`.
+                daemon_serverthread.checked = True
+                daemon_serverthread.start()
+                daemon_queue.put('start')
+                return 0, daemon_serverthread
+
+        def join(self, daemon_serverthread):
+                while daemon_serverthread.is_alive():
+                        daemon_serverthread.join(timeout = 0.5)
+
+        def clean(self, daemon_serverthread):
+                server_terminate(daemon_serverthread, exit_server = True, exit_thread = True)
+
+def server_main_terminal():
         # Parse options.
         server_options()
-        # Run threaded server.
-        serverqueue.put('start')
-        # Wait to finish.
-        try:
-                while serverthread.is_alive():
-                        serverthread.join(timeout = 0.5)
-        except (KeyboardInterrupt, SystemExit):
-                srv_terminate(exit_server = True, exit_thread = True)
+        # Check options.
+        server_check()
+        serverthread.checked = True
 
-def srv_main_with_gui(width = 950, height = 660):
+        if 'etrigan' not in srv_config.values():
+                # (without GUI) and (without daemon).
+                # Run threaded server.
+                serverqueue.put('start')
+                # Wait to finish.
+                try:
+                        while serverthread.is_alive():
+                                serverthread.join(timeout = 0.5)
+                except (KeyboardInterrupt, SystemExit):
+                        server_terminate(serverthread, exit_server = True, exit_thread = True)
+        else:
+                # (with or without GUI) and (with daemon)
+                # Setup daemon (eventually).
+                server_daemon()
+
+def server_with_gui():
         import pykms_GuiBase
 
+        width = 950
+        height = 660
+
         root = pykms_GuiBase.KmsGui()
         root.title(pykms_GuiBase.gui_description + ' ' + pykms_GuiBase.gui_version)
         # Main window initial position.
@@ -314,6 +428,11 @@ def srv_main_with_gui(width = 950, height = 660):
         root.resizable(0, 0)
         root.mainloop()
 
+def server_main_no_terminal():
+        # Run tkinter GUI.
+        # (with GUI) and (without daemon).
+        server_with_gui()
+
 class kmsServerHandler(socketserver.BaseRequestHandler):
         def setup(self):
                 loggersrv.info("Connection accepted: %s:%d" % (self.client_address[0], self.client_address[1]))
@@ -376,9 +495,9 @@ serverthread.start()
 
 if __name__ == "__main__":
         if sys.stdout.isatty():
-                srv_main_without_gui()
+                server_main_terminal()
         else:
                 try:
-                        srv_main_with_gui()
+                        server_main_no_terminal()
                 except:
-                        srv_main_without_gui()
+                        server_main_terminal()