From bc42ff6f28a1d6941d7bbafb6b5910191d063ec4 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Wed, 27 Jul 2016 15:55:46 +0100 Subject: [PATCH] Allow monitoring of client log over network share on windows and Linux. --- L10n/en.template | 10 +++- hotkey.py | 5 ++ monitor.py | 85 ++++++++++++++++++++++--------- prefs.py | 130 +++++++++++++++++++++++++++++++++-------------- 4 files changed, 166 insertions(+), 64 deletions(-) diff --git a/L10n/en.template b/L10n/en.template index 88012a9c..f9cb5c33 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -70,6 +70,9 @@ /* Combat rank. [stats.py] */ "Competent" = "Competent"; +/* Tab heading in settings. [prefs.py] */ +"Configuration" = "Configuration"; + /* Update button in main window. [EDMarketConnector.py] */ "cooldown {SS}s" = "cooldown {SS}s"; @@ -103,6 +106,9 @@ /* Empire rank. [stats.py] */ "Duke" = "Duke"; +/* Configuration setting. [prefs.py] */ +"E:D log file location" = "E:D log file location"; + /* Empire rank. [stats.py] */ "Earl" = "Earl"; @@ -181,7 +187,7 @@ /* Dark theme color setting. [prefs.py] */ "Highlighted text" = "Highlighted text"; -/* Tab heading in settings on Windows. [prefs.py] */ +/* Hotkey/Shortcut settings prompt on Windows. [prefs.py] */ "Hotkey" = "Hotkey"; /* [prefs.py] */ @@ -190,7 +196,7 @@ /* Tab heading in settings. [prefs.py] */ "Identity" = "Identity"; -/* Tab heading in settings on OSX. [prefs.py] */ +/* Hotkey/Shortcut settings prompt on OSX. [prefs.py] */ "Keyboard shortcut" = "Keyboard shortcut"; /* Empire rank. [stats.py] */ diff --git a/hotkey.py b/hotkey.py index 50c1b36c..3ba0ceda 100644 --- a/hotkey.py +++ b/hotkey.py @@ -410,6 +410,11 @@ else: # Linux def unregister(self): pass + def play_good(self): + pass + + def play_bad(self): + pass # singleton hotkeymgr = HotkeyMgr() diff --git a/monitor.py b/monitor.py index 2fd46e4b..c5655a77 100644 --- a/monitor.py +++ b/monitor.py @@ -1,7 +1,7 @@ import atexit import re import threading -from os import listdir, pardir, rename, unlink +from os import listdir, pardir, rename, unlink, SEEK_SET, SEEK_CUR, SEEK_END from os.path import basename, exists, isdir, isfile, join from platform import machine import sys @@ -53,6 +53,7 @@ elif platform=='win32': RegEnumKeyEx.argtypes = [HKEY, DWORD, LPWSTR, ctypes.POINTER(DWORD), ctypes.POINTER(DWORD), LPWSTR, ctypes.POINTER(DWORD), ctypes.POINTER(FILETIME)] else: + # Linux's inotify doesn't work over CIFS or NFS, so poll FileSystemEventHandler = object # dummy @@ -63,9 +64,11 @@ class EDLogs(FileSystemEventHandler): def __init__(self): FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog self.root = None - self.logdir = self._logdir() + self.logdir = self._logdir() # E:D client's default Logs directory, or None if not found + self.currentdir = None # The actual logdir that we're monitoring self.logfile = None self.observer = None + self.observed = None self.thread = None self.callbacks = { 'Jump': None, 'Dock': None } self.last_event = None # for communicating the Jump event @@ -76,40 +79,58 @@ class EDLogs(FileSystemEventHandler): def start(self, root): self.root = root - if not self.logdir: + logdir = config.get('logdir') or self.logdir + if not logdir or not isdir(logdir): self.stop() return False - if self.running(): - return True + + if self.currentdir and self.currentdir != logdir: + self.stop() + self.currentdir = logdir self.root.bind_all('<>', self.jump) # user-generated self.root.bind_all('<>', self.dock) # user-generated # Set up a watchog observer. This is low overhead so is left running irrespective of whether monitoring is desired. - if not self.observer: - if __debug__: - print 'Monitoring "%s"' % self.logdir - elif getattr(sys, 'frozen', False): - sys.stderr.write('Monitoring "%s"\n' % self.logdir) - sys.stderr.flush() # Required for line to show up immediately on Windows - + # File system events are unreliable/non-existent over network drives on Linux. + # We can't easily tell whether a path points to a network drive, so assume + # any non-standard logdir might be on a network drive and poll instead. + polling = bool(config.get('logdir')) and platform != 'win32' + if not polling and not self.observer: self.observer = Observer() self.observer.daemon = True - self.observer.schedule(self, self.logdir) self.observer.start() atexit.register(self.observer.stop) - # Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically. - logfiles = sorted([x for x in listdir(self.logdir) if x.startswith('netLog.')]) - self.logfile = logfiles and join(self.logdir, logfiles[-1]) or None + if not self.observed and not polling: + self.observed = self.observer.schedule(self, self.currentdir) + + # Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically. + try: + logfiles = sorted([x for x in listdir(logdir) if x.startswith('netLog.')]) + self.logfile = logfiles and join(logdir, logfiles[-1]) or None + except: + self.logfile = None + + if __debug__: + print '%s "%s"' % (polling and 'Polling' or 'Monitoring', logdir) + print 'Start logfile "%s"' % self.logfile + + if not self.running(): + self.thread = threading.Thread(target = self.worker, name = 'netLog worker') + self.thread.daemon = True + self.thread.start() - self.thread = threading.Thread(target = self.worker, name = 'netLog worker') - self.thread.daemon = True - self.thread.start() return True def stop(self): - self.thread = None # Orphan the worker thread + if __debug__: + print 'Stopping monitoring' + self.currentdir = None + if self.observed: + self.observed = None + self.observer.unschedule_all() + self.thread = None # Orphan the worker thread - will terminate at next poll self.last_event = None def running(self): @@ -147,8 +168,8 @@ class EDLogs(FileSystemEventHandler): # Seek to the end of the latest log file logfile = self.logfile if logfile: - loghandle = open(logfile, 'rt') - loghandle.seek(0, 2) # seek to EOF + loghandle = open(logfile, 'r') + loghandle.seek(0, SEEK_END) # seek to EOF else: loghandle = None @@ -161,16 +182,30 @@ class EDLogs(FileSystemEventHandler): print "%s :\t%s %s" % ('Updated', docked and " docked" or "!docked", updated and " updated" or "!updated") # Check whether new log file started, e.g. client (re)started. - newlogfile = self.logfile + if self.observed: + newlogfile = self.logfile # updated by on_created watchdog callback + else: + # Poll + logdir = config.get('logdir') or self.logdir + try: + logfiles = sorted([x for x in listdir(logdir) if x.startswith('netLog.')]) + newlogfile = logfiles and join(logdir, logfiles[-1]) or None + except: + if __debug__: print_exc() + newlogfile = None + if logfile != newlogfile: logfile = newlogfile if loghandle: loghandle.close() - loghandle = open(logfile, 'rt') + if logfile: + loghandle = open(logfile, 'r') + if __debug__: + print 'New logfile "%s"' % logfile if logfile: system = visited = coordinates = None - loghandle.seek(0, 1) # reset EOF flag + loghandle.seek(0, SEEK_CUR) # reset EOF flag for line in loghandle: match = regexp.match(line) diff --git a/prefs.py b/prefs.py index 9900ce42..81ede168 100644 --- a/prefs.py +++ b/prefs.py @@ -132,7 +132,7 @@ class PreferencesDialog(tk.Toplevel): self.out_auto_button = nb.Checkbutton(outframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting self.out_auto_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) - self.outdir_label = nb.Label(outframe, text=_('File location')) # Section heading in settings + self.outdir_label = nb.Label(outframe, text=_('File location')+':') # Section heading in settings self.outdir_label.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) self.outdir = nb.Entry(outframe, takefocus=False) if config.get('outdir').startswith(config.home): @@ -141,13 +141,30 @@ class PreferencesDialog(tk.Toplevel): self.outdir.insert(0, config.get('outdir')) self.outdir.grid(row=20, padx=(PADX,0), sticky=tk.EW) self.outbutton = nb.Button(outframe, text=(platform=='darwin' and _('Change...') or # Folder selection button on OSX - _('Browse...')), command=self.outbrowse) # Folder selection button on Windows - self.outbutton.grid(row=20, column=1, padx=PADX) + _('Browse...')), # Folder selection button on Windows + command = lambda:self.filebrowse(_('File location'), self.outdir)) + self.outbutton.grid(row=20, column=1, padx=PADX, sticky=tk.NSEW) nb.Frame(outframe).grid(pady=5) # bottom spacer notebook.add(outframe, text=_('Output')) # Tab heading in settings + # eddnframe = nb.Frame(notebook) + # eddnframe.columnconfigure(0, weight=1) + + # HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/jamesremuscat/EDDN/wiki', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate + # ttk.Separator(eddnframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) + # self.eddn_station= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1) + # nb.Checkbutton(eddnframe, text=_('Send station data to EDDN'), variable=self.eddn_station, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Output setting + # self.eddn_auto_button = nb.Checkbutton(eddnframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting + # self.eddn_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) + # self.eddn_system= tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1) + # nb.Checkbutton(eddnframe, text=_('Send system and scan data to EDDN'), variable=self.eddn_system, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Output setting + # self.eddn_delay= tk.IntVar(value = (output & config.OUT_SYS_DELAY) and 1) + # self.eddn_delay_button = nb.Checkbutton(eddnframe, text=_('Delay sending until docked'), variable=self.out_auto, command=self.outvarchanged) # Output setting under 'Send system and scan data to EDDN' + + # notebook.add(eddnframe, text='EDDN') # Not translated + edsmframe = nb.Frame(notebook) edsmframe.columnconfigure(1, weight=1) @@ -174,40 +191,58 @@ class PreferencesDialog(tk.Toplevel): notebook.add(edsmframe, text='EDSM') # Not translated + configframe = nb.Frame(notebook) + configframe.columnconfigure(1, weight=1) + + self.logdir = nb.Entry(configframe, takefocus=False) + if platform != 'darwin': + # Apple's SMB implementation is way too flaky - no filesystem events and bogus NULLs + nb.Label(configframe, text = _('E:D log file location')+':').grid(columnspan=3, padx=PADX, sticky=tk.W) # Configuration setting + logdir = config.get('logdir') or monitor.logdir + if not logdir: + pass + elif logdir.startswith(config.home): + self.logdir.insert(0, '~' + logdir[len(config.home):]) + else: + self.logdir.insert(0, logdir) + self.logdir['state'] = 'readonly' + self.logdir.grid(row=10, columnspan=2, padx=(PADX,0), sticky=tk.EW) + self.logbutton = nb.Button(configframe, text=(platform=='darwin' and _('Change...') or # Folder selection button on OSX + _('Browse...')), # Folder selection button on Windows + command = lambda:self.filebrowse(_('E:D log file location'), self.logdir)) + self.logbutton.grid(row=10, column=2, padx=PADX, sticky=tk.EW) + nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = monitor.logdir and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting + + if platform == 'win32': + ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) if platform in ['darwin','win32']: self.hotkey_code = config.getint('hotkey_code') self.hotkey_mods = config.getint('hotkey_mods') self.hotkey_only = tk.IntVar(value = not config.getint('hotkey_always')) self.hotkey_play = tk.IntVar(value = not config.getint('hotkey_mute')) - hotkeyframe = nb.Frame(notebook) - hotkeyframe.columnconfigure(1, weight=1) - nb.Label(hotkeyframe).grid(sticky=tk.W) # big spacer + nb.Label(configframe, text = platform=='darwin' and + _('Keyboard shortcut') or # Hotkey/Shortcut settings prompt on OSX + _('Hotkey') # Hotkey/Shortcut settings prompt on Windows + ).grid(row=20, padx=PADX, sticky=tk.W) if platform == 'darwin' and not was_accessible_at_launch: if AXIsProcessTrusted(): - nb.Label(hotkeyframe, text = _('Re-start {APP} to use shortcuts').format(APP=applongname), foreground='firebrick').grid(padx=PADX, sticky=tk.NSEW) # Shortcut settings prompt on OSX + nb.Label(configframe, text = _('Re-start {APP} to use shortcuts').format(APP=applongname), foreground='firebrick').grid(padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX else: - nb.Label(hotkeyframe, text = _('{APP} needs permission to use shortcuts').format(APP=applongname), foreground='firebrick').grid(columnspan=2, padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX - nb.Button(hotkeyframe, text = _('Open System Preferences'), command = self.enableshortcuts).grid(column=1, padx=PADX, sticky=tk.E) # Shortcut settings button on OSX + nb.Label(configframe, text = _('{APP} needs permission to use shortcuts').format(APP=applongname), foreground='firebrick').grid(columnspan=3, padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX + nb.Button(configframe, text = _('Open System Preferences'), command = self.enableshortcuts).grid(column=2, padx=PADX, sticky=tk.E) # Shortcut settings button on OSX else: - self.hotkey_text = nb.Entry(hotkeyframe, width = (platform == 'darwin' and 20 or 30), justify=tk.CENTER) + self.hotkey_text = nb.Entry(configframe, width = (platform == 'darwin' and 20 or 30), justify=tk.CENTER) self.hotkey_text.insert(0, self.hotkey_code and hotkeymgr.display(self.hotkey_code, self.hotkey_mods) or _('None')) # No hotkey/shortcut currently defined self.hotkey_text.bind('', self.hotkeystart) self.hotkey_text.bind('', self.hotkeyend) - nb.Label(hotkeyframe, text = platform=='darwin' and - _('Keyboard shortcut') or # Tab heading in settings on OSX - _('Hotkey') # Tab heading in settings on Windows - ).grid(row=10, column=0, padx=PADX, sticky=tk.NSEW) - self.hotkey_text.grid(row=10, column=1, padx=PADX, sticky=tk.NSEW) - nb.Label(hotkeyframe).grid(sticky=tk.W) # big spacer - self.hotkey_only_btn = nb.Checkbutton(hotkeyframe, text=_('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting - self.hotkey_only_btn.grid(columnspan=2, padx=PADX, sticky=tk.W) - self.hotkey_play_btn = nb.Checkbutton(hotkeyframe, text=_('Play sound'), variable=self.hotkey_play, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting - self.hotkey_play_btn.grid(columnspan=2, padx=PADX, sticky=tk.W) + self.hotkey_text.grid(row=20, column=1, columnspan=2, padx=PADX, pady=(5,0), sticky=tk.W) + self.hotkey_only_btn = nb.Checkbutton(configframe, text=_('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting + self.hotkey_only_btn.grid(columnspan=3, padx=PADX, pady=(5,0), sticky=tk.W) + self.hotkey_play_btn = nb.Checkbutton(configframe, text=_('Play sound'), variable=self.hotkey_play, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting + self.hotkey_play_btn.grid(columnspan=3, padx=PADX, sticky=tk.W) - notebook.add(hotkeyframe, text = platform=='darwin' and - _('Keyboard shortcut') or # Tab heading in settings on OSX - _('Hotkey')) # Tab heading in settings on Windows + notebook.add(configframe, text=_('Configuration')) # Tab heading in settings self.languages = Translations().available_names() self.lang = tk.StringVar(value = self.languages.get(config.get('language'), _('Default'))) # Appearance theme and language setting @@ -223,7 +258,7 @@ class PreferencesDialog(tk.Toplevel): nb.Label(themeframe, text=_('Language')).grid(row=10, padx=PADX, sticky=tk.W) # Appearance setting prompt self.lang_button = nb.OptionMenu(themeframe, self.lang, self.lang.get(), *self.languages.values()) self.lang_button.grid(row=10, column=1, columnspan=2, padx=PADX, sticky=tk.W) - ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*6, sticky=tk.EW) + ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) nb.Label(themeframe, text=_('Theme')).grid(columnspan=3, padx=PADX, sticky=tk.W) # Appearance setting nb.Radiobutton(themeframe, text=_('Default'), variable=self.theme, value=0, command=self.themevarchanged).grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme and language setting nb.Radiobutton(themeframe, text=_('Dark'), variable=self.theme, value=1, command=self.themevarchanged).grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme setting @@ -235,9 +270,10 @@ class PreferencesDialog(tk.Toplevel): self.theme_label_1.grid(row=21, padx=PADX, sticky=tk.W) self.theme_button_1 = nb.ColoredButton(themeframe, text=' Hutton Orbital ', background='grey4', command=lambda:self.themecolorbrowse(1)) # Do not translate self.theme_button_1.grid(row=21, column=1, padx=PADX, pady=PADY, sticky=tk.NSEW) - ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*6, sticky=tk.EW) + ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) self.ontop_button = nb.Checkbutton(themeframe, text=_('Always on top'), variable=self.always_ontop, command=self.themevarchanged) self.ontop_button.grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance setting + nb.Label(themeframe).grid(sticky=tk.W) # big spacer notebook.add(themeframe, text=_('Appearance')) # Tab heading in settings @@ -273,14 +309,15 @@ class PreferencesDialog(tk.Toplevel): def outvarchanged(self): - self.out_auto_button['state'] = monitor.logdir and tk.NORMAL or tk.DISABLED + logdir = self.logdir.get().startswith('~') and join(config.home, self.logdir.get()[2:]) or self.logdir.get() + self.out_auto_button['state'] = logdir and isdir(logdir) and tk.NORMAL or tk.DISABLED local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship_eds.get() or self.out_ship_coriolis.get() self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED - self.edsm_log_button['state'] = monitor.logdir and tk.NORMAL or tk.DISABLED + self.edsm_log_button['state'] = self.logdir.get() and isdir(self.logdir.get()) and tk.NORMAL or tk.DISABLED edsm_state = self.edsm_log.get() and tk.NORMAL or tk.DISABLED self.edsm_label['state'] = edsm_state @@ -289,10 +326,10 @@ class PreferencesDialog(tk.Toplevel): self.edsm_cmdr['state'] = edsm_state self.edsm_apikey['state'] = edsm_state - def outbrowse(self): + def filebrowse(self, title, entryfield): if platform != 'win32': import tkFileDialog - d = tkFileDialog.askdirectory(parent=self, initialdir=expanduser(self.outdir.get()), title=_('File location'), mustexist=tk.TRUE) + d = tkFileDialog.askdirectory(parent=self, initialdir=expanduser(entryfield.get()), title=title, mustexist=tk.TRUE) else: def browsecallback(hwnd, uMsg, lParam, lpData): # set initial folder @@ -301,10 +338,10 @@ class PreferencesDialog(tk.Toplevel): return 0 browseInfo = BROWSEINFO() - browseInfo.lpszTitle = _('File location') + browseInfo.lpszTitle = title browseInfo.ulFlags = BIF_RETURNONLYFSDIRS|BIF_USENEWUI browseInfo.lpfn = BrowseCallbackProc(browsecallback) - browseInfo.lParam = self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[1:]) or self.outdir.get() + browseInfo.lParam = entryfield.get().startswith('~') and join(config.home, entryfield.get()[2:]) or entryfield.get() ctypes.windll.ole32.CoInitialize(None) pidl = ctypes.windll.shell32.SHBrowseForFolderW(ctypes.byref(browseInfo)) if pidl: @@ -316,13 +353,26 @@ class PreferencesDialog(tk.Toplevel): d = None if d: - self.outdir['state'] = tk.NORMAL # must be writable to update - self.outdir.delete(0, tk.END) + entryfield['state'] = tk.NORMAL # must be writable to update + entryfield.delete(0, tk.END) if d.startswith(config.home): - self.outdir.insert(0, '~' + d[len(config.home):]) + entryfield.insert(0, '~' + d[len(config.home):]) else: - self.outdir.insert(0, d) - self.outdir['state'] = 'readonly' + entryfield.insert(0, d) + entryfield['state'] = 'readonly' + self.outvarchanged() + + def logdir_reset(self): + self.logdir['state'] = tk.NORMAL # must be writable to update + self.logdir.delete(0, tk.END) + if not monitor.logdir: + pass + elif monitor.logdir.startswith(config.home): + self.logdir.insert(0, '~' + monitor.logdir[len(config.home):]) + else: + self.logdir.insert(0, monitor.logdir) + self.logdir['state'] = 'readonly' + self.outvarchanged() def themecolorbrowse(self, index): (rgb, color) = tkColorChooser.askcolor(self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent) @@ -403,6 +453,11 @@ class PreferencesDialog(tk.Toplevel): config.set('edsm_cmdrname', self.edsm_cmdr.get().strip()) config.set('edsm_apikey', self.edsm_apikey.get().strip()) + logdir = self.logdir.get().startswith('~') and join(config.home, self.logdir.get()[2:]) or self.logdir.get() + if monitor.logdir and logdir.lower() == monitor.logdir.lower(): + config.set('logdir', '') # default location + else: + config.set('logdir', logdir) if platform in ['darwin','win32']: config.set('hotkey_code', self.hotkey_code) config.set('hotkey_mods', self.hotkey_mods) @@ -426,8 +481,9 @@ class PreferencesDialog(tk.Toplevel): self.callback() def _destroy(self): - # Re-enable hotkey monitoring before exit + # Re-enable hotkey and log monitoring before exit hotkeymgr.register(self.parent, config.getint('hotkey_code'), config.getint('hotkey_mods')) + monitor.start(self.parent) self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.destroy()