From b4b57af0928e7b4d847e824a2ef185892e161fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matteo=20=E2=84=B1an?= Date: Fri, 6 Mar 2020 22:56:12 +0100 Subject: [PATCH] py-kms Gui: Added pages for options, adjusted custom background, introduced sick animations Added options FILEOFF and STDOUTOFF --- py-kms/graphics/pykms_Arrow_Left.gif | Bin 0 -> 45112 bytes py-kms/graphics/pykms_Arrow_Right.gif | Bin 0 -> 43022 bytes py-kms/graphics/pykms_Keyhole_Left.gif | Bin 0 -> 10240 bytes py-kms/graphics/pykms_Keyhole_Right.gif | Bin 0 -> 10233 bytes py-kms/{ => graphics}/pykms_Keys.gif | Bin py-kms/pykms_Client.py | 27 +- py-kms/pykms_GuiBase.py | 433 ++++++++++++++++-------- py-kms/pykms_GuiMisc.py | 233 +++++++++++-- py-kms/pykms_Misc.py | 63 +++- py-kms/pykms_Server.py | 55 +-- 10 files changed, 600 insertions(+), 211 deletions(-) create mode 100644 py-kms/graphics/pykms_Arrow_Left.gif create mode 100644 py-kms/graphics/pykms_Arrow_Right.gif create mode 100644 py-kms/graphics/pykms_Keyhole_Left.gif create mode 100644 py-kms/graphics/pykms_Keyhole_Right.gif rename py-kms/{ => graphics}/pykms_Keys.gif (100%) diff --git a/py-kms/graphics/pykms_Arrow_Left.gif b/py-kms/graphics/pykms_Arrow_Left.gif new file mode 100644 index 0000000000000000000000000000000000000000..731405dad7b82e777b7814ed4925734c9729d9de GIT binary patch literal 45112 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 HmZbj$=ynq+ literal 0 HcmV?d00001 diff --git a/py-kms/graphics/pykms_Arrow_Right.gif b/py-kms/graphics/pykms_Arrow_Right.gif new file mode 100644 index 0000000000000000000000000000000000000000..422583fdfbf6003d34adadfd9088f724fbeee406 GIT binary patch literal 43022 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 sGabkKW& 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 F{{hXHL4W`N literal 0 HcmV?d00001 diff --git a/py-kms/graphics/pykms_Keyhole_Left.gif b/py-kms/graphics/pykms_Keyhole_Left.gif new file mode 100644 index 0000000000000000000000000000000000000000..5d2933caac82552ed5148581182df54285e5ae37 GIT binary patch literal 10240 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 literal 0 HcmV?d00001 diff --git a/py-kms/graphics/pykms_Keyhole_Right.gif b/py-kms/graphics/pykms_Keyhole_Right.gif new file mode 100644 index 0000000000000000000000000000000000000000..d7499bb5e0d8bf765e3901bbb5e9d2ff973c8bb1 GIT binary patch literal 10233 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
0@rs%c78Tr{fT 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 D3}GaT literal 0 HcmV?d00001 diff --git a/py-kms/pykms_Keys.gif b/py-kms/graphics/pykms_Keys.gif similarity index 100% rename from py-kms/pykms_Keys.gif rename to py-kms/graphics/pykms_Keys.gif diff --git a/py-kms/pykms_Client.py b/py-kms/pykms_Client.py index f208680..0ee89b8 100644 --- a/py-kms/pykms_Client.py +++ b/py-kms/pykms_Client.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- import re import binascii @@ -21,9 +22,9 @@ from pykms_RequestV5 import kmsRequestV5 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_Misc import KmsParser, KmsException, KmsHelper -from pykms_Format import justify, byterize, enco, deco, ShellMessage, pretty_printer +from pykms_Misc import check_setup +from pykms_Misc import KmsParser, KmsParserException, KmsParserHelp +from pykms_Format import justify, byterize, enco, deco, pretty_printer clt_version = "py-kms_2020-02-02" __license__ = "The Unlicense" @@ -91,20 +92,14 @@ def client_options(): try: if "-h" in sys.argv[1:]: - KmsHelper().printer(parsers = [client_parser]) + KmsParserHelp().printer(parsers = [client_parser]) clt_config.update(vars(client_parser.parse_args())) - except KmsException as e: + except KmsParserException 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') + # Setup and some checks. + check_setup(clt_config, clt_options, loggerclt, where = "clt") # Check cmid. if clt_config['cmid'] is not None: @@ -115,6 +110,12 @@ def client_check(): put_text = "{reverse}{red}{bold}Bad CMID. Exiting...{end}") # Check machineName. if clt_config['machineName'] is not None: + try: + clt_config['machineName'].encode('utf-16le') + except UnicodeEncodeError: + pretty_printer(log_obj = loggerclt.error, to_exit = True, where = "clt", + put_text = "{reverse}{red}{bold}Bad machineName. Exiting...{end}") + if len(clt_config['machineName']) < 2 or len(clt_config['machineName']) > 63: 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}") diff --git a/py-kms/pykms_GuiBase.py b/py-kms/pykms_GuiBase.py index 72df2cc..7d3c3df 100644 --- a/py-kms/pykms_GuiBase.py +++ b/py-kms/pykms_GuiBase.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- import os import sys @@ -21,7 +22,8 @@ except ImportError: import tkinter.font as tkFont 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_GuiMisc import ToolTip, TextDoubleScroll, TextRedirect, ListboxOfRadiobuttons +from pykms_GuiMisc import custom_background, custom_pages from pykms_Client import clt_options, clt_version, clt_config, client_thread gui_version = "py-kms_gui_v2.0" @@ -55,7 +57,7 @@ def gui_redirect(str_to_print, where): except: print(str_to_print) -##--------------------------------------------------------------------------------------------------------------------------------------------------------- +##----------------------------------------------------------------------------------------------------------------------------------------------------------- class KmsGui(tk.Tk): def browse(self, entrywidget, options): @@ -69,12 +71,14 @@ class KmsGui(tk.Tk): tk.Tk.__init__(self, *args, **kwargs) self.wraplength = 200 serverthread.with_gui = True - self.validation_int = self.register(self.validate_int) + self.validation_int = (self.register(self.validate_int), "%S") + self.validation_float = (self.register(self.validate_float), "%P") ## 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.optfontredux = tkFont.Font(family = 'Helvetica', size = 9, weight = 'bold') self.msgfont = tkFont.Font(family = 'Monospace', size = 6) # need a monospaced type (like courier, etc..). self.customcolors = { 'black' : '#000000', @@ -88,6 +92,9 @@ class KmsGui(tk.Tk): 'cyan' : '#AFEEEE', 'lavender': '#E6E6FA', } + + self.option_add('*TCombobox*Listbox.font', self.optfontredux) + self.gui_create() def gui_create(self): @@ -102,26 +109,117 @@ class KmsGui(tk.Tk): txcol = self.customcolors ## Redirect stderr. sys.stderr = TextRedirect.StderrRedirect(txsrv, txclt, txcol) - + + def gui_pages_show(self, pagename, side): + # https://stackoverflow.com/questions/7546050/switch-between-two-frames-in-tkinter + # https://www.reddit.com/r/learnpython/comments/7xxtsy/trying_to_understand_tkinter_and_how_to_switch/ + pageside = self.pagewidgets[side] + tk.Misc.lift(pageside["PageWin"][pagename], aboveThis = None) + keylist = list(pageside["PageWin"].keys()) + + for elem in [pageside["BtnAni"], pageside["LblAni"]]: + if pagename == "PageStart": + elem["Left"].config(state = "disabled") + if len(keylist) == 2: + elem["Right"].config(state = "normal") + elif pagename == "PageEnd": + elem["Right"].config(state = "disabled") + if len(keylist) == 2: + elem["Left"].config(state = "normal") + else: + for where in ["Left", "Right"]: + elem[where].config(state = "normal") + + if pagename != "PageStart": + page_l = keylist[keylist.index(pagename) - 1] + pageside["BtnAni"]["Left"]['command'] = lambda pag=page_l, pos=side: self.gui_pages_show(pag, pos) + if pagename != "PageEnd": + page_r = keylist[keylist.index(pagename) + 1] + pageside["BtnAni"]["Right"]['command'] = lambda pag=page_r, pos=side: self.gui_pages_show(pag, pos) + + def gui_pages_buttons(self, parent, side): + btnwin = tk.Canvas(parent, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge') + btnwin.grid(row = 14, column = 2, padx = 2, pady = 2, sticky = 'nsew') + btnwin.grid_columnconfigure(1, weight = 1) + self.pagewidgets[side]["BtnWin"] = btnwin + + for position in ["Left", "Right"]: + if position == "Left": + col = [0, 0, 1] + stick = 'e' + elif position == "Right": + col = [2, 1, 0] + stick = 'w' + + aniwin = tk.Canvas(btnwin, background = self.customcolors['white'], borderwidth = 0, relief = 'ridge') + aniwin.grid(row = 0, column = col[0], padx = 5, pady = 5, sticky = 'nsew') + self.pagewidgets[side]["AniWin"][position] = aniwin + + lblani = tk.Label(aniwin, width = 1, height = 1) + lblani.grid(row = 0, column = col[1], padx = 2, pady = 2, sticky = stick) + self.pagewidgets[side]["LblAni"][position] = lblani + + btnani = tk.Button(aniwin) + btnani.grid(row = 0, column = col[2], padx = 2, pady = 2, sticky = stick) + self.pagewidgets[side]["BtnAni"][position] = btnani + # customize buttons. + custom_pages(self, side) + + def gui_pages_create(self, parent, side, create = {}): + self.pagewidgets.update({side : {"PageWin" : create, + "BtnWin" : None, + "BtnAni" : {"Left" : None, + "Right" : None}, + "AniWin" : {"Left" : None, + "Right" : None}, + "LblAni" : {"Left" : None, + "Right" : None}, + } + }) + + for pagename in self.pagewidgets[side]["PageWin"].keys(): + page = tk.Canvas(parent, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge') + self.pagewidgets[side]["PageWin"][pagename] = page + page.grid(row = 0, column = 2, padx = 2, pady = 2, sticky = "nsew") + page.grid_columnconfigure(1, weight = 1) + self.gui_pages_buttons(parent = parent, side = side) + self.gui_pages_show("PageStart", side = side) + + def gui_store(self, side, typewidgets): + stored = [] + for pagename in self.pagewidgets[side]["PageWin"].keys(): + for widget in self.pagewidgets[side]["PageWin"][pagename].winfo_children(): + if widget.winfo_class() in typewidgets: + stored.append(widget) + return stored + def gui_srv(self): - ## Create main containers. ------------------------------------------------------------------------------------------------------------- + ## Create main containers. ------------------------------------------------------------------------------------------------------------------ self.masterwin = tk.Canvas(self, borderwidth = 3, relief = tk.RIDGE) self.btnsrvwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge') self.optsrvwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge') - # self.optaddsrvwin = tk.Canvas(self.masterwin, background = self.customcolors['white'], borderwidth = 3, relief = 'ridge') self.msgsrvwin = tk.Frame(self.masterwin, background = self.customcolors['black'], relief = 'ridge', width = 300, height = 200) ## Layout main containers. self.masterwin.grid(row = 0, column = 0, sticky = 'nsew') self.btnsrvwin.grid(row = 0, column = 1, padx = 2, pady = 2, sticky = 'nw') - self.optsrvwin.grid(row = 0, column = 2, padx = 2, pady = 2, sticky = 'nw') - # self.optaddsrvwin.grid(row = 0, column = 3, padx = 2, pady = 2, sticky = 'nw') + self.optsrvwin.grid(row = 0, column = 2, padx = 2, pady = 2, sticky = 'nsew') + self.optsrvwin.grid_rowconfigure(0, weight = 1) + self.optsrvwin.grid_columnconfigure(1, weight = 1) + + self.pagewidgets = {} + + ## subpages of optsrvwin. + self.gui_pages_create(parent = self.optsrvwin, side = "Srv", create = {"PageStart": None, + "PageEnd": None}) + + ## continue to grid. self.msgsrvwin.grid(row = 1, column = 2, padx = 1, pady = 1, sticky = 'nsew') self.msgsrvwin.grid_propagate(False) self.msgsrvwin.grid_columnconfigure(0, weight = 1) self.msgsrvwin.grid_rowconfigure(0, weight = 1) - ## Create widgets (btnsrvwin) ----------------------------------------------------------------------------------------------------------- + ## 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_on_start) @@ -140,84 +238,90 @@ class KmsGui(tk.Tk): self.clearbtnsrv.grid(row = 3, column = 0, padx = 2, pady = 2, sticky = 'ew') self.exitbtnsrv.grid(row = 4, column = 0, padx = 2, pady = 2, sticky = 'ew') - ## Create widgets (optsrvwin) ------------------------------------------------------------------------------------------------------ + ## Create widgets (optsrvwin:Srv:PageWin:PageStart) ----------------------------------------------------------------------------------------- # Version. - ver = tk.Label(self.optsrvwin, text = 'You are running server version: ' + srv_version, foreground = self.customcolors['red'], + ver = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], + text = 'You are running server version: ' + srv_version, foreground = self.customcolors['red'], font = self.othfont) - self.allopts_srv = [] # Ip Address. - srvipaddlbl = tk.Label(self.optsrvwin, text = 'IP Address: ', font = self.optfont) - self.srvipadd = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) + srvipaddlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'IP Address: ', font = self.optfont) + self.srvipadd = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], 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) + myipadd = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Your IP address is: {}'.format(get_ip_address()), + foreground = self.customcolors['red'], font = self.othfont) # Port. - 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")) + srvportlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Port: ', font = self.optfont) + self.srvport = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.optfont, validate = "key", + validatecommand = self.validation_int) 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) + epidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'EPID: ', font = self.optfont) + self.epid = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], 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, validate = "key", validatecommand = (self.validation_int, "%S")) + lcidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'LCID: ', font = self.optfont) + self.lcid = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.optfont, validate = "key", + validatecommand = self.validation_int) 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']) + hwidlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'HWID: ', font = self.optfont) + self.hwid = ttk.Combobox(self.pagewidgets["Srv"]["PageWin"]["PageStart"], values = (str(srv_options['hwid']['def']), 'RANDOM'), + width = 17, height = 10, font = self.optfontredux) + self.hwid.set(str(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) + countlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Client Count: ', font = self.optfont) + self.count = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], 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, validate = "key", validatecommand = (self.validation_int, "%S")) + activlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Activation Interval: ', font = self.optfont) + self.activ = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.optfont, validate = "key", + validatecommand = self.validation_int) 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, validate = "key", validatecommand = (self.validation_int, "%S")) + renewlbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Activation Interval: ', font = self.optfont) + self.renew = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.optfont, validate = "key", + validatecommand = self.validation_int) 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. - srvfilelbl = tk.Label(self.optsrvwin, text = 'Logfile Path / Name: ', font = self.optfont) - self.srvfile = tk.Entry(self.optsrvwin, width = 10, font = self.optfont) + srvfilelbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Logfile Path / Name: ', font = self.optfont) + self.srvfile = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], 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) + srvfilebtnwin = tk.Button(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Browse', + command = lambda: self.browse(self.srvfile, srv_options)) + # Loglevel. - srvlevellbl = tk.Label(self.optsrvwin, text = 'Loglevel: ', font = self.optfont) - self.srvlevel = ttk.Combobox(self.optsrvwin, values = tuple(srv_options['llevel']['choi']), width = 10) + srvlevellbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Loglevel: ', font = self.optfont) + self.srvlevel = ttk.Combobox(self.pagewidgets["Srv"]["PageWin"]["PageStart"], values = tuple(srv_options['llevel']['choi']), + width = 10, height = 10, font = self.optfontredux, state = "readonly") 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) + self.chksrvfile = ListboxOfRadiobuttons(self.pagewidgets["Srv"]["PageWin"]["PageStart"], + ['FILE', 'FILEOFF', 'STDOUT', 'STDOUTOFF', 'FILESTDOUT'], + self.optfontredux, + changed = [(self.srvfile, srv_options['lfile']['def']), + (srvfilebtnwin, ''), + (self.srvlevel, srv_options['llevel']['def'])], + width = 10, height = 1, borderwidth = 2, relief = 'ridge') + + # Logsize. + srvsizelbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageStart"], text = 'Logsize: ', font = self.optfont) + self.srvsize = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageStart"], width = 10, font = self.optfont, validate = "key", + validatecommand = self.validation_float) + self.srvsize.insert('end', srv_options['lsize']['def']) + ToolTip(self.srvsize, text = srv_options['lsize']['help'], wraplength = self.wraplength) + + ## Layout widgets (optsrvwin:Srv:PageWin:PageStart) ver.grid(row = 0, column = 0, columnspan = 3, 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') @@ -238,22 +342,41 @@ class KmsGui(tk.Tk): self.renew.grid(row = 9, 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') - 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') + srvfilebtnwin.grid(row = 10, column = 2, padx = 5, pady = 5, sticky = 'ew') + self.chksrvfile.grid(row = 11, column = 1, padx = 5, pady = 5, sticky = 'ew') + srvlevellbl.grid(row = 12, column = 0, padx = 5, pady = 5, sticky = 'e') + self.srvlevel.grid(row = 12, column = 1, padx = 5, pady = 5, sticky = 'ew') + srvsizelbl.grid(row = 13, column = 0, padx = 5, pady = 5, sticky = 'e') + self.srvsize.grid(row = 13, column = 1, padx = 5, pady = 5, sticky = 'ew') - ## Create widgets and layout (msgsrvwin) ----------------------------------------------------------------------------------------------- + ## Create widgets (optsrvwin:Srv:PageWin:PageEnd)------------------------------------------------------------------------------------------- + # Timeout connection. + timeout0lbl = tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Timeout connection: ', font = self.optfont) + self.timeout0 = tk.Entry(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 10, font = self.optfont) + self.timeout0.insert('end', str(srv_options['time0']['def'])) + ToolTip(self.timeout0, text = srv_options['time0']['help'], wraplength = self.wraplength) + # Sqlite database. + self.chkvalsql = tk.BooleanVar() + self.chkvalsql.set(srv_options['sql']['def']) + chksql = tk.Checkbutton(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], text = 'Create Sqlite\nDatabase', + font = self.optfont, var = self.chkvalsql) + ToolTip(chksql, text = srv_options['sql']['help'], wraplength = self.wraplength) + + ## Layout widgets (optsrvwin:Srv:PageWin:PageEnd) + tk.Label(self.pagewidgets["Srv"]["PageWin"]["PageEnd"], width = 0, height = 0).grid(row = 0, column = 0, padx = 5, pady = 5, sticky = 'nw') + timeout0lbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e') + self.timeout0.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew') + chksql.grid(row = 2, column = 1, padx = 5, pady = 5, sticky = 'ew') + + # Store Srv widgets. + self.storewidgets_srv = self.gui_store(side = "Srv", typewidgets = ['Button', 'Entry', 'TCombobox', 'Checkbutton']) + self.storewidgets_srv.append(self.chksrvfile) + + ## Create widgets and layout (msgsrvwin) --------------------------------------------------------------------------------------------------- self.textboxsrv = TextDoubleScroll(self.msgsrvwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled', relief = 'ridge', font = self.msgfont) self.textboxsrv.put() - - ## Create widgets (optaddsrvwin) ----------------------------------------------------------------------------------------------------- - # self.timeout = tk.Entry(self.optaddsrvwin, width = 10) - # self.timeout.insert('end', '555') - ## Layout widgets (optaddsrvwin) - # self.timeout.grid(row = 0, column = 0, padx = 5, pady = 5, sticky = 'e') - + def gui_complete(self): ## Create client widgets (optcltwin, msgcltwin, btncltwin) self.update_idletasks() # update Gui to get btnsrvwin values --> btncltwin. @@ -284,76 +407,93 @@ class KmsGui(tk.Tk): self.btncltwin_X = xb + 2 self.btncltwin_Y = yb + hb + 10 self.btncltwin.place(x = self.btncltwin_X, y = self.btncltwin_Y, bordermode = 'inside', anchor = 'nw') - self.optcltwin.grid(row = 0, column = 4, padx = 2, pady = 2, sticky = 'nw') + self.optcltwin.grid(row = 0, column = 4, padx = 2, pady = 2, sticky = 'nsew') + self.optcltwin.grid_rowconfigure(0, weight = 1) + self.optcltwin.grid_columnconfigure(1, weight = 1) + + # subpages of optcltwin. + self.gui_pages_create(parent = self.optcltwin, side = "Clt", create = {"PageStart": None, + "PageEnd": None}) + + # continue to grid. self.msgcltwin.grid(row = 1, column = 4, padx = 1, pady = 1, sticky = 'nsew') self.msgcltwin.grid_propagate(False) self.msgcltwin.grid_columnconfigure(0, weight = 1) self.msgcltwin.grid_rowconfigure(0, weight = 1) - # Create widgets (btncltwin) ------------------------------------------------------------------------------------------------------------ + # 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_on_start) - #self.othbutt = tk.Button(self.btncltwin, text = 'Botton\n2', background = self.customcolors['green'], - # foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont) +## self.othbutt = tk.Button(self.btncltwin, text = 'Botton\n2', background = self.customcolors['green'], +## foreground = self.customcolors['white'], relief = 'flat', font = self.btnwinfont) # Layout widgets (btncltwin) self.runbtnclt.grid(row = 0, column = 0, padx = 2, pady = 2, sticky = 'ew') - #self.othbutt.grid(row = 1, column = 0, padx = 2, pady = 2, sticky = 'ew') +## self.othbutt.grid(row = 1, column = 0, padx = 2, pady = 2, sticky = 'ew') - # Create widgets (optcltwin) ------------------------------------------------------------------------------------------------------------ + # Create widgets (optcltwin:Clt:PageWin:PageStart) ------------------------------------------------------------------------------------------ # Version. - cltver = tk.Label(self.optcltwin, text = 'You are running client version: ' + clt_version, foreground = self.customcolors['red'], - font = self.othfont) - self.allopts_clt = [] + cltver = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'You are running client version: ' + clt_version, + foreground = self.customcolors['red'], font = self.othfont) # Ip Address. - cltipaddlbl = tk.Label(self.optcltwin, text = 'IP Address: ', font = self.optfont) - self.cltipadd = tk.Entry(self.optcltwin, width = 10, font = self.optfont) + cltipaddlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'IP Address: ', font = self.optfont) + self.cltipadd = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], 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, validate = "key", validatecommand = (self.validation_int, "%S")) + cltportlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Port: ', font = self.optfont) + self.cltport = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.optfont, validate = "key", + validatecommand = self.validation_int) 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) + cltmodelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Mode: ', font = self.optfont) + self.cltmode = ttk.Combobox(self.pagewidgets["Clt"]["PageWin"]["PageStart"], values = tuple(clt_options['mode']['choi']), + width = 17, height = 10, font = self.optfontredux, state = "readonly") 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) + cltcmidlbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'CMID: ', font = self.optfont) + self.cltcmid = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], 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) + cltnamelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Machine Name: ', font = self.optfont) + self.cltname = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], 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) + cltfilelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Logfile Path / Name: ', font = self.optfont) + self.cltfile = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], 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) + cltfilebtnwin = tk.Button(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Browse', + command = lambda: self.browse(self.cltfile, clt_options)) # 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) + cltlevellbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Loglevel: ', font = self.optfont) + self.cltlevel = ttk.Combobox(self.pagewidgets["Clt"]["PageWin"]["PageStart"], values = tuple(clt_options['llevel']['choi']), + width = 10, height = 10, font = self.optfontredux, state = "readonly") self.cltlevel.set(clt_options['llevel']['def']) ToolTip(self.cltlevel, text = clt_options['llevel']['help'], wraplength = self.wraplength) - self.allopts_clt.append(self.cltlevel) + + self.chkcltfile = ListboxOfRadiobuttons(self.pagewidgets["Clt"]["PageWin"]["PageStart"], + ['FILE', 'FILEOFF', 'STDOUT', 'STDOUTOFF', 'FILESTDOUT'], + self.optfontredux, + changed = [(self.cltfile, clt_options['lfile']['def']), + (cltfilebtnwin, ''), + (self.cltlevel, clt_options['llevel']['def'])], + width = 10, height = 1, borderwidth = 2, relief = 'ridge') + # Logsize. + cltsizelbl = tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageStart"], text = 'Logsize: ', font = self.optfont) + self.cltsize = tk.Entry(self.pagewidgets["Clt"]["PageWin"]["PageStart"], width = 10, font = self.optfont, validate = "key", + validatecommand = self.validation_float) + self.cltsize.insert('end', clt_options['lsize']['def']) + ToolTip(self.cltsize, text = clt_options['lsize']['help'], wraplength = self.wraplength) - # Layout widgets (optcltwin) + # Layout widgets (optcltwin:Clt:PageWin:PageStart) cltver.grid(row = 0, column = 0, columnspan = 3, padx = 5, pady = 5, sticky = 'ew') cltipaddlbl.grid(row = 1, column = 0, padx = 5, pady = 5, sticky = 'e') self.cltipadd.grid(row = 1, column = 1, padx = 5, pady = 5, sticky = 'ew') @@ -365,40 +505,67 @@ class KmsGui(tk.Tk): self.cltcmid.grid(row = 4, column = 1, padx = 5, pady = 5, sticky = 'ew') cltnamelbl.grid(row = 5, column = 0, padx = 5, pady = 5, sticky = 'e') self.cltname.grid(row = 5, column = 1, padx = 5, pady = 5, sticky = 'ew') - cltfilelbl.grid(row = 6, column = 0, padx = 5, pady = 5, sticky = 'ew') - self.cltfile.grid(row = 6, column = 1, padx = 5, pady = 5, sticky = 'e') + cltfilelbl.grid(row = 6, column = 0, padx = 5, pady = 5, sticky = 'e') + self.cltfile.grid(row = 6, column = 1, padx = 5, pady = 5, sticky = 'ew') cltfilebtnwin.grid(row = 6, column = 2, padx = 5, pady = 5, sticky = 'ew') - cltlevellbl.grid(row = 7, column = 0, padx = 5, pady = 5, sticky = 'e') - self.cltlevel.grid(row = 7, column = 1, padx = 5, pady = 5, sticky = 'ew') + self.chkcltfile.grid(row = 7, column = 1, padx = 5, pady = 5, sticky = 'ew') + cltlevellbl.grid(row = 8, column = 0, padx = 5, pady = 5, sticky = 'e') + self.cltlevel.grid(row = 8, column = 1, padx = 5, pady = 5, sticky = 'ew') + cltsizelbl.grid(row = 9, column = 0, padx = 5, pady = 5, sticky = 'e') + self.cltsize.grid(row = 9, column = 1, padx = 5, pady = 5, sticky = 'ew') + + ## Create widgets (optcltwin:Clt:PageWin:PageEnd) ------------------------------------------------------------------------------------------- + + ## Layout widgets (optcltwin:Clt:PageWin:PageEnd) + tk.Label(self.pagewidgets["Clt"]["PageWin"]["PageEnd"], width = 0, height = 0).grid(row = 0, column = 0, padx = 5, pady = 5, sticky = 'nw') + + # Store Clt widgets. + self.storewidgets_clt = self.gui_store(side = "Clt", typewidgets = ['Button', 'Entry', 'TCombobox']) + self.storewidgets_clt.append(self.chkcltfile) - # Create widgets and layout (msgcltwin) ---------------------------------------------------------------------------------------------------------- + # Create widgets and layout (msgcltwin) ----------------------------------------------------------------------------------------------------- self.textboxclt = TextDoubleScroll(self.msgcltwin, background = self.customcolors['black'], wrap = 'none', state = 'disabled', relief = 'ridge', font = self.msgfont) self.textboxclt.put() def prep_option(self, value): - value = None if value == 'None' else value try: + # is an INT return int(value) except (TypeError, ValueError): - # is NONE or is a STRING. - return value + try: + # is a FLOAT + return float(value) + except (TypeError, ValueError): + # 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 prep_logfile(self, filepath): + # FILE (pretty on, log view off, logfile yes) + # FILEOFF (pretty on, log view off, no logfile) + # STDOUT (pretty off, log view on, no logfile) + # STDOUTOFF (pretty off, log view off, logfile yes) + # FILESTDOUT (pretty off, log view on, logfile yes) + st = self.chksrvfile.state() + if st == 'FILE': + return filepath + elif st in ['FILESTDOUT', 'STDOUTOFF']: + return [st, filepath] + elif st in ['STDOUT', 'FILEOFF']: + return st def validate_int(self, value): - return value.isdigit() - + return value == '' or value.isdigit() + + def validate_float(self, value): + if value == "": + return True + try: + float(value) + return True + except ValueError: + return False + def clt_on_show(self, force = False): if self.optcltwin.winfo_ismapped() or force: self.shbtnclt['text'] = 'SHOW\nCLIENT' @@ -436,7 +603,7 @@ class KmsGui(tk.Tk): 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['epid']['des']] = 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()) @@ -444,11 +611,9 @@ class KmsGui(tk.Tk): 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 + srv_config[srv_options['sql']['des']] = self.chkvalsql.get() + srv_config[srv_options['lsize']['des']] = self.prep_option(self.srvsize.get()) + srv_config[srv_options['time0']['des']] = self.prep_option(self.timeout0.get()) serverqueue.put('start') @@ -468,13 +633,13 @@ class KmsGui(tk.Tk): if on_start: self.runbtnsrv.configure(text = 'STOP\nSERVER', background = self.customcolors['red'], foreground = self.customcolors['white']) - for widget in self.allopts_srv: + for widget in self.storewidgets_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: + for widget in self.storewidgets_srv: widget.configure(state = 'normal') self.runbtnclt.configure(state = 'disabled') @@ -494,20 +659,18 @@ class KmsGui(tk.Tk): self.clt_eject_thread.start() self.on_clear([txsrv, txclt]) - for widget in self.allopts_clt + [self.runbtnsrv, self.runbtnclt]: + for widget in self.storewidgets_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['cmid']['des']] = self.cltcmid.get() + clt_config[clt_options['name']['des']] = 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 + clt_config[clt_options['lsize']['des']] = self.prep_option(self.cltsize.get()) # run client (in a thread). self.clientthread = client_thread(name = "Thread-Clt") @@ -518,7 +681,7 @@ class KmsGui(tk.Tk): def clt_eject(self): while self.clientthread.is_alive(): sleep(0.1) - for widget in self.allopts_clt + [self.runbtnsrv, self.runbtnclt]: + for widget in self.storewidgets_clt + [self.runbtnsrv, self.runbtnclt]: widget.configure(state = 'normal') def on_exit(self): diff --git a/py-kms/pykms_GuiMisc.py b/py-kms/pykms_GuiMisc.py index 74bcb64..b1c7914 100644 --- a/py-kms/pykms_GuiMisc.py +++ b/py-kms/pykms_GuiMisc.py @@ -4,6 +4,8 @@ import os import re import sys from collections import Counter +from time import sleep +import threading try: # Python 2.x imports @@ -18,7 +20,7 @@ except ImportError: from pykms_Format import MsgMap, unshell_message, unformat_message -#--------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------ # https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter class ToolTip(object): @@ -117,7 +119,7 @@ class ToolTip(object): tw.destroy() self.tw = None -##-------------------------------------------------------------------------------------------------------------------------------------------------------- +##----------------------------------------------------------------------------------------------------------------------------------------------------------- # https://stackoverflow.com/questions/2914603/segmentation-fault-while-redirecting-sys-stdout-to-tkinter-text-widget # 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 @@ -237,7 +239,7 @@ class TextRedirect(object): self.srv_text_space.see('end') self.srv_text_space.configure(state = 'disabled') -##------------------------------------------------------------------------------------------------------------------------------------------------------- +##----------------------------------------------------------------------------------------------------------------------------------------------------------- class TextDoubleScroll(tk.Frame): def __init__(self, master, **kwargs): """ Initialize. @@ -245,8 +247,8 @@ class TextDoubleScroll(tk.Frame): - vertical scrollbar - text widget """ - self.master = master tk.Frame.__init__(self, master) + self.master = master self.textbox = tk.Text(self.master, **kwargs) self.sizegrip = ttk.Sizegrip(self.master) @@ -278,43 +280,232 @@ class TextDoubleScroll(tk.Frame): """ Return the "frame" useful to place inner controls. """ return self.textbox -##-------------------------------------------------------------------------------------------------------------------------------------------------- +##----------------------------------------------------------------------------------------------------------------------------------------------------------- def custom_background(window): + # first level canvas. allwidgets = window.grid_slaves(0,0)[0].grid_slaves() + window.grid_slaves(0,0)[0].place_slaves() - widgets = [ widget for widget in allwidgets if widget.winfo_class() == 'Canvas'] + widgets_alphalow = [ widget for widget in allwidgets if widget.winfo_class() == 'Canvas'] + widgets_alphahigh = [] + # sub-level canvas. + for side in ["Srv", "Clt"]: + widgets_alphahigh.append(window.pagewidgets[side]["BtnWin"]) + for position in ["Left", "Right"]: + widgets_alphahigh.append(window.pagewidgets[side]["AniWin"][position]) + for pagename in window.pagewidgets[side]["PageWin"].keys(): + widgets_alphalow.append(window.pagewidgets[side]["PageWin"][pagename]) try: from PIL import Image, ImageTk # Open Image. - img = Image.open(os.path.dirname(os.path.abspath( __file__ )) + "/pykms_Keys.gif") + img = Image.open(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keys.gif") + img = img.convert('RGBA') # Resize image. img.resize((window.winfo_width(), window.winfo_height()), Image.ANTIALIAS) # Put semi-transparent background chunks. - window.backcrops = [] - - for widget in widgets: - x, y, w, h = window.get_position(widget) - cropped = img.crop((x, y, x + w, y + h)) - cropped.putalpha(24) - window.backcrops.append(ImageTk.PhotoImage(cropped)) - - # Not in same loop to prevent reference garbage. - for crop, widget in zip(window.backcrops, widgets): - widget.create_image(1, 1, image = crop, anchor = 'nw') + window.backcrops_alphalow, window.backcrops_alphahigh = ([] for _ in range(2)) + + def cutter(master, image, widgets, crops, alpha): + for widget in widgets: + x, y, w, h = master.get_position(widget) + cropped = image.crop((x, y, x + w, y + h)) + cropped.putalpha(alpha) + crops.append(ImageTk.PhotoImage(cropped)) + # Not in same loop to prevent reference garbage. + for crop, widget in zip(crops, widgets): + widget.create_image(1, 1, image = crop, anchor = 'nw') + + cutter(window, img, widgets_alphalow, window.backcrops_alphalow, 36) + cutter(window, img, widgets_alphahigh, window.backcrops_alphahigh, 96) # Put semi-transparent background overall. - img.putalpha(96) + img.putalpha(128) window.backimg = ImageTk.PhotoImage(img) window.masterwin.create_image(1, 1, image = window.backimg, anchor = 'nw') except ImportError: - for widget in widgets: + for widget in widgets_alphalow + widgets_alphahigh: widget.configure(background = window.customcolors['lavender']) # Hide client. window.clt_on_show(force = True) # Show Gui. window.deiconify() + +##----------------------------------------------------------------------------------------------------------------------------------------------------------- +class Animation(object): + def __init__(self, gifpath, master, widget, loop = False): + from PIL import Image, ImageTk, ImageSequence + + self.master = master + self.widget = widget + self.loop = loop + self.cancelid = None + self.flagstop = False + self.index = 0 + self.frames = [] + + img = Image.open(gifpath) + size = img.size + for frame in ImageSequence.Iterator(img): + static_img = ImageTk.PhotoImage(frame.convert('RGBA')) + try: + static_img.delay = int(frame.info['duration']) + except KeyError: + static_img.delay = 100 + self.frames.append(static_img) + + self.widget.configure(width = size[0], height = size[1]) + self.initialize() + + def initialize(self): + self.widget.configure(image = self.frames[0]) + self.widget.image = self.frames[0] + + def deanimate(self): + while not self.flagstop: + pass + self.flagstop = False + self.index = 0 + self.widget.configure(relief = "raised") + + def animate(self): + frame = self.frames[self.index] + self.widget.configure(image = frame, relief = "sunken") + self.index += 1 + self.cancelid = self.master.after(frame.delay, self.animate) + if self.index == len(self.frames): + if self.loop: + self.index = 0 + else: + self.stop() + + def start(self, event = None): + if str(self.widget['state']) != 'disabled': + if self.cancelid is None: + if not self.loop: + self.btnani_thread = threading.Thread(target = self.deanimate, name = "Thread-BtnAni") + self.btnani_thread.setDaemon(True) + self.btnani_thread.start() + self.cancelid = self.master.after(self.frames[0].delay, self.animate) + + def stop(self, event = None): + if self.cancelid: + self.master.after_cancel(self.cancelid) + self.cancelid = None + self.flagstop = True + self.initialize() + + +def custom_pages(window, side): + buttons = window.pagewidgets[side]["BtnAni"] + labels = window.pagewidgets[side]["LblAni"] -##--------------------------------------------------------------------------------------------------------------------------------------------------------- + for position in buttons.keys(): + buttons[position].config(anchor = "center", + font = window.btnwinfont, + background = window.customcolors['white'], + activebackground = window.customcolors['white'], + borderwidth = 2) + + try: + anibtn = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Keyhole_%s.gif" %position, + window, buttons[position], loop = False) + anilbl = Animation(os.path.dirname(os.path.abspath( __file__ )) + "/graphics/pykms_Arrow_%s.gif" %position, + window, labels[position], loop = True) + + def animationwait(master, button, btn_animation, lbl_animation): + while btn_animation.cancelid: + pass + sleep(1) + x, y = master.winfo_pointerxy() + if master.winfo_containing(x, y) == button: + lbl_animation.start() + + def animationcombo(master, button, btn_animation, lbl_animation): + wait_thread = threading.Thread(target = animationwait, + args = (master, button, btn_animation, lbl_animation), + name = "Thread-WaitAni") + wait_thread.setDaemon(True) + wait_thread.start() + lbl_animation.stop() + btn_animation.start() + + buttons[position].bind("", lambda event, anim1 = anibtn, anim2 = anilbl, + bt = buttons[position], win = window: + animationcombo(win, bt, anim1, anim2)) + buttons[position].bind("", anilbl.start) + buttons[position].bind("", anilbl.stop) + + except ImportError: + buttons[position].config(activebackground = window.customcolors['blue'], + foreground = window.customcolors['blue']) + labels[position].config(background = window.customcolors['lavender']) + + if position == "Left": + buttons[position].config(text = '<<') + elif position == "Right": + buttons[position].config(text = '>>') + +##----------------------------------------------------------------------------------------------------------------------------------------------------------- +class ListboxOfRadiobuttons(tk.Frame): + def __init__(self, master, radios, font, changed, **kwargs): + tk.Frame.__init__(self, master) + + self.master = master + self.radios = radios + self.font = font + self.changed = changed + + self.scrollv = tk.Scrollbar(self, orient = "vertical") + self.textbox = tk.Text(self, yscrollcommand = self.scrollv.set, **kwargs) + self.scrollv.config(command = self.textbox.yview) + # layout. + self.scrollv.pack(side = "right", fill = "y") + self.textbox.pack(side = "left", fill = "both", expand = True) + # create radiobuttons. + self.radiovar = tk.StringVar() + self.radiovar.set('FILE') + self.create() + + def create(self): + self.rdbtns = [] + for n, nameradio in enumerate(self.radios): + rdbtn = tk.Radiobutton(self, text = nameradio, value = nameradio, variable = self.radiovar, + font = self.font, indicatoron = 0, width = 15, + borderwidth = 3, selectcolor = 'yellow', command = self.change) + self.textbox.window_create("end", window = rdbtn) + # to force one checkbox per line + if n != len(self.radios) - 1: + self.textbox.insert("end", "\n") + self.rdbtns.append(rdbtn) + self.textbox.configure(state = "disabled") + + def change(self): + st = self.state() + for widget, default in self.changed: + wclass = widget.winfo_class() + if st in ['STDOUT', 'FILEOFF']: + if wclass == 'Entry': + widget.delete(0, 'end') + elif wclass == 'TCombobox': + widget.set('') + widget.configure(state = "disabled") + elif st in ['FILE', 'FILESTDOUT', 'STDOUTOFF']: + if wclass == 'Entry': + widget.configure(state = "normal") + widget.delete(0, 'end') + widget.insert('end', default) + widget.xview_moveto(1) + elif wclass == 'TCombobox': + widget.configure(state = "readonly") + widget.set(default) + elif wclass == 'Button': + widget.configure(state = "normal") + + def configure(self, state): + for rb in self.rdbtns: + rb.configure(state = state) + + def state(self): + return self.radiovar.get() diff --git a/py-kms/pykms_Misc.py b/py-kms/pykms_Misc.py index 3db56a5..022d473 100644 --- a/py-kms/pykms_Misc.py +++ b/py-kms/pykms_Misc.py @@ -6,9 +6,9 @@ import logging import os import argparse from logging.handlers import RotatingFileHandler -from pykms_Format import ColorExtraMap, pretty_printer +from pykms_Format import ColorExtraMap, ShellMessage, pretty_printer -#----------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------ # https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility # https://stackoverflow.com/questions/17558552/how-do-i-add-custom-field-to-python-log-format-string @@ -93,14 +93,19 @@ def logger_create(log_obj, config, mode = 'a'): # Configure visualization. log_handlers = [] - if any(i in ['STDOUT', 'FILESTDOUT'] for i in config['logfile']): - # (Only STDOUT) or (logfile and STDOUT) - log_handlers.append(logging.StreamHandler(sys.stdout)) - if 'FILESTDOUT' in config['logfile']: + if any(opt in ['STDOUT', 'FILESTDOUT', 'STDOUTOFF'] for opt in config['logfile']): + if 'STDOUTOFF' not in config['logfile']: + # STDOUT. + log_handlers.append(logging.StreamHandler(sys.stdout)) + if any(opt in ['STDOUTOFF', 'FILESTDOUT'] for opt in config['logfile']): + # FILESTDOUT or STDOUTOFF. log_handlers.append(RotatingFileHandler(filename = config['logfile'][1], mode = mode, maxBytes = int(config['logsize'] * 1024 * 512), backupCount = 1, encoding = None, delay = 0)) + elif 'FILEOFF' in config['logfile']: + config['loglevel'] = 'ERROR' # for py-kms GUI: set a recognized level never used. + log_handlers.append(logging.FileHandler(os.devnull)) else: - # Only logfile. + # FILE. log_handlers.append(RotatingFileHandler(filename = config['logfile'][0], mode = mode, maxBytes = int(config['logsize'] * 1024 * 512), backupCount = 1, encoding = None, delay = 0)) @@ -132,7 +137,7 @@ def logger_create(log_obj, config, mode = 'a'): log_obj.setLevel(config['loglevel']) [ log_obj.addHandler(log_handler) for log_handler in log_handlers ] -#---------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------ def check_logfile(optionlog, defaultlog, where): if not isinstance(optionlog, list): @@ -154,7 +159,8 @@ def check_logfile(optionlog, defaultlog, where): if lenopt > 2: pretty_printer(put_text = msg_long, where = where, to_exit = True) - if 'FILESTDOUT' in optionlog: + + if (any(opt in ['FILESTDOUT', 'STDOUTOFF'] for opt in optionlog)): if lenopt == 1: # add default logfile. optionlog.append(defaultlog) @@ -164,12 +170,13 @@ def check_logfile(optionlog, defaultlog, where): else: if lenopt == 2: pretty_printer(put_text = msg_long, where = where, to_exit = True) - elif lenopt == 1 and 'STDOUT' not in optionlog: + elif lenopt == 1 and (any(opt not in ['STDOUT', 'FILEOFF'] for opt in optionlog)): # check directory path. checkdir(optionlog[0]) + return optionlog -#---------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------ # Valid language identifiers to be used in the EPID (see "kms.c" in vlmcsd) ValidLcid = [1025, 1026, 1027, 1028, 1029, @@ -213,16 +220,16 @@ def check_lcid(lcid, log_obj): return fixlcid return lcid -#---------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------ -class KmsException(Exception): +class KmsParserException(Exception): pass class KmsParser(argparse.ArgumentParser): def error(self, message): - raise KmsException(message) + raise KmsParserException(message) -class KmsHelper(object): +class KmsParserHelp(object): def replace(self, parser, replace_epilog_with): text = parser.format_help().splitlines() help_list = [] @@ -255,7 +262,31 @@ class KmsHelper(object): print(parser_base.epilog + '\n') parser_base.exit() -#---------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------ +def proper_none(dictionary): + for key in dictionary.keys(): + dictionary[key] = None if dictionary[key] == 'None' else dictionary[key] + +def check_setup(config, options, logger, where): + # Check logfile. + config['logfile'] = check_logfile(config['logfile'], options['lfile']['def'], where = where) + + # Setup hidden or not messages. + hidden = ['STDOUT', 'FILESTDOUT', 'STDOUTOFF'] + ShellMessage.view = (False if any(opt in hidden for opt in config['logfile']) else True) + + # Create log. + logger_create(logger, config, mode = 'a') + + # 'None'--> None. + proper_none(config) + + # Check port. + if not 1 <= config['port'] <= 65535: + pretty_printer(log_obj = logger.error, to_exit = True, + put_text = "{reverse}{red}{bold}Port number '%s' is invalid. Enter between 1 - 65535. Exiting...{end}" %config['port']) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------ # http://joshpoley.blogspot.com/2011/09/hresults-user-0x004.html (slerror.h) ErrorCodes = { diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py index 883d0de..9b5eb06 100755 --- a/py-kms/pykms_Server.py +++ b/py-kms/pykms_Server.py @@ -27,9 +27,9 @@ except ImportError: import pykms_RpcBind, pykms_RpcRequest from pykms_RpcBase import rpcBase from pykms_Dcerpc import MSRPCHeader -from pykms_Misc import logger_create, check_logfile, check_lcid -from pykms_Misc import KmsParser, KmsException, KmsHelper -from pykms_Format import enco, deco, ShellMessage, pretty_printer +from pykms_Misc import check_setup, check_lcid +from pykms_Misc import KmsParser, KmsParserException, KmsParserHelp +from pykms_Format import enco, deco, pretty_printer from Etrigan import Etrigan, Etrigan_parser, Etrigan_check, Etrigan_job srv_version = "py-kms_2020-02-02" @@ -177,11 +177,13 @@ for server OSes and Office >=5', 'def' : None, 'des' : "CurrentClientCount"}, 'def' : False, 'des' : "sqlite"}, 'hwid' : {'help' : 'Use this option 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.', 'def' : "364F463A8863D35F", 'des' : "hwid"}, - 'time' : {'help' : 'Max time (in seconds) for server to generate an answer. If \"None\" (default) serve forever.', 'def' : None, 'des' : "timeout"}, + 'time0' : {'help' : 'Maximum inactivity time (in seconds) after which the connection with the client is closed. If \"None\" (default) serve forever.', + 'def' : None, 'des' : "timeout_idle"}, 'llevel' : {'help' : 'Use this option to set a log level. The default is \"ERROR\".', 'def' : "ERROR", 'des' : "loglevel", '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.', + '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. \ +Use \"STDOUTOFF\" to disable stdout messages. Use \"FILEOFF\" if you not want to create 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"}, } @@ -199,7 +201,7 @@ def server_options(): 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) + help = srv_options['count']['help'], type = str) 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'], @@ -208,8 +210,8 @@ def server_options(): 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("-t0", "--timeout-idle", action = "store", dest = srv_options['time0']['des'], default = srv_options['time0']['def'], + help = srv_options['time0']['help'], type = str) 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'], @@ -234,7 +236,7 @@ def server_options(): try: if "-h" in sys.argv[1:]: - KmsHelper().printer(parsers = [server_parser, daemon_parser, etrigan_parser]) + KmsParserHelp().printer(parsers = [server_parser, daemon_parser, etrigan_parser]) # Set defaults for config. # case: python3 pykms_Server.py @@ -267,11 +269,11 @@ def server_options(): # 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)) + raise KmsParserException("unrecognized arguments: %s" %' '.join(knw_extras)) else: srv_config.update(vars(knw_args)) - except KmsException as e: + except KmsParserException as e: pretty_printer(put_text = "{reverse}{red}{bold}%s. Exiting...{end}" %str(e), to_exit = True) @@ -311,14 +313,8 @@ def server_daemon(): 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') + # Setup and some checks. + check_setup(srv_config, srv_options, loggersrv, where = "srv") # Random HWID. if srv_config['hwid'] == "RANDOM": @@ -361,14 +357,21 @@ def server_check(): else: srv_config['dbSupport'] = True - # Check port. - if not 1 <= srv_config['port'] <= 65535: - 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']) + # Check client count, timeout. + list_dest = ['CurrentClientCount', 'timeout_idle'] + list_opt = ['--client-count', '--timeout-idle'] + for dest, opt in zip(list_dest, list_opt): + if srv_config[dest] is not None: + if not srv_config[dest].isdigit(): + pretty_printer(log_obj = loggersrv.error, to_exit = True, + put_text = "{reverse}{red}{bold}Option %s is invalid with '%s'. Exiting...{end}" + %(opt, srv_config[dest])) + else: + srv_config[dest] = int(srv_config[dest]) def server_create(): server = KeyServer((srv_config['ip'], srv_config['port']), kmsServerHandler) - server.timeout = srv_config['timeout'] + server.timeout = srv_config['timeout_idle'] 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 @@ -427,7 +430,7 @@ def server_with_gui(): height = 660 root = pykms_GuiBase.KmsGui() - root.title(pykms_GuiBase.gui_description + ' ' + pykms_GuiBase.gui_version) + root.title(pykms_GuiBase.gui_description + ' (' + pykms_GuiBase.gui_version + ')') # Main window initial position. ## https://stackoverflow.com/questions/14910858/how-to-specify-where-a-tkinter-window-opens ws = root.winfo_screenwidth()