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/CHANGELOG.md b/CHANGELOG.md index ff90fa5..10d1c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +### py-kms_2020-02-02 +- Optimized pretty-print messages process. +- Added -F FILESTDOUT option. +- Added deamonization options (via [Etrigan](https://github.com/SystemRage/Etrigan) project). +- py-kms GUI resurrected (and improved). +- Cleaned, cleaned, cleaned. + ### py-kms_2019-05-15 - Merging for Python2 / Python3 compatibility all-in-one. - Added new options: diff --git a/README.md b/README.md index 74cbcce..7d76c9d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ _py-kms_ is a port of node-kms created by [cyrozap](http://forums.mydigitallife. - Windows 7 - Windows 8 - Windows 8.1 - - Windows 10 ( 1511 / 1607 / 1703 / 1709 / 1803 / 1809 / 1903 ) + - Windows 10 ( 1511 / 1607 / 1703 / 1709 / 1803 / 1809 / 1903 / 1909 ) - Windows Server 2008 - Windows Server 2008 R2 - Windows Server 2012 @@ -39,16 +39,19 @@ _py-kms_ is a port of node-kms created by [cyrozap](http://forums.mydigitallife. # Usage - __NOTE__: Pay attention to how invoke scripts, if you want to run with python2 use ```python...``` while for python3 use ```python3...```, also depending on the Python versions that resides in your PC. -- To start the server, execute ```python pykms_Server.py [IPADDRESS] [PORT]```. - The default _IPADDRESS_ is "0.0.0.0" ( all interfaces ) and the default _PORT_ is "1688". -- To run the client (only for testing purposes), use ```python pykms_Client.py IPADDRESS [PORT]```. -Argument _IPADDRESS_ is always required, while the default _PORT_ is "1688", so a valid command is: ```python pykms_Client.py 0.0.0.0``` -- To show the help pages type: ```python pykms_Server.py -h``` and ```python pykms_Client.py -h``` -- To generate a random HWID use ```-w``` option: ```python pykms_Server.py -w RANDOM``` -- To get the HWID from any server use the client, for example type: ```python pykms_Client.py 0.0.0.0 1688 -m Windows8.1 -V INFO``` -- To view a minimal set of logging information use ```-V MINI``` option, for example: ```python pykms_Server.py -V MINI``` -- To redirect logging on stdout use ```-F STDOUT``` option, for example: ```python pykms_Server.py -F STDOUT``` -- For launching py-kms GUI make executable all _.py_ files in _py-kms_ directory ```chmod +x /path/to/scripts/py-kms/*.py```, then simply run ```pykms_Server.py``` double-clicking. +- To start the server, execute ```python pykms_Server.py [IPADDRESS] [PORT]```, the default _IPADDRESS_ is "0.0.0.0" ( all interfaces ) and the default _PORT_ is "1688". +- To run the client (only for testing purposes), use ```python pykms_Client.py [IPADDRESS] [PORT]```, with the same defaults of ```pykms_Server.py```. +- To show the help pages type: ```python pykms_Server.py -h``` and ```python pykms_Client.py -h```. +- To generate a random HWID use ```-w``` option: ```python pykms_Server.py -w RANDOM```. +- To get the HWID from any server use the client, for example type: ```python pykms_Client.py 0.0.0.0 1688 -m Windows8.1 -V INFO```. +- To view a minimal set of logging information use ```-V MINI``` option, for example: ```python pykms_Server.py -F /path/to/your/logfile.log -V MINI```. +- To redirect logging on stdout use ```-F STDOUT``` option, for example: ```python pykms_Server.py -F STDOUT -V DEBUG```. +- You can create logfile and view logging information on stdout at the same time with ```-F FILESTDOUT``` option, for example: ```python pykms_Server.py -F FILESTDOUT /path/to/your/logfile.log -V DEBUG```. +- Select timeout (seconds) for py-kms with ```-t``` option, for example ```python pykms_Server.py -t 10``` +- For launching py-kms GUI make executable ```pykms_Server.py``` file with ```chmod +x /path/to/folder/py-kms/pykms_Server.py```, then simply run ```pykms_Server.py``` double-clicking. +- You can run py-kms deamonized (via [Etrigan](https://github.com/SystemRage/Etrigan)) using a command like: ```python pykms_Server.py etrigan start``` and stop it with: ```python pykms_Server.py etrigan stop```. +- With Etrigan you have another way to launch py-kms GUI (specially suitable if you're using a virtualenv), so: ```python pykms_Server.py etrigan start -g``` +and stop the GUI with the same precedent command (or interact with EXIT button). # Other Important Stuff Consult the [Wiki](https://github.com/SystemRage/py-kms/wiki) for more information about activation with _py-kms_ and to get GVLK keys. diff --git a/docker/README.md b/docker/README.md index eb64bca..e21f0b8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -25,7 +25,7 @@ _py-kms_ is a port of node-kms created by [cyrozap](http://forums.mydigitallife. # Usage ``` -docker run -d --name py3-kms \ +docker run -it -d --name py3-kms \ -p 8080:8080 \ -p 1688:1688 \ -e IP=0.0.0.0 \ diff --git a/docker/docker-py2-kms/Dockerfile b/docker/docker-py2-kms/Dockerfile index d8dabfd..8c7aa72 100644 --- a/docker/docker-py2-kms/Dockerfile +++ b/docker/docker-py2-kms/Dockerfile @@ -50,7 +50,7 @@ ENV SQLITE false # EN: hwid # RU: hwid -ENV HWID 364F463A8863D35F +ENV HWID "364F463A8863D35F" # Use this flag to specify a HWID. # The HWID must be an 16-character string of hex characters. # The default is "364F463A8863D35F" or type "RANDOM" to auto generate the HWID. @@ -107,7 +107,7 @@ chmod a+x /usr/bin/start.sh && \ # EN: Clear after install software # RU: Очистка после установки программного обеспечения -apk del git py2-pip +apk del git # Set Workdir WORKDIR /home/py-kms diff --git a/docker/docker-py2-kms/run-py2-kms.sh b/docker/docker-py2-kms/run-py2-kms.sh index cb4aedb..5519d62 100755 --- a/docker/docker-py2-kms/run-py2-kms.sh +++ b/docker/docker-py2-kms/run-py2-kms.sh @@ -14,4 +14,4 @@ docker run -d --name py2-kms \ -e LOGSIZE=2 \ -v /etc/localtime:/etc/localtime:ro \ -v /var/log:/var/log:rw \ - --restart unless-stopped pykms/pykms:py2-kms + --restart unless-stopped pykms/pykms:py2-kms \ No newline at end of file diff --git a/docker/docker-py2-kms/start.sh b/docker/docker-py2-kms/start.sh index 08a85a0..716255d 100644 --- a/docker/docker-py2-kms/start.sh +++ b/docker/docker-py2-kms/start.sh @@ -27,25 +27,25 @@ else then /bin/bash -c "/usr/bin/python pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} &" sleep 5 - /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows10 & + /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows7 & /usr/bin/python /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/clients.db else /bin/bash -c "/usr/bin/python pykms_Server.py ${IP} ${PORT} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} -S ${LOGSIZE} &" sleep 5 - /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows10 & + /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows7 & /usr/bin/python /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/clients.db fi else if [ "$LOGSIZE" == "" ]; then /bin/bash -c "/usr/bin/python pykms_Server.py ${IP} ${PORT} -e ${EPID} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} &" - sleep5 - /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows10 & + sleep 5 + /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows7 & /usr/bin/python /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/clients.db else /bin/sh -c "/usr/bin/python pykms_Server.py ${IP} ${PORT} -e ${EPID} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} -S ${LOGSIZE} &" sleep 5 - /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows10 & + /usr/bin/python pykms_Client.py ${IP} ${PORT} -m Windows7 & /usr/bin/python /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/clients.db fi fi diff --git a/docker/docker-py3-kms/run-py3-kms.sh b/docker/docker-py3-kms/run-py3-kms.sh index d781411..c96bcdc 100755 --- a/docker/docker-py3-kms/run-py3-kms.sh +++ b/docker/docker-py3-kms/run-py3-kms.sh @@ -13,5 +13,4 @@ docker run -d --name py3-kms \ -e LOGSIZE=2 \ -v /etc/localtime:/etc/localtime:ro \ -v /var/log:/var/log:rw \ - --restart unless-stopped pykms/pykms:py3-kms -# --restart unless-stopped ekonprof18/pykms:py3-kms + --restart unless-stopped pykms/pykms:py3-kms \ No newline at end of file diff --git a/docker/docker-py3-kms/start.sh b/docker/docker-py3-kms/start.sh index 66fa2e0..2180747 100644 --- a/docker/docker-py3-kms/start.sh +++ b/docker/docker-py3-kms/start.sh @@ -39,7 +39,7 @@ else if [ "$LOGSIZE" == "" ]; then /bin/bash -c "/usr/bin/python3 pykms_Server.py ${IP} ${PORT} -e ${EPID} -l ${LCID} -c ${CLIENT_COUNT} -a ${ACTIVATION_INTERVAL} -r ${RENEWAL_INTERVAL} -s -w ${HWID} -V ${LOGLEVEL} -F ${LOGFILE} &" - sleep5 + sleep 5 /usr/bin/python3 pykms_Client.py ${IP} ${PORT} -m Windows10 & /usr/bin/python3 /home/sqlite_web/sqlite_web.py -H ${IP} -x ${PWD}/clients.db --read-only else 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 " +__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_Base.py b/py-kms/pykms_Base.py index 5143de8..15c4ff9 100644 --- a/py-kms/pykms_Base.py +++ b/py-kms/pykms_Base.py @@ -2,8 +2,6 @@ import binascii import logging -import os -import sys import time import uuid import socket @@ -13,7 +11,7 @@ from pykms_DB2Dict import kmsDB2Dict from pykms_PidGenerator import epidGenerator from pykms_Filetimes import filetime_to_dt from pykms_Sql import sql_initialize, sql_update, sql_update_epid -from pykms_Format import justify, byterize, enco, deco, ShellMessage +from pykms_Format import justify, byterize, enco, deco, pretty_printer #-------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -111,8 +109,8 @@ class kmsBase: def serverLogic(self, kmsRequest): if self.srv_config['sqlite'] and self.srv_config['dbSupport']: self.dbName = sql_initialize() - - ShellMessage.Process(15).run() + + pretty_printer(num_text = 15, where = "srv") kmsRequest = byterize(kmsRequest) loggersrv.debug("KMS Request Bytes: \n%s\n" % justify(deco(binascii.b2a_hex(enco(str(kmsRequest), 'latin-1')), 'latin-1'))) loggersrv.debug("KMS Request: \n%s\n" % justify(kmsRequest.dump(print_to_stdout = False))) @@ -130,10 +128,12 @@ class kmsBase: tz = get_localzone() local_dt = tz.localize(requestDatetime) except UnknownTimeZoneError: - loggersrv.warning('Unknown time zone ! Request time not localized.') + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}Unknown time zone ! Request time not localized.{end}") local_dt = requestDatetime except ImportError: - loggersrv.warning('Module "tzlocal" not available ! Request time not localized.') + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}Module 'tzlocal' not available ! Request time not localized.{end}") local_dt = requestDatetime # Activation threshold. @@ -144,15 +144,19 @@ class kmsBase: if 0 < self.srv_config["CurrentClientCount"] < MinClients: # fixed to 6 (product server) or 26 (product desktop) currentClientCount = MinClients + 1 - loggersrv.warning("Not enough clients ! Fixed with %s, but activated client could be detected as not genuine !" %currentClientCount) + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}Not enough clients ! Fixed with %s, but activated client \ +could be detected as not genuine !{end}" %currentClientCount) elif MinClients <= self.srv_config["CurrentClientCount"] < RequiredClients: currentClientCount = self.srv_config["CurrentClientCount"] - loggersrv.warning("With count = %s, activated client could be detected as not genuine !" %currentClientCount) + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}With count = %s, activated client could be detected as not genuine !{end}" %currentClientCount) elif self.srv_config["CurrentClientCount"] >= RequiredClients: # fixed to 10 (product server) or 50 (product desktop) currentClientCount = RequiredClients if self.srv_config["CurrentClientCount"] > RequiredClients: - loggersrv.warning("Too many clients ! Fixed with %s" %currentClientCount) + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}Too many clients ! Fixed with %s{end}" %currentClientCount) else: # fixed to 10 (product server) or 50 (product desktop) currentClientCount = RequiredClients @@ -173,14 +177,16 @@ class kmsBase: break except: skuName = skuId - loggersrv.warning("Can't find a name for this product !!") + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}Can't find a name for this product !{end}") try: if uuid.UUID(appitem['Id']) == applicationId: appName = appitem['DisplayName'] except: appName = applicationId - loggersrv.warning("Can't find a name for this application group !!") + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}Can't find a name for this application group !{end}") infoDict = { "machineName" : kmsRequest.getMachineName(), diff --git a/py-kms/pykms_Client.py b/py-kms/pykms_Client.py index bef52dd..f208680 100644 --- a/py-kms/pykms_Client.py +++ b/py-kms/pykms_Client.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import re -import argparse import binascii import datetime import random @@ -11,7 +10,7 @@ import sys import uuid import logging import os -import errno +import threading import pykms_RpcBind, pykms_RpcRequest from pykms_Filetimes import dt_to_filetime @@ -23,12 +22,26 @@ from pykms_RequestV6 import kmsRequestV6 from pykms_RpcBase import rpcBase from pykms_DB2Dict import kmsDB2Dict from pykms_Misc import logger_create, check_logfile -from pykms_Format import justify, byterize, enco, deco, ShellMessage +from pykms_Misc import KmsParser, KmsException, KmsHelper +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 " +__url__ = "https://github.com/SystemRage/py-kms" +clt_description = "py-kms: KMS Client Emulator written in Python" clt_config = {} +#--------------------------------------------------------------------------------------------------------------------------------------------------------- +class client_thread(threading.Thread): + def __init__(self, name): + threading.Thread.__init__(self) + self.name = name + self.with_gui = False + + def run(self): + clt_main(with_gui = self.with_gui) + #--------------------------------------------------------------------------------------------------------------------------------------------------------- loggerclt = logging.getLogger('logclt') @@ -48,33 +61,48 @@ 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"}, } def client_options(): - parser = argparse.ArgumentParser(description = clt_description, epilog = 'version: ' + clt_version) - parser.add_argument("ip", nargs = "?", action = "store", default = clt_options['ip']['def'], help = clt_options['ip']['help'], type = str) - parser.add_argument("port", nargs = "?", action = "store", default = clt_options['port']['def'], help = clt_options['port']['help'], type = int) - parser.add_argument("-m", "--mode", dest = clt_options['mode']['des'], default = clt_options['mode']['def'], choices = clt_options['mode']['choi'], - help = clt_options['mode']['help'], type = str) - parser.add_argument("-c", "--cmid", dest = clt_options['cmid']['des'], default = clt_options['cmid']['def'], help = clt_options['cmid']['help'], type = str) - parser.add_argument("-n", "--name", dest = clt_options['name']['des'] , default = clt_options['name']['def'], help = clt_options['name']['help'], type = str) - parser.add_argument("-V", "--loglevel", dest = clt_options['llevel']['des'], action = "store", choices = clt_options['llevel']['choi'], - default = clt_options['llevel']['def'], help = clt_options['llevel']['help'], type = str) - parser.add_argument("-F", "--logfile", nargs = "+", dest = clt_options['lfile']['des'], default = clt_options['lfile']['def'], - help = clt_options['lfile']['help'], type = str) - parser.add_argument("-S", "--logsize", dest = clt_options['lsize']['des'], action = "store", default = clt_options['lsize']['def'], - help = clt_options['lsize']['help'], type = float) - - clt_config.update(vars(parser.parse_args())) - # Check logfile. - clt_config['logfile'] = check_logfile(clt_config['logfile'], clt_options['lfile']['def'], loggerclt) - + try: + client_parser = KmsParser(description = clt_description, epilog = 'version: ' + clt_version, add_help = False, allow_abbrew = False) + except TypeError: + client_parser = KmsParser(description = clt_description, epilog = 'version: ' + clt_version, add_help = False) + client_parser.add_argument("ip", nargs = "?", action = "store", default = clt_options['ip']['def'], + help = clt_options['ip']['help'], type = str) + client_parser.add_argument("port", nargs = "?", action = "store", default = clt_options['port']['def'], + help = clt_options['port']['help'], type = int) + client_parser.add_argument("-m", "--mode", dest = clt_options['mode']['des'], default = clt_options['mode']['def'], + choices = clt_options['mode']['choi'], help = clt_options['mode']['help'], type = str) + client_parser.add_argument("-c", "--cmid", dest = clt_options['cmid']['des'], default = clt_options['cmid']['def'], + help = clt_options['cmid']['help'], type = str) + client_parser.add_argument("-n", "--name", dest = clt_options['name']['des'] , default = clt_options['name']['def'], + help = clt_options['name']['help'], type = str) + client_parser.add_argument("-V", "--loglevel", dest = clt_options['llevel']['des'], action = "store", + choices = clt_options['llevel']['choi'], default = clt_options['llevel']['def'], + help = clt_options['llevel']['help'], type = str) + client_parser.add_argument("-F", "--logfile", nargs = "+", action = "store", dest = clt_options['lfile']['des'], + default = clt_options['lfile']['def'], help = clt_options['lfile']['help'], type = str) + client_parser.add_argument("-S", "--logsize", dest = clt_options['lsize']['des'], action = "store", + default = clt_options['lsize']['def'], help = clt_options['lsize']['help'], type = float) + client_parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit") + + try: + if "-h" in sys.argv[1:]: + KmsHelper().printer(parsers = [client_parser]) + clt_config.update(vars(client_parser.parse_args())) + except KmsException as e: + pretty_printer(put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e), to_exit = True) def client_check(): + # Check logfile. + clt_config['logfile'] = check_logfile(clt_config['logfile'], clt_options['lfile']['def'], where = "clt") + # Setup hidden or not messages. ShellMessage.view = ( False if any(i in ['STDOUT', 'FILESTDOUT'] for i in clt_config['logfile']) else True ) + # Create log. logger_create(loggerclt, clt_config, mode = 'a') @@ -83,14 +111,13 @@ def client_check(): try: uuid.UUID(clt_config['cmid']) except ValueError: - loggerclt.error("Bad CMID. Exiting...") - sys.exit() - + pretty_printer(log_obj = loggerclt.error, to_exit = True, where = "clt", + put_text = "{reverse}{red}{bold}Bad CMID. Exiting...{end}") # Check machineName. if clt_config['machineName'] is not None: if len(clt_config['machineName']) < 2 or len(clt_config['machineName']) > 63: - loggerclt.error("machineName must be between 2 and 63 characters in length.") - sys.exit() + pretty_printer(log_obj = loggerclt.error, to_exit = True, where = "clt", + put_text = "{reverse}{red}{bold}machineName must be between 2 and 63 characters in length. Exiting...{end}") clt_config['call_id'] = 1 @@ -125,32 +152,47 @@ def client_create(): loggerclt.info("Connection successful !") binder = pykms_RpcBind.handler(None, clt_config) RPC_Bind = enco(str(binder.generateRequest()), 'latin-1') - loggerclt.info("Sending RPC bind request...") - ShellMessage.Process([-1, 1]).run() - s.send(RPC_Bind) + try: - ShellMessage.Process([-4, 7]).run() - bindResponse = s.recv(1024) + loggerclt.info("Sending RPC bind request...") + pretty_printer(num_text = [-1, 1], where = "clt") + s.send(RPC_Bind) except socket.error as e: - if e.errno == errno.ECONNRESET: - loggerclt.error("Connection reset by peer. Exiting...") - sys.exit() - else: - raise - if bindResponse == '' or not bindResponse: - loggerclt.error("No data received ! Exiting...") - sys.exit() + pretty_printer(log_obj = loggerclt.error, to_exit = True, where = "clt", + put_text = "{reverse}{red}{bold}While sending: %s{end}" %str(e)) + try: + bindResponse = s.recv(1024) + if bindResponse == '' or not bindResponse: + pretty_printer(log_obj = loggerclt.warning, to_exit = True, where = "clt", + put_text = "{reverse}{yellow}{bold}No data received.{end}") + pretty_printer(num_text = [-4, 7], where = "clt") + except socket.error as e: + pretty_printer(log_obj = loggerclt.error, to_exit = True, where = "clt", + put_text = "{reverse}{red}{bold}While receiving: %s{end}" %str(e)) + packetType = MSRPCHeader(bindResponse)['type'] if packetType == rpcBase.packetType['bindAck']: loggerclt.info("RPC bind acknowledged.") - ShellMessage.Process(8).run() + pretty_printer(num_text = 8, where = "clt") kmsRequest = createKmsRequest() requester = pykms_RpcRequest.handler(kmsRequest, clt_config) - s.send(enco(str(requester.generateRequest()), 'latin-1')) - ShellMessage.Process([-1, 12]).run() - response = s.recv(1024) + + try: + loggerclt.info("Sending RPC activation request...") + RPC_Actv = enco(str(requester.generateRequest()), 'latin-1') + pretty_printer(num_text = [-1, 12], where = "clt") + s.send(RPC_Actv) + except socket.error as e: + pretty_printer(log_obj = loggerclt.error, to_exit = True, where = "clt", + put_text = "{reverse}{red}{bold}While sending: %s{end}" %str(e)) + try: + response = s.recv(1024) + pretty_printer(num_text = [-4, 20], where = "clt") + except socket.error as e: + pretty_printer(log_obj = loggerclt.error, to_exit = True, where = "clt", + put_text = "{reverse}{red}{bold}While receiving: %s{end}" %str(e)) + loggerclt.debug("Response: \n%s\n" % justify(deco(binascii.b2a_hex(response), 'latin-1'))) - ShellMessage.Process([-4, 20]).run() parsed = MSRPCRespHeader(response) kmsData = readKmsResponse(parsed['pduData'], kmsRequest, clt_config) kmsResp = kmsData['response'] @@ -169,16 +211,15 @@ def client_create(): loggerclt.mini("", extra = {'host': socket.gethostname() + " [" + clt_config["ip"] + "]", 'status' : "Activated", 'product' : clt_config["mode"]}) - - ShellMessage.Process(21).run() + + pretty_printer(num_text = 21, where = "clt") elif packetType == rpcBase.packetType['bindNak']: loggerclt.info(justify(MSRPCBindNak(bindResponse).dump(print_to_stdout = False))) - sys.exit() + sys.exit(0) else: - loggerclt.critical("Something went wrong.") - sys.exit() - + pretty_printer(log_obj = loggerclt.warning, to_exit = True, where = "clt", + put_text = "{reverse}{magenta}{bold}Something went wrong.{end}") def clt_main(with_gui = False): if not with_gui: @@ -211,7 +252,7 @@ def createKmsRequestBase(): requestDict['mnPad'] = '\0'.encode('utf-16le') * (63 - len(requestDict['machineName'].decode('utf-16le'))) # Debug Stuff - ShellMessage.Process(9).run() + pretty_printer(num_text = 9, where = "clt") requestDict = byterize(requestDict) loggerclt.debug("Request Base Dictionary: \n%s\n" % justify(requestDict.dump(print_to_stdout = False))) diff --git a/py-kms/pykms_Format.py b/py-kms/pykms_Format.py index 32be4cd..080609b 100644 --- a/py-kms/pykms_Format.py +++ b/py-kms/pykms_Format.py @@ -3,7 +3,8 @@ from __future__ import print_function, unicode_literals import re import sys -import threading +import os +from collections import OrderedDict try: # Python 2.x imports @@ -20,19 +21,16 @@ pyver = sys.version_info[:2] def enco(strg, typ = 'latin-1'): if pyver >= (3, 0): if isinstance(strg, str): - strgenc = strg.encode(typ) - return strgenc + return strg.encode(typ) else: - return strg + return strg def deco(strg, typ = 'latin-1'): if pyver >= (3, 0): if isinstance(strg, bytes): - strgdec = strg.decode(typ) - return strgdec + return strg.decode(typ) else: - return strg - + return strg def byterize(obj): @@ -94,68 +92,97 @@ ExtraMap = {'end' : '\x1b[0m', } ColorExtraMap = dict(ColorMap, **ExtraMap) +ColorMapReversed = dict(zip(ColorMap.values(), ColorMap.keys())) +ExtraMapReversed = dict(zip(ExtraMap.values(), ExtraMap.keys())) -MsgMap = {0 : {'text' : "{yellow}\n\t\t\tClient generating RPC Bind Request...{end}", 'where' : "clt"}, - 1 : {'text' : "{white}<==============={end}{yellow}\tClient sending RPC Bind Request...{end}", 'where' : "clt"}, - 2 : {'text' : "{yellow}Server received RPC Bind Request !!!\t\t\t\t{end}{white}<==============={end}", 'where' : "srv"}, - 3 : {'text' : "{yellow}Server parsing RPC Bind Request...{end}", 'where' : "srv"}, - 4 : {'text' : "{yellow}Server generating RPC Bind Response...{end}", 'where' : "srv"}, - 5 : {'text' : "{yellow}Server sending RPC Bind Response...\t\t\t\t{end}{white}===============>{end}", 'where' : "srv"}, - 6 : {'text' : "{green}{bold}RPC Bind acknowledged !!!\n\n{end}", 'where' : "srv"}, - 7 : {'text' : "{white}===============>{end}{yellow}\tClient received RPC Bind Response !!!{end}", 'where' : "clt"}, - 8 : {'text' : "{green}{bold}\t\t\tRPC Bind acknowledged !!!\n{end}", 'where' : "clt"}, - 9 : {'text' : "{blue}\t\t\tClient generating Activation Request dictionary...{end}", 'where' : "clt"}, - 10 : {'text' : "{blue}\t\t\tClient generating Activation Request data...{end}", 'where' : "clt"}, - 11 : {'text' : "{blue}\t\t\tClient generating RPC Activation Request...{end}", 'where' : "clt"}, - 12 : {'text' : "{white}<==============={end}{blue}\tClient sending RPC Activation Request...\n\n{end}", 'where' : "clt"}, - 13 : {'text' : "{blue}Server received RPC Activation Request !!!\t\t\t{end}{white}<==============={end}", 'where' : "srv"}, - 14 : {'text' : "{blue}Server parsing RPC Activation Request...{end}", 'where' : "srv"}, - 15 : {'text' : "{blue}Server processing KMS Activation Request...{end}", 'where' : "srv"}, - 16 : {'text' : "{blue}Server processing KMS Activation Response...{end}", 'where' : "srv"}, - 17 : {'text' : "{blue}Server generating RPC Activation Response...{end}", 'where' : "srv"}, - 18 : {'text' : "{blue}Server sending RPC Activation Response...\t\t\t{end}{white}===============>{end}", 'where' : "srv"}, - 19 : {'text' : "{green}{bold}Server responded, now in Stand by...\n{end}", 'where' : "srv"}, - 20 : {'text' : "{white}===============>{end}{blue}\tClient received Response !!!{end}", 'where' : "clt"}, - 21 : {'text' : "{green}{bold}\t\t\tActivation Done !!!{end}", 'where' : "clt"}, - -1 : {'text' : "{white}Server receiving{end}", 'where' : "clt"}, - -2 : {'text' : "{white}\n\n\t\t\t\t\t\t\t\tClient sending{end}", 'where' : "srv"}, - -3 : {'text' : "{white}\t\t\t\t\t\t\t\tClient receiving{end}", 'where' : "srv"}, - -4 : {'text' : "{white}\n\nServer sending{end}", 'where' : "clt"}, - - 40 : {'text' : "{red}{bold}Server connection timed out. Exiting...{end}", 'where' : "srv"}, - 41 : {'text' : "{red}{bold}HWID '{0}' is invalid. Digit {1} non hexadecimal. Exiting...{end}", 'where' : "srv"}, - 42 : {'text' : "{red}{bold}HWID '{0}' is invalid. Hex string is odd length. Exiting...{end}", 'where' : "srv"}, - 43 : {'text' : "{red}{bold}HWID '{0}' is invalid. Hex string is too short. Exiting...{end}", 'where' : "srv"}, - 44 : {'text' : "{red}{bold}HWID '{0}' is invalid. Hex string is too long. Exiting...{end}", 'where' : "srv"}, - 45 : {'text' : "{red}{bold}Port number '{0}' is invalid. Enter between 1 - 65535. Exiting...{end}", 'where' : "srv"}, - 46 : {'text' : "{red}{bold}{0}. Exiting...{end}", 'where' : "srv"}, +MsgMap = {0 : {'text' : "{yellow}\n\t\t\tClient generating RPC Bind Request...{end}", 'align' : ()}, + 1 : {'text' : "{white}<==============={end}{yellow}\tClient sending RPC Bind Request...{end}", 'align' : ()}, + 2 : {'text' : "{yellow}Server received RPC Bind Request !!!\t\t\t\t{end}{white}<==============={end}", 'align' : ()}, + 3 : {'text' : "{yellow}Server parsing RPC Bind Request...{end}", 'align' : ()}, + 4 : {'text' : "{yellow}Server generating RPC Bind Response...{end}", 'align' : ()}, + 5 : {'text' : "{yellow}Server sending RPC Bind Response...\t\t\t\t{end}{white}===============>{end}", 'align' : ()}, + 6 : {'text' : "{green}{bold}\nRPC Bind acknowledged !!!{end}", 'align' : ()}, + 7 : {'text' : "{white}===============>{end}{yellow}\tClient received RPC Bind Response !!!{end}", 'align' : ()}, + 8 : {'text' : "{green}{bold}\t\t\tRPC Bind acknowledged !!!{end}", 'align' : ()}, + 9 : {'text' : "{blue}\t\t\tClient generating Activation Request dictionary...{end}", 'align' : ()}, + 10 : {'text' : "{blue}\t\t\tClient generating Activation Request data...{end}", 'align' : ()}, + 11 : {'text' : "{blue}\t\t\tClient generating RPC Activation Request...{end}", 'align' : ()}, + 12 : {'text' : "{white}<==============={end}{blue}\tClient sending RPC Activation Request...{end}", 'align' : ()}, + 13 : {'text' : "{blue}Server received RPC Activation Request !!!\t\t\t{end}{white}<==============={end}", 'align' : ()}, + 14 : {'text' : "{blue}Server parsing RPC Activation Request...{end}", 'align' : ()}, + 15 : {'text' : "{blue}Server processing KMS Activation Request...{end}", 'align' : ()}, + 16 : {'text' : "{blue}Server processing KMS Activation Response...{end}", 'align' : ()}, + 17 : {'text' : "{blue}Server generating RPC Activation Response...{end}", 'align' : ()}, + 18 : {'text' : "{blue}Server sending RPC Activation Response...\t\t\t{end}{white}===============>{end}", 'align' : ()}, + 19 : {'text' : "{green}{bold}\nServer responded, now in Stand by...\n{end}", 'align' : ()}, + 20 : {'text' : "{white}===============>{end}{blue}\tClient received Response !!!{end}", 'align' : ()}, + 21 : {'text' : "{green}{bold}\t\t\tActivation Done !!!{end}", 'align' : ()}, + -1 : {'text' : "{white}Server receiving{end}", 'align' : ()}, + -2 : {'text' : "{white}\t\t\t\t\t\t\t\tClient sending{end}", 'align' : ()}, + -3 : {'text' : "{white}\t\t\t\t\t\t\t\tClient receiving{end}", 'align' : ()}, + -4 : {'text' : "{white}Server sending{end}", 'align' : ()}, } -def pick_MsgMap(messagelist): +def unformat_message(symbolic_string_list): + """ `symbolic_string_list` : a list of strings with symbolic formattation, example: + symbolic_string_list = ["{yellow}\tPluto\n{end}", + "{reverse}{blue}======>{end}{red}\t\tPaperino{end}"] + >>> unformat_message(symbolic_string_list) + >>> [['\tPluto\n'], ['======>', '\t\tPaperino']] + """ pattern = r"(?>> unshell_message(ansi_string count = 0) + >>> ({'tag00': {'color': 'white', 'extra': [], 'text': 'Pippo'}, + 'tag01': {'color': 'blue', 'extra': [], 'text': 'Pluto\t\t'} + 'tag02': {'color': 'green', 'extra': ['bold'], 'text': '\nPaperino\n'} + }, 3) + """ + ansi_find = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + ansi_list = re.findall(ansi_find, ansi_string) + ansi_indx_start = [ n for n in range(len(ansi_string)) for ansi in list(set(ansi_list)) if ansi_string.find(ansi, n) == n ] + ansi_indx_stop = [ n + len(value) for n, value in zip(ansi_indx_start, ansi_list)] + ansi_indx = sorted(list(set(ansi_indx_start + ansi_indx_stop))) + + msgcolored = {} + + for k in range(len(ansi_indx) - 1): + ansi_value = ansi_string[ansi_indx[k] : ansi_indx[k + 1]] + if ansi_value not in ['\x1b[0m', '\n']: + tagname = "tag" + str(count).zfill(2) + if tagname not in msgcolored: + msgcolored[tagname] = {'color' : '', 'extra' : [], 'text' : ''} + + if ansi_value in ColorMapReversed.keys(): + msgcolored[tagname]['color'] = ColorMapReversed[ansi_value] + elif ansi_value in ExtraMapReversed.keys(): + msgcolored[tagname]['extra'].append(ExtraMapReversed[ansi_value]) + else: + msgcolored[tagname]['text'] = ansi_value + else: + if ansi_value != '\n': + count += 1 + # Ordering. + msgcolored = OrderedDict(sorted(msgcolored.items())) + + return msgcolored, count - if txt[0][0] in arrows: - unMsgMap.update({txt[1][0] : values['where']}) - else: - unMsgMap.update({txt[0][0] : values['where']}) - return unMsgMap - #------------------------------------------------------------------------------------------------------------------------------------------------------- # https://stackoverflow.com/questions/230751/how-to-flush-output-of-print-function if pyver < (3, 3): @@ -168,75 +195,135 @@ if pyver < (3, 3): file = kwargs.get('file', sys.stdout) file.flush() if file is not None else sys.stdout.flush() -# https://ryanjoneil.github.io/posts/2014-02-14-capturing-stdout-in-a-python-child-process.html +# based on: https://ryanjoneil.github.io/posts/2014-02-14-capturing-stdout-in-a-python-child-process.html, +# but not using threading/multiprocessing so: +# 1) message visualization order preserved. +# 2) newlines_count function output not wrong. class ShellMessage(object): view = True + count, remain, numlist = (0, 0, []) class Collect(StringIO): # Capture string sent to stdout. def write(self, s): StringIO.write(self, s) - + class Process(object): - def __init__(self, nshell, get_text = False, put_text = None): + def __init__(self, nshell, get_text = False, put_text = None, where = 'srv'): self.nshell = nshell - self.print_queue = Queue.Queue() self.get_text = get_text self.put_text = put_text + self.where = where self.plaintext = [] + self.path = os.path.dirname(os.path.abspath( __file__ )) + '/pykms_newlines.txt' + self.print_queue = Queue.Queue() - if not isinstance(nshell, list): - self.nshell = [nshell] - if not isinstance(put_text, list): - self.put_text = [put_text] - - def formatter(self, num): - if self.put_text is None: - self.msg = MsgMap[num]['text'].format(**ColorExtraMap) + def formatter(self, msgtofrmt): + if self.newlines: + text = unformat_message([msgtofrmt])[0][0] + msgtofrmt = msgtofrmt['text'].replace(text, self.newlines * '\n' + text) + self.newlines = 0 else: - self.msg = MsgMap[num]['text'].format(*self.put_text, **ColorExtraMap) - + try: + # comes from MsgMap. + msgtofrmt = msgtofrmt['text'] + except: + # comes from `put_text` option. + pass + self.msgfrmt = msgtofrmt.format(**ColorExtraMap) if self.get_text: - self.plaintext.append(unshell_message(self.msg, m = 0)[0]["tag00"]['text']) + self.plaintext.append(unshell_message(self.msgfrmt, count = 0)[0]["tag00"]['text']) - def run(self): + def newlines_file(self, mode, *args): + try: + with open(self.path, mode) as file: + if mode in ['w', 'a']: + file.write(args[0]) + elif mode == 'r': + data = [int(i) for i in [line.rstrip('\n') for line in file.readlines()]] + self.newlines, ShellMessage.remain = data[0], sum(data[1:]) + except: + with open(self.path, 'w') as file: + pass + + def newlines_count(self, num): + ShellMessage.count += MsgMap[num]['text'].count('\n') + if num >= 0: + ShellMessage.numlist.append(num) + if self.continuecount: + # Note: is bypassed '\n' counted after message with arrow, + # so isn't: str(len(ShellMessage.numlist) + ShellMessage.count) + towrite = str(len(ShellMessage.numlist)) + '\n' + self.newlines_file('a', towrite) + ShellMessage.count, ShellMessage.numlist = (0, []) + else: + ShellMessage.count += (len(ShellMessage.numlist) - ShellMessage.remain) * 2 + if num in [-1, -3]: + towrite = str(ShellMessage.count) + '\n' + self.newlines_file('w', towrite) + ShellMessage.count, ShellMessage.remain, ShellMessage.numlist = (0, 0, []) + self.continuecount = True + elif num in [-2 ,-4]: + self.newlines_file('r') + if num == 21: + ShellMessage.count, ShellMessage.remain, ShellMessage.numlist = (0, 0, []) + os.remove(self.path) + + def run(self): + # view = False part. if not ShellMessage.view: if self.get_text: - for num in self.nshell: - self.formatter(num) + self.newlines = 0 + if self.put_text is not None: + for msg in self.put_text: + self.formatter(msg) + else: + for num in self.nshell: + self.formatter(MsgMap[num]) return self.plaintext else: return - - # Start thread process. - print_thread = threading.Thread(target = self.spawn(), args=(self.print_queue,)) - print_thread.setDaemon(True) - print_thread.start() - # Do something with output. - toprint = self.read(0.1) # 0.1 s to let the shell output the result + # Do job. + self.produce() + toprint = self.consume(timeout = 0.1) # Redirect output. if sys.stdout.isatty(): print(toprint) else: try: - from pykms_GuiBase import gui_redirect # Import after variables creation. - gui_redirect(toprint) + # Import after variables creation. + from pykms_GuiBase import gui_redirect + gui_redirect(toprint, self.where) except: print(toprint) # Get string/s printed. if self.get_text: return self.plaintext - - def spawn(self): + + def produce(self): # Save everything that would otherwise go to stdout. outstream = ShellMessage.Collect() sys.stdout = outstream try: + self.continuecount = False + self.newlines = 0 + # Print something. - for num in self.nshell: - self.formatter(num) - print(self.msg, flush = True) + if self.put_text is not None: + for msg in self.put_text: + ShellMessage.count += msg.count('\n') + # Append a dummy element. + ShellMessage.numlist.append('put') + self.formatter(msg) + print(self.msgfrmt, end = '\n', flush = True) + else: + for num in self.nshell: + self.newlines_count(num) + self.formatter(MsgMap[num]) + print(self.msgfrmt, end = '\n', flush = True) + except Exception as e: + print(e, end = '\n', flush = True) finally: # Restore stdout and send content. sys.stdout = sys.__stdout__ @@ -245,7 +332,7 @@ class ShellMessage(object): except Queue.Full: pass - def read(self, timeout = None): + def consume(self, timeout = None): try: toprint = self.print_queue.get(block = timeout is not None, timeout = timeout) self.print_queue.task_done() @@ -253,34 +340,54 @@ class ShellMessage(object): except Queue.Empty: return None +def pretty_printer(**kwargs): + """kwargs: + `log_obj` --> if logging object specified the text not ansi + formatted is logged. + `get_text` --> if True obtain text not ansi formatted, + after printing it with ansi formattation. + `put_text` --> a string or list of strings with ansi formattation. + if None refer to `num_text` for printing process. + `num_text` --> a number or list of numbers refering numbered message map. + if None `put_text` must be defined for printing process. + `to_exit ` --> if True system exit is called. + `where` --> specifies if message is server-side or client-side + (useful for GUI redirect). + """ + # Set defaults for not defined options. + options = {'log_obj' : None, + 'get_text' : False, + 'put_text' : None, + 'num_text' : None, + 'to_exit' : False, + 'where' : 'srv' + } + options.update(kwargs) + # Check options. + if (options['num_text'] is None) and (options['put_text'] is None): + raise ValueError('One of `num_text` and `put_text` must be provided.') + elif (options['num_text'] is not None) and (options['put_text'] is not None): + raise ValueError('These parameters are mutually exclusive.') -def unshell_message(ansi_string, m): - ansi_find = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') - ansi_list = re.findall(ansi_find, ansi_string) - ansi_indx_start = [ n for n in range(len(ansi_string)) for ansi in list(set(ansi_list)) if ansi_string.find(ansi, n) == n ] - ansi_indx_stop = [ n + len(value) for n, value in zip(ansi_indx_start, ansi_list)] - ansi_indx = sorted(list(set(ansi_indx_start + ansi_indx_stop))) + if (options['num_text'] is not None) and (not isinstance(options['num_text'], list)): + options['num_text'] = [options['num_text']] + if (options['put_text'] is not None) and (not isinstance(options['put_text'], list)): + options['put_text'] = [options['put_text']] - msgcolored = {} - ColorMapReversed = dict(zip(ColorMap.values(), ColorMap.keys())) - ExtraMapReversed = dict(zip(ExtraMap.values(), ExtraMap.keys())) + # Overwrite `get_text` (used as hidden). + if options['put_text']: + options['get_text'] = True + elif options['num_text']: + options['get_text'] = False - for k in range(len(ansi_indx) - 1): - ansi_value = ansi_string[ansi_indx[k] : ansi_indx[k + 1]] - if ansi_value != '\x1b[0m': - tagname = "tag" + str(m).zfill(2) - if tagname not in msgcolored: - msgcolored[tagname] = {'color' : '', 'extra' : [], 'text' : ''} - - if ansi_value in ColorMapReversed.keys(): - msgcolored[tagname]['color'] = ColorMapReversed[ansi_value] - elif ansi_value in ExtraMapReversed.keys(): - msgcolored[tagname]['extra'].append(ExtraMapReversed[ansi_value]) - else: - msgcolored[tagname]['text'] = ansi_value - else: - m += 1 - # Ordering. - msgcolored = dict(sorted(msgcolored.items())) - - return msgcolored, m + # Process messages. + plain_messages = ShellMessage.Process(options['num_text'], + get_text = options['get_text'], + put_text = options['put_text'], + where = options['where']).run() + + if options['log_obj']: + for plain_message in plain_messages: + options['log_obj'](plain_message) + if options['to_exit']: + sys.exit(1) diff --git a/py-kms/pykms_GuiBase.py b/py-kms/pykms_GuiBase.py index 9d0c819..72df2cc 100644 --- a/py-kms/pykms_GuiBase.py +++ b/py-kms/pykms_GuiBase.py @@ -3,6 +3,7 @@ import os import sys import threading +from time import sleep try: # Python 2.x imports @@ -19,12 +20,15 @@ except ImportError: from tkinter import filedialog import tkinter.font as tkFont -from pykms_Server import srv_options, srv_version, srv_config, serverqueue, serverthread -from pykms_GuiMisc import ToolTip, TextDoubleScroll, TextRedirect, custom_background, make_clear -from pykms_Client import clt_options, clt_version, clt_config, clt_main +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 " +__url__ = "https://github.com/SystemRage/py-kms" +gui_description = "A GUI for py-kms." ##--------------------------------------------------------------------------------------------------------------------------------------------------------- def get_ip_address(): @@ -40,26 +44,14 @@ def get_ip_address(): import socket ip = socket.gethostbyname(socket.gethostname()) else: - ip = '' - print('Error: Couldn\'t get local ip') + ip = 'Unknown' return ip - -def switch_dir(path): - if os.path.isdir(path): - os.chdir(path) - return True - - if path == '': - os.chdir(os.getcwd()) - return True - else: - return -def gui_redirect(str_to_print): - global txsrv, txclt, txcol, rclt - +def gui_redirect(str_to_print, where): + global txsrv, txclt, txcol + try: - TextRedirect.StdoutRedirect(txsrv, txclt, txcol, rclt, str_to_print) + TextRedirect.StdoutRedirect(txsrv, txclt, txcol, str_to_print, where) except: print(str_to_print) @@ -76,12 +68,14 @@ class KmsGui(tk.Tk): def __init__(self, *args, **kwargs): tk.Tk.__init__(self, *args, **kwargs) self.wraplength = 200 - + serverthread.with_gui = True + self.validation_int = self.register(self.validate_int) + ## Define fonts and colors. self.btnwinfont = tkFont.Font(family = 'Times', size = 12, weight = 'bold') self.othfont = tkFont.Font(family = 'Times', size = 9, weight = 'bold') self.optfont = tkFont.Font(family = 'Helvetica', size = 11, weight = 'bold') - self.msgfont = tkFont.Font(family = 'Helvetica', size = 7) + self.msgfont = tkFont.Font(family = 'Monospace', size = 6) # need a monospaced type (like courier, etc..). self.customcolors = { 'black' : '#000000', 'white' : '#FFFFFF', @@ -102,11 +96,10 @@ class KmsGui(tk.Tk): ## Create client gui + other operations. self.gui_complete() ## Create globals for printing process (redirect stdout). - global txsrv, txclt, txcol, rclt + global txsrv, txclt, txcol txsrv = self.textboxsrv.get() txclt = self.textboxclt.get() txcol = self.customcolors - rclt = self.runbtnclt ## Redirect stderr. sys.stderr = TextRedirect.StderrRedirect(txsrv, txclt, txcol) @@ -131,14 +124,14 @@ class KmsGui(tk.Tk): ## Create widgets (btnsrvwin) ----------------------------------------------------------------------------------------------------------- self.statesrv = tk.Label(self.btnsrvwin, text = 'Server\nState:\nStopped', font = self.othfont, foreground = self.customcolors['red']) self.runbtnsrv = tk.Button(self.btnsrvwin, text = 'START\nSERVER', background = self.customcolors['green'], - foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, command = self.srv_clickedmain) + foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, command = self.srv_on_start) self.shbtnclt = tk.Button(self.btnsrvwin, text = 'SHOW\nCLIENT', background = self.customcolors['magenta'], - foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, command = self.clt_showhide) + foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, command = self.clt_on_show) self.clearbtnsrv = tk.Button(self.btnsrvwin, text = 'CLEAR', background = self.customcolors['orange'], foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, - command = lambda: make_clear([txsrv, txclt])) + command = lambda: self.on_clear([txsrv, txclt])) self.exitbtnsrv = tk.Button(self.btnsrvwin, text = 'EXIT', background = self.customcolors['black'], - foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, command = self.destroy) + foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, command = self.on_exit) ## Layout widgets (btnsrvwin) self.statesrv.grid(row = 0, column = 0, padx = 2, pady = 2, sticky = 'ew') @@ -151,72 +144,86 @@ class KmsGui(tk.Tk): # Version. ver = tk.Label(self.optsrvwin, text = 'You are running server version: ' + srv_version, foreground = self.customcolors['red'], font = self.othfont) + self.allopts_srv = [] # Ip Address. - ipaddlbl = tk.Label(self.optsrvwin, text = 'IP Address: ', font = self.optfont) - self.ipadd = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) - self.ipadd.insert('end', srv_options['ip']['def']) - ToolTip(self.ipadd, text = srv_options['ip']['help'], wraplength = self.wraplength) + srvipaddlbl = tk.Label(self.optsrvwin, text = 'IP Address: ', font = self.optfont) + self.srvipadd = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) + self.srvipadd.insert('end', srv_options['ip']['def']) + ToolTip(self.srvipadd, text = srv_options['ip']['help'], wraplength = self.wraplength) myipadd = tk.Label(self.optsrvwin, text = 'Your IP address is: {}'.format(get_ip_address()), foreground = self.customcolors['red'], font = self.othfont) + self.allopts_srv.append(self.srvipadd) # Port. - portlbl = tk.Label(self.optsrvwin, text = 'Port: ', font = self.optfont) - self.port = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) - self.port.insert('end', str(srv_options['port']['def'])) - ToolTip(self.port, text = srv_options['port']['help'], wraplength = self.wraplength) + srvportlbl = tk.Label(self.optsrvwin, text = 'Port: ', font = self.optfont) + self.srvport = tk.Entry(self.optsrvwin, width = 10, font = self.optfont, validate = "key", validatecommand = (self.validation_int, "%S")) + self.srvport.insert('end', str(srv_options['port']['def'])) + ToolTip(self.srvport, text = srv_options['port']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.srvport) # EPID. epidlbl = tk.Label(self.optsrvwin, text = 'EPID: ', font = self.optfont) self.epid = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) self.epid.insert('end', str(srv_options['epid']['def'])) ToolTip(self.epid, text = srv_options['epid']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.epid) # LCID. lcidlbl = tk.Label(self.optsrvwin, text = 'LCID: ', font = self.optfont) - self.lcid = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) + self.lcid = tk.Entry(self.optsrvwin, width = 10, font = self.optfont, validate = "key", validatecommand = (self.validation_int, "%S")) self.lcid.insert('end', str(srv_options['lcid']['def'])) ToolTip(self.lcid, text = srv_options['lcid']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.lcid) # HWID. hwidlbl = tk.Label(self.optsrvwin, text = 'HWID: ', font = self.optfont) self.hwid = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) self.hwid.insert('end', srv_options['hwid']['def']) ToolTip(self.hwid, text = srv_options['hwid']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.hwid) # Client Count countlbl = tk.Label(self.optsrvwin, text = 'Client Count: ', font = self.optfont) self.count = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) self.count.insert('end', str(srv_options['count']['def'])) ToolTip(self.count, text = srv_options['count']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.count) # Activation Interval. activlbl = tk.Label(self.optsrvwin, text = 'Activation Interval: ', font = self.optfont) - self.activ = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) + self.activ = tk.Entry(self.optsrvwin, width = 10, font = self.optfont, validate = "key", validatecommand = (self.validation_int, "%S")) self.activ.insert('end', str(srv_options['activation']['def'])) ToolTip(self.activ, text = srv_options['activation']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.activ) # Renewal Interval. renewlbl = tk.Label(self.optsrvwin, text = 'Activation Interval: ', font = self.optfont) - self.renew = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) + self.renew = tk.Entry(self.optsrvwin, width = 10, font = self.optfont, validate = "key", validatecommand = (self.validation_int, "%S")) self.renew.insert('end', str(srv_options['renewal']['def'])) ToolTip(self.renew, text = srv_options['renewal']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.renew) # Logfile. - filelbl = tk.Label(self.optsrvwin, text = 'Logfile Path / Name: ', font = self.optfont) - self.file = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) - self.file.insert('end', srv_options['lfile']['def']) - ToolTip(self.file, text = srv_options['lfile']['help'], wraplength = self.wraplength) - filebtnwin = tk.Button(self.optsrvwin, text = 'Browse', command = lambda: self.browse(self.file, srv_options)) + srvfilelbl = tk.Label(self.optsrvwin, text = 'Logfile Path / Name: ', font = self.optfont) + self.srvfile = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) + self.srvfile.insert('end', srv_options['lfile']['def']) + self.srvfile.xview_moveto(1) + ToolTip(self.srvfile, text = srv_options['lfile']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.srvfile) + filebtnwin = tk.Button(self.optsrvwin, text = 'Browse', command = lambda: self.browse(self.srvfile, srv_options)) + self.allopts_srv.append(filebtnwin) # Loglevel. - levellbl = tk.Label(self.optsrvwin, text = 'Loglevel: ', font = self.optfont) - self.level = ttk.Combobox(self.optsrvwin, values = tuple(srv_options['llevel']['choi']), width = 10) - self.level.set(srv_options['llevel']['def']) - ToolTip(self.level, text = srv_options['llevel']['help'], wraplength = self.wraplength) + srvlevellbl = tk.Label(self.optsrvwin, text = 'Loglevel: ', font = self.optfont) + self.srvlevel = ttk.Combobox(self.optsrvwin, values = tuple(srv_options['llevel']['choi']), width = 10) + self.srvlevel.set(srv_options['llevel']['def']) + ToolTip(self.srvlevel, text = srv_options['llevel']['help'], wraplength = self.wraplength) + self.allopts_srv.append(self.srvlevel) # Sqlite database. self.chkval = tk.BooleanVar() self.chkval.set(srv_options['sql']['def']) chksql = tk.Checkbutton(self.optsrvwin, text = 'Create Sqlite\nDatabase', font = self.optfont, var = self.chkval) ToolTip(chksql, text = srv_options['sql']['help'], wraplength = self.wraplength) + self.allopts_srv.append(chksql) ## Layout widgets (optsrvwin) ver.grid(row = 0, column = 0, columnspan = 3, padx = 5, pady = 5, sticky = 'ew') - ipaddlbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e') - self.ipadd.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew') + srvipaddlbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e') + self.srvipadd.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew') myipadd.grid(row = 2, column = 1, columnspan = 2, padx = 5, pady = 5, sticky = 'ew') - portlbl.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e') - self.port.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'ew') + srvportlbl.grid(row = 3, column = 0, padx = 5, pady = 5, sticky = 'e') + self.srvport.grid(row = 3, column = 1, padx = 5, pady = 5, sticky = 'ew') epidlbl.grid(row = 4, column = 0, padx = 5, pady = 5, sticky = 'e') self.epid.grid(row = 4, column = 1, padx = 5, pady = 5, sticky = 'ew') lcidlbl.grid(row = 5, column = 0, padx = 5, pady = 5, sticky = 'e') @@ -229,15 +236,15 @@ class KmsGui(tk.Tk): self.activ.grid(row = 8, column = 1, padx = 5, pady = 5, sticky = 'ew') renewlbl.grid(row = 9, column = 0, padx = 5, pady = 5, sticky = 'e') self.renew.grid(row = 9, column = 1, padx = 5, pady = 5, sticky = 'ew') - filelbl.grid(row = 10, column = 0, padx = 5, pady = 5, sticky = 'e') - self.file.grid(row = 10, column = 1, padx = 5, pady = 5, sticky = 'ew') + srvfilelbl.grid(row = 10, column = 0, padx = 5, pady = 5, sticky = 'e') + self.srvfile.grid(row = 10, column = 1, padx = 5, pady = 5, sticky = 'ew') filebtnwin.grid(row = 10, column = 2, padx = 5, pady = 5, sticky = 'ew') - levellbl.grid(row = 11, column = 0, padx = 5, pady = 5, sticky = 'e') - self.level.grid(row = 11, column = 1, padx = 5, pady = 5, sticky = 'ew') + srvlevellbl.grid(row = 11, column = 0, padx = 5, pady = 5, sticky = 'e') + self.srvlevel.grid(row = 11, column = 1, padx = 5, pady = 5, sticky = 'ew') chksql.grid(row = 12, column = 1, padx = 5, pady = 5, sticky = 'ew') ## Create widgets and layout (msgsrvwin) ----------------------------------------------------------------------------------------------- - self.textboxsrv = TextDoubleScroll(self.msgsrvwin, background = self.customcolors['black'], wrap = 'word', state = 'disabled', + self.textboxsrv = TextDoubleScroll(self.msgsrvwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled', relief = 'ridge', font = self.msgfont) self.textboxsrv.put() @@ -286,7 +293,7 @@ class KmsGui(tk.Tk): # Create widgets (btncltwin) ------------------------------------------------------------------------------------------------------------ self.runbtnclt = tk.Button(self.btncltwin, text = 'START\nCLIENT', background = self.customcolors['blue'], foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont, - state = 'disabled', command = self.clt_clickedstart) + state = 'disabled', command = self.clt_on_start) #self.othbutt = tk.Button(self.btncltwin, text = 'Botton\n2', background = self.customcolors['green'], # foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont) @@ -299,42 +306,52 @@ class KmsGui(tk.Tk): # Version. cltver = tk.Label(self.optcltwin, text = 'You are running client version: ' + clt_version, foreground = self.customcolors['red'], font = self.othfont) + self.allopts_clt = [] # Ip Address. cltipaddlbl = tk.Label(self.optcltwin, text = 'IP Address: ', font = self.optfont) self.cltipadd = tk.Entry(self.optcltwin, width = 10, font = self.optfont) self.cltipadd.insert('end', clt_options['ip']['def']) ToolTip(self.cltipadd, text = clt_options['ip']['help'], wraplength = self.wraplength) + self.allopts_clt.append(self.cltipadd) # Port. cltportlbl = tk.Label(self.optcltwin, text = 'Port: ', font = self.optfont) - self.cltport = tk.Entry(self.optcltwin, width = 10, font = self.optfont) + self.cltport = tk.Entry(self.optcltwin, width = 10, font = self.optfont, validate = "key", validatecommand = (self.validation_int, "%S")) self.cltport.insert('end', str(clt_options['port']['def'])) ToolTip(self.cltport, text = clt_options['port']['help'], wraplength = self.wraplength) + self.allopts_clt.append(self.cltport) # Mode. cltmodelbl = tk.Label(self.optcltwin, text = 'Mode: ', font = self.optfont) self.cltmode = ttk.Combobox(self.optcltwin, values = tuple(clt_options['mode']['choi']), width = 10) self.cltmode.set(clt_options['mode']['def']) ToolTip(self.cltmode, text = clt_options['mode']['help'], wraplength = self.wraplength) + self.allopts_clt.append(self.cltmode) # CMID. cltcmidlbl = tk.Label(self.optcltwin, text = 'CMID: ', font = self.optfont) self.cltcmid = tk.Entry(self.optcltwin, width = 10, font = self.optfont) self.cltcmid.insert('end', str(clt_options['cmid']['def'])) ToolTip(self.cltcmid, text = clt_options['cmid']['help'], wraplength = self.wraplength) + self.allopts_clt.append(self.cltcmid) # Machine Name. cltnamelbl = tk.Label(self.optcltwin, text = 'Machine Name: ', font = self.optfont) self.cltname = tk.Entry(self.optcltwin, width = 10, font = self.optfont) self.cltname.insert('end', str(clt_options['name']['def'])) ToolTip(self.cltname, text = clt_options['name']['help'], wraplength = self.wraplength) + self.allopts_clt.append(self.cltname) # Logfile. cltfilelbl = tk.Label(self.optcltwin, text = 'Logfile Path / Name: ', font = self.optfont) self.cltfile = tk.Entry(self.optcltwin, width = 10, font = self.optfont) self.cltfile.insert('end', clt_options['lfile']['def']) + self.cltfile.xview_moveto(1) ToolTip(self.cltfile, text = clt_options['lfile']['help'], wraplength = self.wraplength) + self.allopts_clt.append(self.cltfile) cltfilebtnwin = tk.Button(self.optcltwin, text = 'Browse', command = lambda: self.browse(self.cltfile, clt_options)) + self.allopts_clt.append(cltfilebtnwin) # Loglevel. cltlevellbl = tk.Label(self.optcltwin, text = 'Loglevel: ', font = self.optfont) self.cltlevel = ttk.Combobox(self.optcltwin, values = tuple(clt_options['llevel']['choi']), width = 10) self.cltlevel.set(clt_options['llevel']['def']) ToolTip(self.cltlevel, text = clt_options['llevel']['help'], wraplength = self.wraplength) + self.allopts_clt.append(self.cltlevel) # Layout widgets (optcltwin) cltver.grid(row = 0, column = 0, columnspan = 3, padx = 5, pady = 5, sticky = 'ew') @@ -355,18 +372,34 @@ class KmsGui(tk.Tk): self.cltlevel.grid(row = 7, column = 1, padx = 5, pady = 5, sticky = 'ew') # Create widgets and layout (msgcltwin) ---------------------------------------------------------------------------------------------------------- - self.textboxclt = TextDoubleScroll(self.msgcltwin, background = self.customcolors['black'], wrap = 'word', state = 'disabled', + self.textboxclt = TextDoubleScroll(self.msgcltwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled', relief = 'ridge', font = self.msgfont) self.textboxclt.put() - def proper_none(self, value): + def prep_option(self, value): value = None if value == 'None' else value try: return int(value) - except TypeError: + except (TypeError, ValueError): + # is NONE or is a STRING. return value + + def prep_logfile(self, optionlog): + if optionlog.startswith('FILESTDOUT '): + split = optionlog.split('FILESTDOUT ') + split[0] = 'FILESTDOUT' + return split + elif optionlog.startswith('STDOUT '): + split = optionlog.split('STDOUT ') + split[0] = 'STDOUT' + return split + else: + return optionlog + + def validate_int(self, value): + return value.isdigit() - def clt_showhide(self, force = False): + def clt_on_show(self, force = False): if self.optcltwin.winfo_ismapped() or force: self.shbtnclt['text'] = 'SHOW\nCLIENT' self.optcltwin.grid_remove() @@ -378,87 +411,127 @@ class KmsGui(tk.Tk): self.msgcltwin.grid() self.btncltwin.place(x = self.btncltwin_X, y = self.btncltwin_Y, bordermode = 'inside', anchor = 'nw') - def srv_clickedmain(self): + def srv_on_start(self): if self.runbtnsrv['text'] == 'START\nSERVER': - if self.srv_clickedstart(): - self.runbtnsrv.configure(text = 'STOP\nSERVER', background = self.customcolors['red'], - foreground = self.customcolors['white']) - self.runbtnclt.configure(state = 'normal') - elif self.runbtnsrv['text'] == 'STOP\nSERVER': - self.srv_clickedstop() - self.runbtnsrv.configure(text = 'START\nSERVER', background = self.customcolors['green'], - foreground = self.customcolors['white']) - self.runbtnclt.configure(state = 'disabled') - - def srv_clickedstart(self): - ok = False - if switch_dir(os.path.dirname(self.file.get())): - if self.file.get().lower().endswith('.log'): - # Load dict. - srv_config[srv_options['ip']['des']] = self.ipadd.get() - srv_config[srv_options['port']['des']] = int(self.port.get()) - srv_config[srv_options['epid']['des']] = self.proper_none(self.epid.get()) - srv_config[srv_options['lcid']['des']] = int(self.lcid.get()) - srv_config[srv_options['hwid']['des']] = self.hwid.get() - srv_config[srv_options['count']['des']] = self.proper_none(self.count.get()) - srv_config[srv_options['activation']['des']] = int(self.activ.get()) - srv_config[srv_options['renewal']['des']] = int(self.renew.get()) - srv_config[srv_options['lfile']['des']] = self.file.get() - srv_config[srv_options['llevel']['des']] = self.level.get() - srv_config[srv_options['sql']['des']] = self.chkval.get() - ## TODO. - srv_config[srv_options['lsize']['des']] = 0 - srv_config[srv_options['time']['des']] = 30 - - serverqueue.put('start') - # wait for switch. - while not serverthread.is_running: - pass - self.srv_togglestate() - ok = True - else: - messagebox.showerror('Invalid extension', 'Not a .log file !') - else: - messagebox.showerror('Invalid path', 'Path you have provided not found !') - return ok - - - def srv_clickedstop(self): - if serverthread.is_running: - serverqueue.put('stop') - serverthread.server.shutdown() + self.srv_actions_start() # wait for switch. - while serverthread.is_running: + while not serverthread.is_running_server: pass - self.srv_togglestate() - def srv_togglestate(self): - if serverthread.is_running: + self.on_clear([txsrv, txclt]) + self.srv_toggle_all(on_start = True) + # run thread for interrupting server when an error happens. + self.srv_eject_thread = threading.Thread(target = self.srv_eject, name = "Thread-SrvEjt") + self.srv_eject_thread.setDaemon(True) + self.srv_eject_thread.start() + + elif self.runbtnsrv['text'] == 'STOP\nSERVER': + serverthread.terminate_eject() + + def srv_eject(self): + while not serverthread.eject: + sleep(0.1) + self.srv_actions_stop() + + def srv_actions_start(self): + srv_config[srv_options['ip']['des']] = self.srvipadd.get() + srv_config[srv_options['port']['des']] = self.prep_option(self.srvport.get()) + srv_config[srv_options['epid']['des']] = self.prep_option(self.epid.get()) + srv_config[srv_options['lcid']['des']] = self.prep_option(self.lcid.get()) + srv_config[srv_options['hwid']['des']] = self.hwid.get() + srv_config[srv_options['count']['des']] = self.prep_option(self.count.get()) + srv_config[srv_options['activation']['des']] = self.prep_option(self.activ.get()) + srv_config[srv_options['renewal']['des']] = self.prep_option(self.renew.get()) + srv_config[srv_options['lfile']['des']] = self.prep_logfile(self.srvfile.get()) + srv_config[srv_options['llevel']['des']] = self.srvlevel.get() + srv_config[srv_options['sql']['des']] = self.chkval.get() + + ## TODO. + srv_config[srv_options['lsize']['des']] = 0 + srv_config[srv_options['time']['des']] = None + + serverqueue.put('start') + + def srv_actions_stop(self): + if serverthread.is_running_server: + if serverthread.server is not None: + server_terminate(serverthread, exit_server = True) + # wait for switch. + while serverthread.is_running_server: + pass + else: + serverthread.is_running_server = False + self.srv_toggle_all(on_start = False) + + def srv_toggle_all(self, on_start = True): + self.srv_toggle_state() + if on_start: + self.runbtnsrv.configure(text = 'STOP\nSERVER', background = self.customcolors['red'], + foreground = self.customcolors['white']) + for widget in self.allopts_srv: + widget.configure(state = 'disabled') + self.runbtnclt.configure(state = 'normal') + else: + self.runbtnsrv.configure(text = 'START\nSERVER', background = self.customcolors['green'], + foreground = self.customcolors['white']) + for widget in self.allopts_srv: + widget.configure(state = 'normal') + self.runbtnclt.configure(state = 'disabled') + + def srv_toggle_state(self): + if serverthread.is_running_server: txt, color = ('Server\nState:\nServing', self.customcolors['green']) else: txt, color = ('Server\nState:\nStopped', self.customcolors['red']) self.statesrv.configure(text = txt, foreground = color) - def clt_clickedstart(self): - if switch_dir(os.path.dirname(self.cltfile.get())): - if self.cltfile.get().lower().endswith('.log'): - # Load dict. - clt_config[clt_options['ip']['des']] = self.cltipadd.get() - clt_config[clt_options['port']['des']] = int(self.cltport.get()) - clt_config[clt_options['mode']['des']] = self.cltmode.get() - clt_config[clt_options['cmid']['des']] = self.proper_none(self.cltcmid.get()) - clt_config[clt_options['name']['des']] = self.proper_none(self.cltname.get()) - clt_config[clt_options['lfile']['des']] = self.cltfile.get() - clt_config[clt_options['llevel']['des']] = self.cltlevel.get() - ## TODO - clt_config[clt_options['lsize']['des']] = 0 + def clt_on_start(self): + self.clt_actions_start() + # run thread for disabling interrupt server and client, when client running. + self.clt_eject_thread = threading.Thread(target = self.clt_eject, name = "Thread-CltEjt") + self.clt_eject_thread.setDaemon(True) + self.clt_eject_thread.start() - - clientthread = threading.Thread(target = clt_main, args=(True,)) - clientthread.setDaemon(True) - clientthread.start() + self.on_clear([txsrv, txclt]) + for widget in self.allopts_clt + [self.runbtnsrv, self.runbtnclt]: + widget.configure(state = 'disabled') + + def clt_actions_start(self): + clt_config[clt_options['ip']['des']] = self.cltipadd.get() + clt_config[clt_options['port']['des']] = self.prep_option(self.cltport.get()) + clt_config[clt_options['mode']['des']] = self.cltmode.get() + clt_config[clt_options['cmid']['des']] = self.prep_option(self.cltcmid.get()) + clt_config[clt_options['name']['des']] = self.prep_option(self.cltname.get()) + clt_config[clt_options['llevel']['des']] = self.cltlevel.get() + clt_config[clt_options['lfile']['des']] = self.prep_logfile(self.cltfile.get()) + + ## TODO. + clt_config[clt_options['lsize']['des']] = 0 + + # run client (in a thread). + self.clientthread = client_thread(name = "Thread-Clt") + self.clientthread.setDaemon(True) + self.clientthread.with_gui = True + self.clientthread.start() + + def clt_eject(self): + while self.clientthread.is_alive(): + sleep(0.1) + for widget in self.allopts_clt + [self.runbtnsrv, self.runbtnclt]: + widget.configure(state = 'normal') + + def on_exit(self): + if serverthread.is_running_server: + if serverthread.server is not None: + server_terminate(serverthread, exit_server = True) else: - messagebox.showerror('Invalid extension', 'Not a .log file !') - else: - messagebox.showerror('Invalid path', 'Path you have provided not found !') + serverthread.is_running_server = False + server_terminate(serverthread, exit_thread = True) + self.destroy() + + def on_clear(self, widgetlist): + for widget in widgetlist: + widget.configure(state = 'normal') + widget.delete('1.0', 'end') + widget.configure(state = 'disabled') diff --git a/py-kms/pykms_GuiMisc.py b/py-kms/pykms_GuiMisc.py index 45d62ba..74bcb64 100644 --- a/py-kms/pykms_GuiMisc.py +++ b/py-kms/pykms_GuiMisc.py @@ -16,7 +16,7 @@ except ImportError: from tkinter import ttk import tkinter.font as tkFont -from pykms_Format import unshell_message, MsgMap, pick_MsgMap, unshell_MsgMap +from pykms_Format import MsgMap, unshell_message, unformat_message #--------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -122,108 +122,95 @@ class ToolTip(object): # https://stackoverflow.com/questions/7217715/threadsafe-printing-across-multiple-processes-python-2-x # https://stackoverflow.com/questions/3029816/how-do-i-get-a-thread-safe-print-in-python-2-6 # https://stackoverflow.com/questions/20303291/issue-with-redirecting-stdout-to-tkinter-text-widget-with-threads - -def make_clear(widgetlist): - for widget in widgetlist: - widget.configure(state = 'normal') - widget.delete('1.0', 'end') - widget.configure(state = 'disabled') class TextRedirect(object): class StdoutRedirect(object): tag_num = 0 - listwhere = [] - arrows, clt_msg_nonewline = pick_MsgMap([MsgMap[1], MsgMap[7], MsgMap[12], MsgMap[20]]) - srv_msg_nonewline, _ = pick_MsgMap([MsgMap[2], MsgMap[5], MsgMap[13], MsgMap[18]]) + + grpmsg = unformat_message([MsgMap[1], MsgMap[7], MsgMap[12], MsgMap[20]]) + arrows = [ item[0] for item in grpmsg ] + clt_msg_nonewline = [ item[1] for item in grpmsg ] arrows = list(set(arrows)) lenarrow = len(arrows[0]) - unMsgMap = unshell_MsgMap(arrows) - - def __init__(self, srv_text_space, clt_text_space, customcolors, runclt, str_to_print): + srv_msg_nonewline = [ item[0] for item in unformat_message([MsgMap[2], MsgMap[5], MsgMap[13], MsgMap[18]]) ] + terminator = unformat_message([MsgMap[21]])[0][0] + msg_align = [ msg[0].replace('\t', '').replace('\n', '') for msg in unformat_message([MsgMap[-2], MsgMap[-4]])] + newlinecut = [-1, -2, -4, -5] + + def __init__(self, srv_text_space, clt_text_space, customcolors, str_to_print, where): self.srv_text_space = srv_text_space self.clt_text_space = clt_text_space self.customcolors = customcolors - self.runclt = runclt - self.runclt.configure(state = 'disabled') self.str_to_print = str_to_print + self.where = where self.textbox_do() - + def textbox_finish(self, message): - if all(x == "srv" for x in TextRedirect.StdoutRedirect.listwhere): - terminator = pick_MsgMap([MsgMap[19]])[0] - else: - terminator = pick_MsgMap([MsgMap[21]])[0] - - if message in terminator: - TextRedirect.StdoutRedirect.tag_num = 0 - self.runclt.configure(state = 'normal') - - def textbox_clear(self): - if TextRedirect.StdoutRedirect.tag_num == 0: - # Clear "srv" and "clt" textboxs. - make_clear([self.srv_text_space, self.clt_text_space]) + if message == self.terminator: + TextRedirect.StdoutRedirect.tag_num = 0 + TextRedirect.StdoutRedirect.newlinecut = [-1, -2, -4, -5] def textbox_write(self, tag, message, color, extras): widget = self.textbox_choose(message) - TextRedirect.StdoutRedirect.listwhere.append(self.where) - self.maxchar = widget['width'] - self.textbox_color(tag, widget, color, self.customcolors['black'], extras) + self.w_maxpix, self.h_maxpix = widget.winfo_width(), widget.winfo_height() + self.xfont = tkFont.Font(font = widget['font']) widget.configure(state = 'normal') widget.insert('end', self.textbox_format(message), tag) - widget.see('end') + self.textbox_color(tag, widget, color, self.customcolors['black'], extras) + widget.after(100, widget.see('end')) widget.configure(state = 'disabled') self.textbox_finish(message) def textbox_choose(self, message): - if message not in self.arrows: - self.remind = message - self.where = self.unMsgMap[message] - if self.where == "srv": - return self.srv_text_space - elif self.where == "clt": - return self.clt_text_space - else: - if self.remind in self.srv_msg_nonewline: - self.where = "srv" - return self.srv_text_space - else: - self.where = "clt" - return self.clt_text_space + if self.where == "srv": + self.srv_text_space.focus_set() + return self.srv_text_space + elif self.where == "clt": + self.clt_text_space.focus_set() + return self.clt_text_space def textbox_color(self, tag, widget, forecolor = 'white', backcolor = 'black', extras = []): - xfont = tkFont.Font(font = widget['font']) - for extra in extras: if extra == 'bold': - xfont.configure(weight = "bold") + self.xfont.configure(weight = "bold") elif extra == 'italic': - xfont.configure(slant = "italic") + self.xfont.configure(slant = "italic") elif extra == 'underlined': - xfont.text_font.configure(underline = True) + self.xfont.text_font.configure(underline = True) elif extra == 'strike': - xfont.configure(overstrike = True) - - widget.tag_configure(tag, foreground = forecolor, background = backcolor, font = xfont) + self.xfont.configure(overstrike = True) + elif extra == 'reverse': + forecolor, backcolor = backcolor, forecolor + + widget.tag_configure(tag, foreground = forecolor, background = backcolor, font = self.xfont) + widget.tag_add(tag, "insert linestart", "insert lineend") + + def textbox_newline(self, message): + if not message.endswith('\n'): + return message + '\n' + else: + return message def textbox_format(self, message): - lenfixed = self.maxchar - len(message.replace('\t', '')) - - if self.where == "srv": - if message in self.srv_msg_nonewline: - lung = lenfixed - self.lenarrow + 4 - else: - lung = lenfixed + self.lenarrow + 10 - if not message.endswith('\n'): - message += '\n' - elif self.where == "clt": + # vertical align. + self.w_maxpix = self.w_maxpix - 5 # pixel reduction for distance from border. + w_fontpix, h_fontpix = (self.xfont.measure('0'), self.xfont.metrics('linespace')) + msg_unformat = message.replace('\t', '').replace('\n', '') + lenfixed_chars = int((self.w_maxpix / w_fontpix) - len(msg_unformat)) + + if message in self.srv_msg_nonewline + self.clt_msg_nonewline: + lung = lenfixed_chars - self.lenarrow if message in self.clt_msg_nonewline: - lung = lenfixed - self.lenarrow - if not message.endswith('\n'): - message += '\n' - else: - lung = lenfixed + 10 - if not message.endswith('\n') and message not in self.arrows: - message += '\n' + message = self.textbox_newline(message) + else: + lung = lenfixed_chars + if (self.where == "srv") or (self.where == "clt" and message not in self.arrows): + message = self.textbox_newline(message) + # horizontal align. + if msg_unformat in self.msg_align: + msg_strip = message.lstrip('\n') + message = '\n' * (len(message) - len(msg_strip) + TextRedirect.StdoutRedirect.newlinecut[0]) + msg_strip + TextRedirect.StdoutRedirect.newlinecut.pop(0) count = Counter(message) countab = (count['\t'] if count['\t'] != 0 else 1) @@ -231,7 +218,6 @@ class TextRedirect(object): return message def textbox_do(self): - self.textbox_clear() msgs, TextRedirect.StdoutRedirect.tag_num = unshell_message(self.str_to_print, TextRedirect.StdoutRedirect.tag_num) for tag in msgs: self.textbox_write(tag, msgs[tag]['text'], self.customcolors[msgs[tag]['color']], msgs[tag]['extra']) @@ -242,9 +228,9 @@ class TextRedirect(object): self.clt_text_space = clt_text_space self.customcolors = customcolors self.tag_err = 'STDERR' + self.xfont = tkFont.Font(font = self.srv_text_space['font']) def write(self, string): - self.textbox_clear() self.textbox_color(self.tag_err, self.srv_text_space, self.customcolors['red'], self.customcolors['black']) self.srv_text_space.configure(state = 'normal') self.srv_text_space.insert('end', string, self.tag_err) @@ -327,7 +313,7 @@ def custom_background(window): widget.configure(background = window.customcolors['lavender']) # Hide client. - window.clt_showhide(force = True) + window.clt_on_show(force = True) # Show Gui. window.deiconify() diff --git a/py-kms/pykms_Misc.py b/py-kms/pykms_Misc.py index cc7ad91..3db56a5 100644 --- a/py-kms/pykms_Misc.py +++ b/py-kms/pykms_Misc.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 +from __future__ import print_function import sys import logging import os +import argparse from logging.handlers import RotatingFileHandler -from pykms_Format import ColorExtraMap, ShellMessage +from pykms_Format import ColorExtraMap, pretty_printer #----------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -132,20 +134,25 @@ def logger_create(log_obj, config, mode = 'a'): #---------------------------------------------------------------------------------------------------------------------------------------------------------- -def check_logfile(optionlog, defaultlog, logger): +def check_logfile(optionlog, defaultlog, where): if not isinstance(optionlog, list): optionlog = [optionlog] lenopt = len(optionlog) - msg_long = "argument logfile: too much arguments" + msg_dir = "{reverse}{red}{bold}argument logfile: invalid directory: '%s'. Exiting...{end}" + msg_long = "{reverse}{red}{bold}argument logfile: too much arguments. Exiting...{end}" + msg_log = "{reverse}{red}{bold}argument logfile: not a log file, invalid extension: '%s'. Exiting...{end}" def checkdir(path): - msg_path = "argument logfile: No such file or directory: %s" %path - if not os.path.isdir(os.path.dirname(path)): - pretty_errors(46, logger, get_text = False, put_text = msg_path, log_text = False) + filename = os.path.basename(path) + pathname = os.path.dirname(path) + if not os.path.isdir(pathname): + pretty_printer(put_text = msg_dir %pathname, where = where, to_exit = True) + elif not filename.lower().endswith('.log'): + pretty_printer(put_text = msg_log %filename, where = where, to_exit = True) if lenopt > 2: - pretty_errors(46, logger, get_text = False, put_text = msg_long, log_text = False) + pretty_printer(put_text = msg_long, where = where, to_exit = True) if 'FILESTDOUT' in optionlog: if lenopt == 1: @@ -156,36 +163,12 @@ def check_logfile(optionlog, defaultlog, logger): checkdir(optionlog[1]) else: if lenopt == 2: - pretty_errors(46, logger, get_text = False, put_text = msg_long, log_text = False) + pretty_printer(put_text = msg_long, where = where, to_exit = True) elif lenopt == 1 and 'STDOUT' not in optionlog: # check directory path. checkdir(optionlog[0]) return optionlog - -def pretty_errors(error_num, logger, **kwargs): - """ error_num --> an int or list of int. - kwargs: - get_text --> True (default) / False. - put_text --> string / list of strings/ None. (applied to each "error_num") - log_text --> True (default) / False. - to_exit --> True (default) / False. - """ - # Set defaults for not defined options. - options = {'get_text' : True, - 'put_text' : None, - 'log_text' : True, - 'to_exit' : True, - } - options.update(kwargs) - # Process errors. - error_msgs = ShellMessage.Process(error_num, get_text = options['get_text'], put_text = options['put_text']).run() - if options['log_text']: - for err in error_msgs: - logger.error(err) - if options['to_exit']: - sys.exit(1) - #---------------------------------------------------------------------------------------------------------------------------------------------------------- # Valid language identifiers to be used in the EPID (see "kms.c" in vlmcsd) @@ -210,7 +193,7 @@ ValidLcid = [1025, 1026, 1027, 1028, 1029, 13313, 13321, 13322, 14337, 14346, 15361, 15370, 16385, 16394, 17418, 18442, 19466, 20490] # http://stackoverflow.com/questions/3425294/how-to-detect-the-os-default-language-in-python -def check_lcid(lcid, logger): +def check_lcid(lcid, log_obj): if not lcid or (lcid not in ValidLcid): if hasattr(sys, 'implementation') and sys.implementation.name == 'cpython': fixlcid = 1033 @@ -225,12 +208,55 @@ def check_lcid(lcid, logger): fixlcid = next(k for k, v in locale.windows_locale.items() if v == locale.getdefaultlocale()[0]) except StopIteration: fixlcid = 1033 - logger.warning("lcid %s auto-fixed with lcid %s" %(lcid, fixlcid)) + pretty_printer(log_obj = log_obj, + put_text = "{reverse}{yellow}{bold}LCID %s auto-fixed with LCID %s{end}" %(lcid, fixlcid)) return fixlcid return lcid #---------------------------------------------------------------------------------------------------------------------------------------------------------- +class KmsException(Exception): + pass + +class KmsParser(argparse.ArgumentParser): + def error(self, message): + raise KmsException(message) + +class KmsHelper(object): + def replace(self, parser, replace_epilog_with): + text = parser.format_help().splitlines() + help_list = [] + for line in text: + if line == parser.description: + continue + if line == parser.epilog: + line = replace_epilog_with + help_list.append(line) + return help_list + + def printer(self, parsers): + if len(parsers) == 3: + parser_base, parser_adj, parser_sub = parsers + replace_epilog_with = 80 * '*' + '\n' + elif len(parsers) == 1: + parser_base = parsers[0] + replace_epilog_with = '' + print('\n' + parser_base.description) + print(len(parser_base.description) * '-' + '\n') + for line in self.replace(parser_base, replace_epilog_with): + print(line) + try: + print(parser_adj.description + '\n') + for line in self.replace(parser_sub, replace_epilog_with): + print(line) + except: + pass + 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) ErrorCodes = { 'SL_E_SRV_INVALID_PUBLISH_LICENSE' : (0xC004B001, 'The activation server determined that the license is invalid.'), diff --git a/py-kms/pykms_RequestV4.py b/py-kms/pykms_RequestV4.py index ad3f3c5..c85fc30 100644 --- a/py-kms/pykms_RequestV4.py +++ b/py-kms/pykms_RequestV4.py @@ -7,7 +7,7 @@ import logging from pykms_Base import kmsBase from pykms_Structure import Structure from pykms_Aes import AES -from pykms_Format import justify, byterize, enco, deco, ShellMessage +from pykms_Format import justify, byterize, enco, deco, pretty_printer #--------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -105,7 +105,7 @@ class kmsRequestV4(kmsBase): response['padding'] = bytes(bytearray(self.getPadding(bodyLength))) ## Debug stuff. - ShellMessage.Process(16).run() + pretty_printer(num_text = 16, where = "srv") response = byterize(response) loggersrv.debug("KMS V4 Response: \n%s\n" % justify(response.dump(print_to_stdout = False))) loggersrv.debug("KMS V4 Response Bytes: \n%s\n" % justify(deco(binascii.b2a_hex(enco(str(response), 'latin-1')), 'utf-8'))) @@ -124,7 +124,7 @@ class kmsRequestV4(kmsBase): request['padding'] = bytes(bytearray(self.getPadding(bodyLength))) ## Debug stuff. - ShellMessage.Process(10).run() + pretty_printer(num_text = 10, where = "clt") request = byterize(request) loggersrv.debug("Request V4 Data: \n%s\n" % justify(request.dump(print_to_stdout = False))) loggersrv.debug("Request V4: \n%s\n" % justify(deco(binascii.b2a_hex(enco(str(request), 'latin-1')), 'utf-8'))) diff --git a/py-kms/pykms_RequestV5.py b/py-kms/pykms_RequestV5.py index 4fe1fd6..0d022b0 100644 --- a/py-kms/pykms_RequestV5.py +++ b/py-kms/pykms_RequestV5.py @@ -8,7 +8,7 @@ import random import pykms_Aes as aes from pykms_Base import kmsBase from pykms_Structure import Structure -from pykms_Format import justify, byterize, enco, deco, ShellMessage +from pykms_Format import justify, byterize, enco, deco, pretty_printer #-------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -140,7 +140,7 @@ class kmsRequestV5(kmsBase): response['encrypted'] = bytes(bytearray(encryptedResponse)) response['padding'] = bytes(bytearray(self.getPadding(bodyLength))) - ShellMessage.Process(16).run() + pretty_printer(num_text = 16, where = "srv") response = byterize(response) loggersrv.info("KMS V%d Response: \n%s\n" % (self.ver, justify(response.dump(print_to_stdout = False)))) loggersrv.info("KMS V%d Structure Bytes: \n%s\n" % (self.ver, justify(deco(binascii.b2a_hex(enco(str(response), 'latin-1')), 'utf-8')))) @@ -172,7 +172,7 @@ class kmsRequestV5(kmsBase): request['versionMajor'] = requestBase['versionMajor'] request['message'] = message - ShellMessage.Process(10).run() + pretty_printer(num_text = 10, where = "clt") request = byterize(request) loggersrv.info("Request V%d Data: \n%s\n" % (self.ver, justify(request.dump(print_to_stdout = False)))) loggersrv.info("Request V%d: \n%s\n" % (self.ver, justify(deco(binascii.b2a_hex(enco(str(request), 'latin-1')), 'utf-8')))) diff --git a/py-kms/pykms_RpcBind.py b/py-kms/pykms_RpcBind.py index 143d87e..ddb8555 100644 --- a/py-kms/pykms_RpcBind.py +++ b/py-kms/pykms_RpcBind.py @@ -7,7 +7,7 @@ import uuid import pykms_RpcBase from pykms_Dcerpc import MSRPCHeader, MSRPCBindAck from pykms_Structure import Structure -from pykms_Format import justify, byterize, enco, deco, ShellMessage +from pykms_Format import justify, byterize, enco, deco, pretty_printer #-------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -77,7 +77,7 @@ class MSRPCBind(Structure): class handler(pykms_RpcBase.rpcBase): def parseRequest(self): request = MSRPCHeader(self.data) - ShellMessage.Process(3).run() + pretty_printer(num_text = 3, where = "srv") request = byterize(request) loggersrv.debug("RPC Bind Request Bytes: \n%s\n" % justify(deco(binascii.b2a_hex(self.data), 'utf-8'))) loggersrv.debug("RPC Bind Request: \n%s\n%s\n" % (justify(request.dump(print_to_stdout = False)), @@ -121,7 +121,7 @@ class handler(pykms_RpcBase.rpcBase): resp = preparedResponses[ts_uuid] response['ctx_items'] += str(resp) - ShellMessage.Process(4).run() + pretty_printer(num_text = 4, where = "srv") response = byterize(response) loggersrv.debug("RPC Bind Response: \n%s\n" % justify(response.dump(print_to_stdout = False))) loggersrv.debug("RPC Bind Response Bytes: \n%s\n" % justify(deco(binascii.b2a_hex(enco(str(response), 'latin-1')), 'utf-8'))) @@ -161,8 +161,8 @@ class handler(pykms_RpcBase.rpcBase): request['flags'] = self.packetFlags['firstFrag'] | self.packetFlags['lastFrag'] | self.packetFlags['multiplex'] request['call_id'] = self.srv_config['call_id'] request['pduData'] = str(bind) - - ShellMessage.Process(0).run() + + pretty_printer(num_text = 0, where = "clt") bind = byterize(bind) request = byterize(request) loggersrv.debug("RPC Bind Request: \n%s\n%s\n" % (justify(request.dump(print_to_stdout = False)), diff --git a/py-kms/pykms_RpcRequest.py b/py-kms/pykms_RpcRequest.py index 1d947b6..10d8e94 100644 --- a/py-kms/pykms_RpcRequest.py +++ b/py-kms/pykms_RpcRequest.py @@ -6,7 +6,7 @@ import logging import pykms_Base import pykms_RpcBase from pykms_Dcerpc import MSRPCRequestHeader, MSRPCRespHeader -from pykms_Format import justify, byterize, enco, deco, ShellMessage +from pykms_Format import justify, byterize, enco, deco, pretty_printer #---------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -15,7 +15,7 @@ loggersrv = logging.getLogger('logsrv') class handler(pykms_RpcBase.rpcBase): def parseRequest(self): request = MSRPCRequestHeader(self.data) - ShellMessage.Process(14).run() + pretty_printer(num_text = 14, where = "srv") request = byterize(request) loggersrv.debug("RPC Message Request Bytes: \n%s\n" % justify(binascii.b2a_hex(self.data).decode('utf-8'))) loggersrv.debug("RPC Message Request: \n%s\n" % justify(request.dump(print_to_stdout = False))) @@ -40,7 +40,7 @@ class handler(pykms_RpcBase.rpcBase): response['pduData'] = responseData - ShellMessage.Process(17).run() + pretty_printer(num_text = 17, where = "srv") response = byterize(response) loggersrv.debug("RPC Message Response: \n%s\n" % justify(response.dump(print_to_stdout = False))) loggersrv.debug("RPC Message Response Bytes: \n%s\n" % justify(deco(binascii.b2a_hex(enco(str(response), 'latin-1')), 'utf-8'))) @@ -58,8 +58,8 @@ class handler(pykms_RpcBase.rpcBase): request['call_id'] = self.srv_config['call_id'] request['alloc_hint'] = len(self.data) request['pduData'] = str(self.data) - - ShellMessage.Process(11).run() + + pretty_printer(num_text = 11, where = "clt") request = byterize(request) loggersrv.debug("RPC Message Request: \n%s\n" % justify(request.dump(print_to_stdout = False))) loggersrv.debug("RPC Message Request Bytes: \n%s\n" % justify(deco(binascii.b2a_hex(enco(str(request), 'latin-1')), 'utf-8'))) diff --git a/py-kms/pykms_Selectors.py b/py-kms/pykms_Selectors.py new file mode 100644 index 0000000..ec3e0fe --- /dev/null +++ b/py-kms/pykms_Selectors.py @@ -0,0 +1,588 @@ +#!/usr/bin/env python3 + +""" + SPDX-License-Identifier: MIT + Backport of selectors.py from Python 3.5+ to support Python < 3.4 + Also has the behavior specified in PEP 475 which is to retry syscalls + in the case of an EINTR error. This module is required because selectors34 + does not follow this behavior and instead returns that no dile descriptor + events have occurred rather than retry the syscall. The decision to drop + support for select.devpoll is made to maintain 100% test coverage. + + link: https://github.com/netdata/netdata/blob/master/collectors/python.d.plugin/python_modules/urllib3/util/selectors.py +""" + +import errno +import math +import select +import socket +import sys +import time +from collections import namedtuple, Mapping + +try: + monotonic = time.monotonic +except (AttributeError, ImportError): # Python 3.3< + from pykms_Time import monotonic + +EVENT_READ = (1 << 0) +EVENT_WRITE = (1 << 1) + +HAS_SELECT = True # Variable that shows whether the platform has a selector. +_SYSCALL_SENTINEL = object() # Sentinel in case a system call returns None. +_DEFAULT_SELECTOR = None + + +class SelectorError(Exception): + def __init__(self, errcode): + super(SelectorError, self).__init__() + self.errno = errcode + + def __repr__(self): + return "".format(self.errno) + + def __str__(self): + return self.__repr__() + + +def _fileobj_to_fd(fileobj): + """ Return a file descriptor from a file object. If + given an integer will simply return that integer back. """ + if isinstance(fileobj, int): + fd = fileobj + else: + try: + fd = int(fileobj.fileno()) + except (AttributeError, TypeError, ValueError): + raise ValueError("Invalid file object: {0!r}".format(fileobj)) + if fd < 0: + raise ValueError("Invalid file descriptor: {0}".format(fd)) + return fd + + +# Determine which function to use to wrap system calls because Python 3.5+ +# already handles the case when system calls are interrupted. +if sys.version_info >= (3, 5): + def _syscall_wrapper(func, _, *args, **kwargs): + """ This is the short-circuit version of the below logic + because in Python 3.5+ all system calls automatically restart + and recalculate their timeouts. """ + try: + return func(*args, **kwargs) + except (OSError, IOError, select.error) as e: + errcode = None + if hasattr(e, "errno"): + errcode = e.errno + raise SelectorError(errcode) +else: + def _syscall_wrapper(func, recalc_timeout, *args, **kwargs): + """ Wrapper function for syscalls that could fail due to EINTR. + All functions should be retried if there is time left in the timeout + in accordance with PEP 475. """ + timeout = kwargs.get("timeout", None) + if timeout is None: + expires = None + recalc_timeout = False + else: + timeout = float(timeout) + if timeout < 0.0: # Timeout less than 0 treated as no timeout. + expires = None + else: + expires = monotonic() + timeout + + args = list(args) + if recalc_timeout and "timeout" not in kwargs: + raise ValueError( + "Timeout must be in args or kwargs to be recalculated") + + result = _SYSCALL_SENTINEL + while result is _SYSCALL_SENTINEL: + try: + result = func(*args, **kwargs) + # OSError is thrown by select.select + # IOError is thrown by select.epoll.poll + # select.error is thrown by select.poll.poll + # Aren't we thankful for Python 3.x rework for exceptions? + except (OSError, IOError, select.error) as e: + # select.error wasn't a subclass of OSError in the past. + errcode = None + if hasattr(e, "errno"): + errcode = e.errno + elif hasattr(e, "args"): + errcode = e.args[0] + + # Also test for the Windows equivalent of EINTR. + is_interrupt = (errcode == errno.EINTR or (hasattr(errno, "WSAEINTR") and + errcode == errno.WSAEINTR)) + + if is_interrupt: + if expires is not None: + current_time = monotonic() + if current_time > expires: + raise OSError(errno=errno.ETIMEDOUT) + if recalc_timeout: + if "timeout" in kwargs: + kwargs["timeout"] = expires - current_time + continue + if errcode: + raise SelectorError(errcode) + else: + raise + return result + + +SelectorKey = namedtuple('SelectorKey', ['fileobj', 'fd', 'events', 'data']) + + +class _SelectorMapping(Mapping): + """ Mapping of file objects to selector keys """ + + def __init__(self, selector): + self._selector = selector + + def __len__(self): + return len(self._selector._fd_to_key) + + def __getitem__(self, fileobj): + try: + fd = self._selector._fileobj_lookup(fileobj) + return self._selector._fd_to_key[fd] + except KeyError: + raise KeyError("{0!r} is not registered.".format(fileobj)) + + def __iter__(self): + return iter(self._selector._fd_to_key) + + +class BaseSelector(object): + """ Abstract Selector class + + A selector supports registering file objects to be monitored + for specific I/O events. + + A file object is a file descriptor or any object with a + `fileno()` method. An arbitrary object can be attached to the + file object which can be used for example to store context info, + a callback, etc. + + A selector can use various implementations (select(), poll(), epoll(), + and kqueue()) depending on the platform. The 'DefaultSelector' class uses + the most efficient implementation for the current platform. + """ + def __init__(self): + # Maps file descriptors to keys. + self._fd_to_key = {} + + # Read-only mapping returned by get_map() + self._map = _SelectorMapping(self) + + def _fileobj_lookup(self, fileobj): + """ Return a file descriptor from a file object. + This wraps _fileobj_to_fd() to do an exhaustive + search in case the object is invalid but we still + have it in our map. Used by unregister() so we can + unregister an object that was previously registered + even if it is closed. It is also used by _SelectorMapping + """ + try: + return _fileobj_to_fd(fileobj) + except ValueError: + + # Search through all our mapped keys. + for key in self._fd_to_key.values(): + if key.fileobj is fileobj: + return key.fd + + # Raise ValueError after all. + raise + + def register(self, fileobj, events, data=None): + """ Register a file object for a set of events to monitor. """ + if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)): + raise ValueError("Invalid events: {0!r}".format(events)) + + key = SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data) + + if key.fd in self._fd_to_key: + raise KeyError("{0!r} (FD {1}) is already registered" + .format(fileobj, key.fd)) + + self._fd_to_key[key.fd] = key + return key + + def unregister(self, fileobj): + """ Unregister a file object from being monitored. """ + try: + key = self._fd_to_key.pop(self._fileobj_lookup(fileobj)) + except KeyError: + raise KeyError("{0!r} is not registered".format(fileobj)) + + # Getting the fileno of a closed socket on Windows errors with EBADF. + except socket.error as e: # Platform-specific: Windows. + if e.errno != errno.EBADF: + raise + else: + for key in self._fd_to_key.values(): + if key.fileobj is fileobj: + self._fd_to_key.pop(key.fd) + break + else: + raise KeyError("{0!r} is not registered".format(fileobj)) + return key + + def modify(self, fileobj, events, data=None): + """ Change a registered file object monitored events and data. """ + # NOTE: Some subclasses optimize this operation even further. + try: + key = self._fd_to_key[self._fileobj_lookup(fileobj)] + except KeyError: + raise KeyError("{0!r} is not registered".format(fileobj)) + + if events != key.events: + self.unregister(fileobj) + key = self.register(fileobj, events, data) + + elif data != key.data: + # Use a shortcut to update the data. + key = key._replace(data=data) + self._fd_to_key[key.fd] = key + + return key + + def select(self, timeout=None): + """ Perform the actual selection until some monitored file objects + are ready or the timeout expires. """ + raise NotImplementedError() + + def close(self): + """ Close the selector. This must be called to ensure that all + underlying resources are freed. """ + self._fd_to_key.clear() + self._map = None + + def get_key(self, fileobj): + """ Return the key associated with a registered file object. """ + mapping = self.get_map() + if mapping is None: + raise RuntimeError("Selector is closed") + try: + return mapping[fileobj] + except KeyError: + raise KeyError("{0!r} is not registered".format(fileobj)) + + def get_map(self): + """ Return a mapping of file objects to selector keys """ + return self._map + + def _key_from_fd(self, fd): + """ Return the key associated to a given file descriptor + Return None if it is not found. """ + try: + return self._fd_to_key[fd] + except KeyError: + return None + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +# Almost all platforms have select.select() +if hasattr(select, "select"): + class SelectSelector(BaseSelector): + """ Select-based selector. """ + def __init__(self): + super(SelectSelector, self).__init__() + self._readers = set() + self._writers = set() + + def register(self, fileobj, events, data=None): + key = super(SelectSelector, self).register(fileobj, events, data) + if events & EVENT_READ: + self._readers.add(key.fd) + if events & EVENT_WRITE: + self._writers.add(key.fd) + return key + + def unregister(self, fileobj): + key = super(SelectSelector, self).unregister(fileobj) + self._readers.discard(key.fd) + self._writers.discard(key.fd) + return key + + def _select(self, r, w, timeout=None): + """ Wrapper for select.select because timeout is a positional arg """ + return select.select(r, w, [], timeout) + + def select(self, timeout=None): + # Selecting on empty lists on Windows errors out. + if not len(self._readers) and not len(self._writers): + return [] + + timeout = None if timeout is None else max(timeout, 0.0) + ready = [] + r, w, _ = _syscall_wrapper(self._select, True, self._readers, + self._writers, timeout) + r = set(r) + w = set(w) + for fd in r | w: + events = 0 + if fd in r: + events |= EVENT_READ + if fd in w: + events |= EVENT_WRITE + + key = self._key_from_fd(fd) + if key: + ready.append((key, events & key.events)) + return ready + + +if hasattr(select, "poll"): + class PollSelector(BaseSelector): + """ Poll-based selector """ + def __init__(self): + super(PollSelector, self).__init__() + self._poll = select.poll() + + def register(self, fileobj, events, data=None): + key = super(PollSelector, self).register(fileobj, events, data) + event_mask = 0 + if events & EVENT_READ: + event_mask |= select.POLLIN + if events & EVENT_WRITE: + event_mask |= select.POLLOUT + self._poll.register(key.fd, event_mask) + return key + + def unregister(self, fileobj): + key = super(PollSelector, self).unregister(fileobj) + self._poll.unregister(key.fd) + return key + + def _wrap_poll(self, timeout=None): + """ Wrapper function for select.poll.poll() so that + _syscall_wrapper can work with only seconds. """ + if timeout is not None: + if timeout <= 0: + timeout = 0 + else: + # select.poll.poll() has a resolution of 1 millisecond, + # round away from zero to wait *at least* timeout seconds. + timeout = math.ceil(timeout * 1e3) + + result = self._poll.poll(timeout) + return result + + def select(self, timeout=None): + ready = [] + fd_events = _syscall_wrapper(self._wrap_poll, True, timeout=timeout) + for fd, event_mask in fd_events: + events = 0 + if event_mask & ~select.POLLIN: + events |= EVENT_WRITE + if event_mask & ~select.POLLOUT: + events |= EVENT_READ + + key = self._key_from_fd(fd) + if key: + ready.append((key, events & key.events)) + + return ready + + +if hasattr(select, "epoll"): + class EpollSelector(BaseSelector): + """ Epoll-based selector """ + def __init__(self): + super(EpollSelector, self).__init__() + self._epoll = select.epoll() + + def fileno(self): + return self._epoll.fileno() + + def register(self, fileobj, events, data=None): + key = super(EpollSelector, self).register(fileobj, events, data) + events_mask = 0 + if events & EVENT_READ: + events_mask |= select.EPOLLIN + if events & EVENT_WRITE: + events_mask |= select.EPOLLOUT + _syscall_wrapper(self._epoll.register, False, key.fd, events_mask) + return key + + def unregister(self, fileobj): + key = super(EpollSelector, self).unregister(fileobj) + try: + _syscall_wrapper(self._epoll.unregister, False, key.fd) + except SelectorError: + # This can occur when the fd was closed since registry. + pass + return key + + def select(self, timeout=None): + if timeout is not None: + if timeout <= 0: + timeout = 0.0 + else: + # select.epoll.poll() has a resolution of 1 millisecond + # but luckily takes seconds so we don't need a wrapper + # like PollSelector. Just for better rounding. + timeout = math.ceil(timeout * 1e3) * 1e-3 + timeout = float(timeout) + else: + timeout = -1.0 # epoll.poll() must have a float. + + # We always want at least 1 to ensure that select can be called + # with no file descriptors registered. Otherwise will fail. + max_events = max(len(self._fd_to_key), 1) + + ready = [] + fd_events = _syscall_wrapper(self._epoll.poll, True, + timeout=timeout, + maxevents=max_events) + for fd, event_mask in fd_events: + events = 0 + if event_mask & ~select.EPOLLIN: + events |= EVENT_WRITE + if event_mask & ~select.EPOLLOUT: + events |= EVENT_READ + + key = self._key_from_fd(fd) + if key: + ready.append((key, events & key.events)) + return ready + + def close(self): + self._epoll.close() + super(EpollSelector, self).close() + + +if hasattr(select, "kqueue"): + class KqueueSelector(BaseSelector): + """ Kqueue / Kevent-based selector """ + def __init__(self): + super(KqueueSelector, self).__init__() + self._kqueue = select.kqueue() + + def fileno(self): + return self._kqueue.fileno() + + def register(self, fileobj, events, data=None): + key = super(KqueueSelector, self).register(fileobj, events, data) + if events & EVENT_READ: + kevent = select.kevent(key.fd, + select.KQ_FILTER_READ, + select.KQ_EV_ADD) + + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + + if events & EVENT_WRITE: + kevent = select.kevent(key.fd, + select.KQ_FILTER_WRITE, + select.KQ_EV_ADD) + + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + + return key + + def unregister(self, fileobj): + key = super(KqueueSelector, self).unregister(fileobj) + if key.events & EVENT_READ: + kevent = select.kevent(key.fd, + select.KQ_FILTER_READ, + select.KQ_EV_DELETE) + try: + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + except SelectorError: + pass + if key.events & EVENT_WRITE: + kevent = select.kevent(key.fd, + select.KQ_FILTER_WRITE, + select.KQ_EV_DELETE) + try: + _syscall_wrapper(self._kqueue.control, False, [kevent], 0, 0) + except SelectorError: + pass + + return key + + def select(self, timeout=None): + if timeout is not None: + timeout = max(timeout, 0) + + max_events = len(self._fd_to_key) * 2 + ready_fds = {} + + kevent_list = _syscall_wrapper(self._kqueue.control, True, + None, max_events, timeout) + + for kevent in kevent_list: + fd = kevent.ident + event_mask = kevent.filter + events = 0 + if event_mask == select.KQ_FILTER_READ: + events |= EVENT_READ + if event_mask == select.KQ_FILTER_WRITE: + events |= EVENT_WRITE + + key = self._key_from_fd(fd) + if key: + if key.fd not in ready_fds: + ready_fds[key.fd] = (key, events & key.events) + else: + old_events = ready_fds[key.fd][1] + ready_fds[key.fd] = (key, (events | old_events) & key.events) + + return list(ready_fds.values()) + + def close(self): + self._kqueue.close() + super(KqueueSelector, self).close() + + +if not hasattr(select, 'select'): # Platform-specific: AppEngine + HAS_SELECT = False + + +def _can_allocate(struct): + """ Checks that select structs can be allocated by the underlying + operating system, not just advertised by the select module. We don't + check select() because we'll be hopeful that most platforms that + don't have it available will not advertise it. (ie: GAE) """ + try: + # select.poll() objects won't fail until used. + if struct == 'poll': + p = select.poll() + p.poll(0) + + # All others will fail on allocation. + else: + getattr(select, struct)().close() + return True + except (OSError, AttributeError) as e: + return False + + +# Choose the best implementation, roughly: +# kqueue == epoll > poll > select. Devpoll not supported. (See above) +# select() also can't accept a FD > FD_SETSIZE (usually around 1024) +def DefaultSelector(): + """ This function serves as a first call for DefaultSelector to + detect if the select module is being monkey-patched incorrectly + by eventlet, greenlet, and preserve proper behavior. """ + global _DEFAULT_SELECTOR + if _DEFAULT_SELECTOR is None: + if _can_allocate('kqueue'): + _DEFAULT_SELECTOR = KqueueSelector + elif _can_allocate('epoll'): + _DEFAULT_SELECTOR = EpollSelector + elif _can_allocate('poll'): + _DEFAULT_SELECTOR = PollSelector + elif hasattr(select, 'select'): + _DEFAULT_SELECTOR = SelectSelector + else: # Platform-specific: AppEngine + raise ValueError('Platform does not have a selector') + return _DEFAULT_SELECTOR() diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py old mode 100644 new mode 100755 index 30b83ab..883d0de --- a/py-kms/pykms_Server.py +++ b/py-kms/pykms_Server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- -import argparse import binascii import re import sys @@ -8,66 +8,154 @@ import socket import uuid import logging import os -import errno import threading +import pickle try: # Python 2 import. import SocketServer as socketserver import Queue as Queue + import pykms_Selectors as selectors + from pykms_Time import monotonic as time except ImportError: # Python 3 import. import socketserver import queue as Queue + import selectors + from time import monotonic as time 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, pretty_errors -from pykms_Format import enco, deco, ShellMessage +from pykms_Misc import logger_create, check_logfile, check_lcid +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 " +__url__ = "https://github.com/SystemRage/py-kms" +srv_description = "py-kms: KMS Server Emulator written in Python" srv_config = {} ##--------------------------------------------------------------------------------------------------------------------------------------------------------- class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): daemon_threads = True allow_reuse_address = True - - def handle_timeout(self): - pretty_errors(40, loggersrv) - -class server_thread(threading.Thread): - def __init__(self): - threading.Thread.__init__(self) - self.queue = serverqueue - self.is_running = False - self.daemon = True - - def run(self): - while True: - if not self.queue.empty(): - item = self.queue.get() - if item == 'start': - self.is_running = True - # Check options. - server_check() - # Create and run threaded server. - self.server = server_create() - try: - while True: - self.server.handle_request() - except KeyboardInterrupt: - pass - finally: - self.server.server_close() - elif item == 'stop': - self.is_running = False - self.server = None - self.queue.task_done() -##----------------------------------------------------------------------------------------------------------------------------------------------- + def __init__(self, server_address, RequestHandlerClass): + socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass) + self.__shutdown_request = False + self.r_service, self.w_service = os.pipe() + + if hasattr(selectors, 'PollSelector'): + self._ServerSelector = selectors.PollSelector + else: + self._ServerSelector = selectors.SelectSelector + + def pykms_serve(self): + """ Mixing of socketserver serve_forever() and handle_request() functions, + without elements blocking tkinter. + Handle one request at a time, possibly blocking. + Respects self.timeout. + """ + # Support people who used socket.settimeout() to escape + # pykms_serve() before self.timeout was available. + timeout = self.socket.gettimeout() + if timeout is None: + timeout = self.timeout + elif self.timeout is not None: + timeout = min(timeout, self.timeout) + if timeout is not None: + deadline = time() + timeout + + try: + # Wait until a request arrives or the timeout expires. + with self._ServerSelector() as selector: + selector.register(fileobj = self, events = selectors.EVENT_READ) + # self-pipe trick. + selector.register(fileobj = self.r_service, events = selectors.EVENT_READ) + + while not self.__shutdown_request: + ready = selector.select(timeout) + if self.__shutdown_request: + break + + if ready == []: + if timeout is not None: + timeout = deadline - time() + if timeout < 0: + return self.handle_timeout() + else: + for key, mask in ready: + if key.fileobj is self: + self._handle_request_noblock() + elif key.fileobj is self.r_service: + # only to clean buffer. + msgkill = os.read(self.r_service, 8).decode('utf-8') + sys.exit(0) + finally: + self.__shutdown_request = False + + def shutdown(self): + self.__shutdown_request = True + + def handle_timeout(self): + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}Server connection timed out. Exiting...{end}") + + def handle_error(self, request, client_address): + pass + + +class server_thread(threading.Thread): + def __init__(self, queue, name): + threading.Thread.__init__(self) + self.name = name + self.queue = queue + self.server = None + 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() + + def terminate_eject(self): + os.write(self.server.w_service, u'☠'.encode('utf-8')) + + def run(self): + while not self.is_running_thread.is_set(): + try: + item = self.queue.get(block = True, timeout = 0.1) + self.queue.task_done() + except Queue.Empty: + continue + else: + try: + if item == 'start': + self.eject = False + self.is_running_server = True + # Check options. + if not self.checked: + server_check() + # Create and run server. + self.server = server_create() + self.server.pykms_serve() + except SystemExit as e: + self.eject = True + if not self.with_gui: + raise + else: + continue + +##--------------------------------------------------------------------------------------------------------------------------------------------------------- loggersrv = logging.getLogger('logsrv') @@ -94,53 +182,141 @@ 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"}, } - -class KmsSrvException(Exception): - pass - -class KmsSrvParser(argparse.ArgumentParser): - def error(self, message): - raise KmsSrvException(message) - def server_options(): - parser = KmsSrvParser(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 = "+", 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) + try: + server_parser = KmsParser(description = srv_description, epilog = 'version: ' + srv_version, add_help = False, allow_abbrev = False) + except TypeError: + server_parser = KmsParser(description = srv_description, epilog = 'version: ' + srv_version, add_help = False) + + server_parser.add_argument("ip", nargs = "?", action = "store", default = srv_options['ip']['def'], help = srv_options['ip']['help'], type = str) + server_parser.add_argument("port", nargs = "?", action = "store", default = srv_options['port']['def'], help = srv_options['port']['help'], type = int) + server_parser.add_argument("-e", "--epid", action = "store", dest = srv_options['epid']['des'], default = srv_options['epid']['def'], + help = srv_options['epid']['help'], type = str) + server_parser.add_argument("-l", "--lcid", action = "store", dest = srv_options['lcid']['des'], default = srv_options['lcid']['def'], + help = srv_options['lcid']['help'], type = int) + server_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) + server_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) + server_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) + server_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']) + server_parser.add_argument("-w", "--hwid", action = "store", dest = srv_options['hwid']['des'], default = srv_options['hwid']['def'], + help = srv_options['hwid']['help'], type = str) + server_parser.add_argument("-t", "--timeout", action = "store", dest = srv_options['time']['des'], default = srv_options['time']['def'], + help = srv_options['time']['help'], type = int) + server_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) + server_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) + server_parser.add_argument("-S", "--logsize", action = "store", dest = srv_options['lsize']['des'], default = srv_options['lsize']['def'], + help = srv_options['lsize']['help'], type = float) + server_parser.add_argument("-h", "--help", action = "help", help = "show this help message and exit") try: - srv_config.update(vars(parser.parse_args())) - # Check logfile. - srv_config['logfile'] = check_logfile(srv_config['logfile'], srv_options['lfile']['def'], loggersrv) - except KmsSrvException as e: - pretty_errors(46, loggersrv, get_text = False, put_text = str(e), log_text = False) + daemon_parser = KmsParser(description = "daemon options inherited from Etrigan", add_help = False, allow_abbrev = False) + except TypeError: + daemon_parser = KmsParser(description = "daemon options inherited from Etrigan", add_help = False) + + daemon_subparser = daemon_parser.add_subparsers(dest = "mode") + try: + etrigan_parser = daemon_subparser.add_parser("etrigan", add_help = False, allow_abbrev = False) + except TypeError: + etrigan_parser = daemon_subparser.add_parser("etrigan", add_help = 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: + if "-h" in sys.argv[1:]: + KmsHelper().printer(parsers = [server_parser, daemon_parser, etrigan_parser]) + + # Set defaults for config. + # case: python3 pykms_Server.py + srv_config.update(vars(server_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 = server_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 = server_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. + 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 ) + # Create log. logger_create(loggersrv, srv_config, mode = 'a') @@ -155,33 +331,40 @@ def server_check(): diff = set(hexstr).symmetric_difference(set(hexsub)) if len(diff) != 0: - pretty_errors(41, loggersrv, put_text = [hexstr.upper(), diff]) + diff = str(diff).replace('{', '').replace('}', '') + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}HWID '%s' is invalid. Digit %s non hexadecimal. Exiting...{end}" %(hexstr.upper(), diff)) else: lh = len(hexsub) if lh % 2 != 0: - pretty_errors(42, loggersrv, put_text = hexsub.upper()) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}HWID '%s' is invalid. Hex string is odd length. Exiting...{end}" %hexsub.upper()) elif lh < 16: - pretty_errors(43, loggersrv, put_text = hexsub.upper()) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}HWID '%s' is invalid. Hex string is too short. Exiting...{end}" %hexsub.upper()) elif lh > 16: - pretty_errors(44, loggersrv, put_text = hexsub.upper()) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}HWID '%s' is invalid. Hex string is too long. Exiting...{end}" %hexsub.upper()) else: srv_config['hwid'] = binascii.a2b_hex(hexsub) # Check LCID. - srv_config['lcid'] = check_lcid(srv_config['lcid'], loggersrv) + srv_config['lcid'] = check_lcid(srv_config['lcid'], loggersrv.warning) # Check sqlite. try: import sqlite3 except: - loggersrv.warning("Module \"sqlite3\" is not installed, database support disabled.") + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}Module 'sqlite3' is not installed, database support disabled.{end}") srv_config['dbSupport'] = False else: srv_config['dbSupport'] = True # Check port. if not 1 <= srv_config['port'] <= 65535: - pretty_errors(45, loggersrv, put_text = srv_config['port']) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{red}{bold}Port number '%s' is invalid. Enter between 1 - 65535. Exiting...{end}" %srv_config['port']) def server_create(): server = KeyServer((srv_config['ip'], srv_config['port']), kmsServerHandler) @@ -189,17 +372,60 @@ def server_create(): loggersrv.info("TCP server listening at %s on port %d." % (srv_config['ip'], srv_config['port'])) loggersrv.info("HWID: %s" % deco(binascii.b2a_hex(srv_config['hwid']), 'utf-8').upper()) return server - -def srv_main_without_gui(): + +def server_terminate(generic_srv, exit_server = False, exit_thread = False): + if exit_server: + generic_srv.terminate_serve() + if exit_thread: + generic_srv.terminate_thread() + +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') - serverthread.join() - -def srv_main_with_gui(width = 950, height = 660): + # Check options. + server_check() + serverthread.checked = True + + 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. @@ -209,9 +435,14 @@ def srv_main_with_gui(width = 950, height = 660): x = (ws / 2) - (width / 2) y = (hs / 2) - (height / 2) root.geometry('+%d+%d' %(x, y)) - + # disable maximize button. + 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): @@ -222,38 +453,45 @@ class kmsServerHandler(socketserver.BaseRequestHandler): # self.request is the TCP socket connected to the client try: self.data = self.request.recv(1024) - except socket.error as e: - if e.errno == errno.ECONNRESET: - loggersrv.error("Connection reset by peer.") + if self.data == '' or not self.data: + pretty_printer(log_obj = loggersrv.warning, + put_text = "{reverse}{yellow}{bold}No data received.{end}") break - else: - raise - if self.data == '' or not self.data: - loggersrv.warning("No data received !") + except socket.error as e: + pretty_printer(log_obj = loggersrv.error, + put_text = "{reverse}{red}{bold}While receiving: %s{end}" %str(e)) break packetType = MSRPCHeader(self.data)['type'] if packetType == rpcBase.packetType['bindReq']: loggersrv.info("RPC bind request received.") - ShellMessage.Process([-2, 2]).run() + pretty_printer(num_text = [-2, 2], where = "srv") handler = pykms_RpcBind.handler(self.data, srv_config) elif packetType == rpcBase.packetType['request']: loggersrv.info("Received activation request.") - ShellMessage.Process([-2, 13]).run() + pretty_printer(num_text = [-2, 13], where = "srv") handler = pykms_RpcRequest.handler(self.data, srv_config) else: - loggersrv.error("Invalid RPC request type ", packetType) - break - + pretty_printer(log_obj = loggersrv.error, + put_text = "{reverse}{red}{bold}Invalid RPC request type %s.{end}" %packetType) + break + res = enco(str(handler.populate()), 'latin-1') - self.request.send(res) if packetType == rpcBase.packetType['bindReq']: loggersrv.info("RPC bind acknowledged.") - ShellMessage.Process([-3, 5, 6]).run() + pretty_printer(num_text = [-3, 5, 6], where = "srv") elif packetType == rpcBase.packetType['request']: loggersrv.info("Responded to activation request.") - ShellMessage.Process([-3, 18, 19]).run() + pretty_printer(num_text = [-3, 18, 19], where = "srv") + + try: + self.request.send(res) + if packetType == rpcBase.packetType['request']: + break + except socket.error as e: + pretty_printer(log_obj = loggersrv.error, + put_text = "{reverse}{red}{bold}While sending: %s{end}" %str(e)) break def finish(self): @@ -262,14 +500,15 @@ class kmsServerHandler(socketserver.BaseRequestHandler): serverqueue = Queue.Queue(maxsize = 0) -serverthread = server_thread() +serverthread = server_thread(serverqueue, name = "Thread-Srv") +serverthread.setDaemon(True) 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() diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index f19c299..d5b7910 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -2,7 +2,6 @@ import os import logging -import sys # sqlite3 is optional. try: @@ -10,6 +9,8 @@ try: except ImportError: pass +from pykms_Format import pretty_printer + #-------------------------------------------------------------------------------------------------------------------------------------------------------- loggersrv = logging.getLogger('logsrv') @@ -26,8 +27,8 @@ def sql_initialize(): licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER)") except sqlite3.Error as e: - loggersrv.error("Error %s:" % e.args[0]) - sys.exit(1) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) finally: if con: con.commit() @@ -63,11 +64,11 @@ skuId, licenseStatus, lastRequestTime, requestCount) VALUES (:clientMachineId, : cur.execute("UPDATE clients SET requestCount=requestCount+1 WHERE clientMachineId=:clientMachineId;", infoDict) except sqlite3.Error as e: - loggersrv.error("Error %s:" % e.args[0]) - sys.exit(1) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) except sqlite3.Error as e: - loggersrv.error("Error %s:" % e.args[0]) - sys.exit(1) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) finally: if con: con.commit() @@ -88,11 +89,11 @@ def sql_update_epid(dbName, kmsRequest, response): cur.execute("UPDATE clients SET kmsEpid=? WHERE clientMachineId=?;", (str(response["kmsEpid"].decode('utf-16le')), cmid)) except sqlite3.Error as e: - loggersrv.error("Error %s:" % e.args[0]) - sys.exit(1) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) except sqlite3.Error as e: - loggersrv.error("Error %s:" % e.args[0]) - sys.exit(1) + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e)) finally: if con: con.commit() diff --git a/py-kms/pykms_Time.py b/py-kms/pykms_Time.py new file mode 100644 index 0000000..14fff3f --- /dev/null +++ b/py-kms/pykms_Time.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + monotonic + ~~~~~~~~~ + + This module provides a ``monotonic()`` function which returns the + value (in fractional seconds) of a clock which never goes backwards. + + On Python 3.3 or newer, ``monotonic`` will be an alias of + ``time.monotonic`` from the standard library. On older versions, + it will fall back to an equivalent implementation: + + +-------------+----------------------------------------+ + | Linux, BSD | ``clock_gettime(3)`` | + +-------------+----------------------------------------+ + | Windows | ``GetTickCount`` or ``GetTickCount64`` | + +-------------+----------------------------------------+ + | OS X | ``mach_absolute_time`` | + +-------------+----------------------------------------+ + + If no suitable implementation exists for the current platform, + attempting to import this module (or to import from it) will + cause a ``RuntimeError`` exception to be raised. + + + Copyright 2014, 2015, 2016 Ori Livneh + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + link: https://github.com/atdt/monotonic/blob/master/monotonic.py + +""" + +import time + + +__all__ = ('monotonic',) + + +try: + monotonic = time.monotonic +except AttributeError: + import ctypes + import ctypes.util + import os + import sys + import threading + try: + if sys.platform == 'darwin': # OS X, iOS + # See Technical Q&A QA1398 of the Mac Developer Library: + # + libc = ctypes.CDLL('/usr/lib/libc.dylib', use_errno=True) + + class mach_timebase_info_data_t(ctypes.Structure): + """System timebase info. Defined in .""" + _fields_ = (('numer', ctypes.c_uint32), + ('denom', ctypes.c_uint32)) + + mach_absolute_time = libc.mach_absolute_time + mach_absolute_time.restype = ctypes.c_uint64 + + timebase = mach_timebase_info_data_t() + libc.mach_timebase_info(ctypes.byref(timebase)) + ticks_per_second = timebase.numer / timebase.denom * 1.0e9 + + def monotonic(): + """Monotonic clock, cannot go backward.""" + return mach_absolute_time() / ticks_per_second + + elif sys.platform.startswith('win32') or sys.platform.startswith('cygwin'): + if sys.platform.startswith('cygwin'): + # Note: cygwin implements clock_gettime (CLOCK_MONOTONIC = 4) since + # version 1.7.6. Using raw WinAPI for maximum version compatibility. + + # Ugly hack using the wrong calling convention (in 32-bit mode) + # because ctypes has no windll under cygwin (and it also seems that + # the code letting you select stdcall in _ctypes doesn't exist under + # the preprocessor definitions relevant to cygwin). + # This is 'safe' because: + # 1. The ABI of GetTickCount and GetTickCount64 is identical for + # both calling conventions because they both have no parameters. + # 2. libffi masks the problem because after making the call it doesn't + # touch anything through esp and epilogue code restores a correct + # esp from ebp afterwards. + try: + kernel32 = ctypes.cdll.kernel32 + except OSError: # 'No such file or directory' + kernel32 = ctypes.cdll.LoadLibrary('kernel32.dll') + else: + kernel32 = ctypes.windll.kernel32 + + GetTickCount64 = getattr(kernel32, 'GetTickCount64', None) + if GetTickCount64: + # Windows Vista / Windows Server 2008 or newer. + GetTickCount64.restype = ctypes.c_ulonglong + + def monotonic(): + """Monotonic clock, cannot go backward.""" + return GetTickCount64() / 1000.0 + + else: + # Before Windows Vista. + GetTickCount = kernel32.GetTickCount + GetTickCount.restype = ctypes.c_uint32 + + get_tick_count_lock = threading.Lock() + get_tick_count_last_sample = 0 + get_tick_count_wraparounds = 0 + + def monotonic(): + """Monotonic clock, cannot go backward.""" + global get_tick_count_last_sample + global get_tick_count_wraparounds + + with get_tick_count_lock: + current_sample = GetTickCount() + if current_sample < get_tick_count_last_sample: + get_tick_count_wraparounds += 1 + get_tick_count_last_sample = current_sample + + final_milliseconds = get_tick_count_wraparounds << 32 + final_milliseconds += get_tick_count_last_sample + return final_milliseconds / 1000.0 + + else: + try: + clock_gettime = ctypes.CDLL(ctypes.util.find_library('c'), + use_errno=True).clock_gettime + except Exception: + clock_gettime = ctypes.CDLL(ctypes.util.find_library('rt'), + use_errno=True).clock_gettime + + class timespec(ctypes.Structure): + """Time specification, as described in clock_gettime(3).""" + _fields_ = (('tv_sec', ctypes.c_long), + ('tv_nsec', ctypes.c_long)) + + if sys.platform.startswith('linux'): + CLOCK_MONOTONIC = 1 + elif sys.platform.startswith('freebsd'): + CLOCK_MONOTONIC = 4 + elif sys.platform.startswith('sunos5'): + CLOCK_MONOTONIC = 4 + elif 'bsd' in sys.platform: + CLOCK_MONOTONIC = 3 + elif sys.platform.startswith('aix'): + CLOCK_MONOTONIC = ctypes.c_longlong(10) + + def monotonic(): + """Monotonic clock, cannot go backward.""" + ts = timespec() + if clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(ts)): + errno = ctypes.get_errno() + raise OSError(errno, os.strerror(errno)) + return ts.tv_sec + ts.tv_nsec / 1.0e9 + + # Perform a sanity-check. + if monotonic() - monotonic() > 0: + raise ValueError('monotonic() is not monotonic!') + + except Exception as e: + raise RuntimeError('no suitable implementation for this system: ' + repr(e))