From d485a7324337e14d3856d1318548f62823a2cb47 Mon Sep 17 00:00:00 2001
From: Jonathan Harris <jonathan@marginal.org.uk>
Date: Sat, 31 Oct 2015 01:25:18 +0000
Subject: [PATCH] Poll logs for current system.

---
 EDMarketConnector.py |  14 ++
 config.py            |   1 +
 monitor.py           | 296 +++++++++++++++++++++++++++++++++++++++++++
 prefs.py             |  85 ++++++++-----
 4 files changed, 367 insertions(+), 29 deletions(-)
 create mode 100644 monitor.py

diff --git a/EDMarketConnector.py b/EDMarketConnector.py
index 4ba78592..83107598 100755
--- a/EDMarketConnector.py
+++ b/EDMarketConnector.py
@@ -30,6 +30,7 @@ import eddb
 import prefs
 from config import appname, applongname, config
 from hotkey import hotkeymgr
+from monitor import monitor
 
 l10n.Translations().install()
 EDDB = eddb.EDDB()
@@ -169,6 +170,12 @@ class AppWindow:
         self.w.bind_all('<<Invoke>>', self.getandsend)	# user-generated
         hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods'))
 
+        # Install log monitoring
+        monitor.set_callback(self.system_change)
+        if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_AUTO|config.OUT_LOG_EDSM)):
+            monitor.enable_logging()
+            monitor.start()
+
     # call after credentials have changed
     def login(self):
         self.status['text'] = _('Logging in...')
@@ -362,6 +369,13 @@ class AppWindow:
         except:
             pass
 
+    def system_change(self, system, timestamp):
+        if self.system['text'] != system:
+            # TODO: EDSM lookup and csv and/or EDSM log
+            self.system['text'] = system
+            self.station['text'] = EDDB.system(system) and self.STATION_UNDOCKED or ''
+            self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8')
+
     def edsmpoll(self):
         result = self.edsm.result
         if result['done']:
diff --git a/config.py b/config.py
index d2d5e058..530ab3d2 100644
--- a/config.py
+++ b/config.py
@@ -74,6 +74,7 @@ class Config:
     #OUT_STAT = 64	# No longer available
     OUT_SHIP_CORIOLIS = 128
     OUT_LOG_EDSM = 256
+    OUT_LOG_AUTO = 512
 
     if platform=='darwin':
 
diff --git a/monitor.py b/monitor.py
new file mode 100644
index 00000000..14869100
--- /dev/null
+++ b/monitor.py
@@ -0,0 +1,296 @@
+import re
+import threading
+from os import listdir, pardir, rename, unlink
+from os.path import exists, isdir, isfile, join
+from platform import machine
+from sys import platform
+from time import strptime, localtime, mktime, sleep, time
+from datetime import datetime
+
+if __debug__:
+    from traceback import print_exc
+
+if platform=='darwin':
+    from AppKit import NSWorkspace
+    from Foundation import NSSearchPathForDirectoriesInDomains, NSApplicationSupportDirectory, NSUserDomainMask
+ 
+elif platform=='win32':
+    import ctypes
+
+    CSIDL_LOCAL_APPDATA     = 0x001C
+    CSIDL_PROGRAM_FILESX86  = 0x002A
+
+    # _winreg that ships with Python 2 doesn't support unicode, so do this instead
+    from ctypes.wintypes import *
+
+    HKEY_LOCAL_MACHINE      = 0x80000002
+    KEY_READ                = 0x00020019
+    REG_SZ    = 1
+
+    RegOpenKeyEx = ctypes.windll.advapi32.RegOpenKeyExW
+    RegOpenKeyEx.restype = LONG
+    RegOpenKeyEx.argtypes = [HKEY, LPCWSTR, DWORD, DWORD, ctypes.POINTER(HKEY)]
+
+    RegCloseKey = ctypes.windll.advapi32.RegCloseKey
+    RegCloseKey.restype = LONG
+    RegCloseKey.argtypes = [HKEY]
+
+    RegQueryValueEx = ctypes.windll.advapi32.RegQueryValueExW
+    RegQueryValueEx.restype = LONG
+    RegQueryValueEx.argtypes = [HKEY, LPCWSTR, LPCVOID, ctypes.POINTER(DWORD), LPCVOID, ctypes.POINTER(DWORD)]
+
+    RegEnumKeyEx = ctypes.windll.advapi32.RegEnumKeyExW
+    RegEnumKeyEx.restype = LONG
+    RegEnumKeyEx.argtypes = [HKEY, DWORD, LPWSTR, ctypes.POINTER(DWORD), ctypes.POINTER(DWORD), LPWSTR, ctypes.POINTER(DWORD), ctypes.POINTER(FILETIME)]
+
+    WNDENUMPROC = ctypes.WINFUNCTYPE(BOOL, HWND, ctypes.POINTER(DWORD))
+    EnumWindows = ctypes.windll.user32.EnumWindows
+    EnumWindows.argtypes = [WNDENUMPROC, LPARAM]
+    GetWindowText = ctypes.windll.user32.GetWindowTextW
+    GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
+    GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
+
+    @WNDENUMPROC
+    def EnumWindowsProc(hwnd, lParam):
+        l = GetWindowTextLength(hwnd) + 1
+        buf = ctypes.create_unicode_buffer(l)
+        if GetWindowText(hwnd, buf, l) and buf.value.startswith('Elite - Dangerous'):
+            lParam[0] = 1
+            return False	# stop enumeration
+        return True
+
+
+class EDLogs:
+
+    def __init__(self):
+        self.logdir = self._logdir()
+        self.logging_enabled = self._logging_enabled
+        self._restart_required = False
+        self.observer = None
+        self.callback = None
+
+    def set_callback(self, callback):
+        self.callback = callback
+
+    def enable_logging(self):
+        if self.logging_enabled():
+            return True
+        elif self._enable_logging():
+            self._restart_required = self._ED_is_running()
+            return True
+        else:
+            return False
+
+    def restart_required(self):
+        if not self._ED_is_running():
+            self._restart_required = False
+        return self._restart_required
+
+    def logging_enabled_in_file(self, appconf):
+        if not isfile(appconf):
+            return False
+
+        with open(appconf, 'rU') as f:
+            content = f.read().lower()
+            start = content.find('<network')
+            end = content.find('</network>')
+            if start >= 0 and end >= 0:
+                return bool(re.search('verboselogging\s*=\s*\"1\"', content[start+8:end]))
+            else:
+                return False
+
+    def enable_logging_in_file(self, appconf):
+        try:
+            if not exists(appconf):
+                with open(appconf, 'wt') as f:
+                    f.write('<AppConfig>\n\t<Network\n\t\tVerboseLogging="1"\n\t>\n\t</Network>\n</AppConfig>\n')
+                return True
+
+            with open(appconf, 'rU') as f:
+                content = f.read()
+                f.close()
+            backup = appconf[:-4] + '_backup.xml'
+            if exists(backup):
+                unlink(backup)
+            rename(appconf, backup)
+
+            with open(appconf, 'wt') as f:
+                start = content.lower().find('<network')
+                if start >= 0:
+                    f.write(content[:start+8] + '\n\t\tVerboseLogging="1"' + content[start+8:])
+                else:
+                    start = content.lower().find("</appconfig>")
+                    if start >= 0:
+                        f.write(content[:start] + '\t<Network\n\t\tVerboseLogging="1"\n\t>\n\t</Network>\n' + content[start:])
+                    else:
+                        f.write(content)	# eh ?
+                        return False
+
+            assert self._logging_enabled()
+            return self.logging_enabled_in_file(appconf)
+        except:
+            if __debug__: print_exc()
+            return False
+
+    def start(self):
+        self.stop()
+        if not self.logdir or not self.callback:
+            return False
+        self.observer = threading.Thread(target = self.worker, name = 'netLog worker')
+        self.observer.daemon = True
+        self.observer.start()
+
+    def stop(self):
+        if self.observer:
+            self.observer.stop()
+        self.observer = None
+
+    def worker(self):
+        regexp = re.compile('{(.+)} System:[^\(]*\(([^\)]+)')
+
+        # Seek to the end of the latest log file
+        logfiles = sorted([x for x in listdir(self.logdir) if x.startswith('netLog.')])
+        logfile = logfiles and logfiles[-1] or None
+        if logfile:
+            loghandle = open(join(self.logdir, logfile), 'rt')
+            loghandle.seek(0, 2)	# seek to EOF
+        else:
+            loghandle = None
+
+        while True:
+            # Check whether new log file started, e.g. client restarted. Assumes logs sort alphabetically.
+            logfiles = sorted([x for x in listdir(self.logdir) if x.startswith('netLog.')])
+            newlogfile = logfiles and logfiles[-1] or None
+            if logfile != newlogfile:
+                logfile = newlogfile
+                if loghandle:
+                    loghandle.close()
+                loghandle = open(join(self.logdir, logfile), 'rt')
+
+            if logfile:
+                system = visited = None
+                loghandle.seek(0, 1)	# reset EOF flag
+
+                for line in loghandle:
+                    match = regexp.match(line)
+                    if match:
+                        system, visited = match.group(2), match.group(1)
+
+                if system:
+                    self._restart_required = False	# clearly logging is working
+                    # Convert local time string to UTC date and time
+                    visited_struct = strptime(visited, '%H:%M:%S')
+                    now = localtime()
+                    if now.tm_hour == 0 and visited_struct.tm_hour == 23:
+                        # Crossed midnight between timestamp and poll
+                        now = localtime(time()-60)
+                    datetime_struct = datetime(now.tm_year, now.tm_mon, now.tm_mday, visited_struct.tm_hour, visited_struct.tm_min, visited_struct.tm_sec).timetuple()	# still local time
+                    self.callback(system, mktime(datetime_struct))
+
+            sleep(10)	# New system gets posted to log file before hyperspace ends, so don't need to poll too often
+
+
+    if platform=='darwin':
+
+        def _logdir(self):
+            # https://support.frontier.co.uk/kb/faq.php?id=97
+            suffix = join('Frontier Developments', 'Elite Dangerous')
+            paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)
+            if len(paths) and isdir(paths[0]) and isfile(join(paths[0], suffix, 'AppNetCfg.xml')) and isdir(join(paths[0], suffix, 'Logs')):
+                return join(paths[0], suffix, 'Logs')
+            else:
+                return None
+
+        def _logging_enabled(self):
+            return self.logdir and self.logging_enabled_in_file(join(self.logdir, pardir, 'AppConfigLocal.xml'))
+
+        def _enable_logging(self):
+            return self.logdir and self.enable_logging_in_file(join(self.logdir, pardir, 'AppConfigLocal.xml'))
+
+        def _ED_is_running(self):
+            for x in NSWorkspace.sharedWorkspace().runningApplications():
+                if x.bundleIdentifier() == 'uk.co.frontier.EliteDangerous':
+                    return True
+            else:
+                return False
+
+    elif platform=='win32':
+
+        def _logdir(self):
+            # First try under the Launcher
+            key = HKEY()
+            if not RegOpenKeyEx(HKEY_LOCAL_MACHINE,
+                                machine().endswith('64') and
+                                r'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' or	# Assumes that the launcher is a 32bit process
+                                r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
+                                0, KEY_READ, ctypes.byref(key)):
+                buf = ctypes.create_unicode_buffer(MAX_PATH)
+                i = 0
+                while True:
+                    size = DWORD(MAX_PATH)
+                    if RegEnumKeyEx(key, i, buf, ctypes.byref(size), None, None, None, None):
+                        break
+
+                    subkey = HKEY()
+                    if not RegOpenKeyEx(key, buf, 0, KEY_READ, ctypes.byref(subkey)):
+                        valtype = DWORD()
+                        valsize = DWORD((len('Frontier Developments')+1)*2)
+                        valbuf = ctypes.create_unicode_buffer(valsize.value / 2)
+                        if not RegQueryValueEx(subkey, 'Publisher', 0, ctypes.byref(valtype), valbuf, ctypes.byref(valsize)) and valtype.value == REG_SZ and valbuf.value == 'Frontier Developments':
+                            if not RegQueryValueEx(subkey, 'InstallLocation', 0, ctypes.byref(valtype), None, ctypes.byref(valsize)) and valtype.value == REG_SZ:
+                                valbuf = ctypes.create_unicode_buffer(valsize.value / 2)
+                                if not RegQueryValueEx(subkey, 'InstallLocation', 0, ctypes.byref(valtype), valbuf, ctypes.byref(valsize)):
+                                    custpath = join(valbuf.value, 'Products')
+                                    if isdir(custpath):
+                                        for d in listdir(custpath):
+                                            if d.startswith('FORC-FDEV-D-1') and isfile(join(custpath, d, 'AppConfig.xml')) and isdir(join(custpath, d, 'Logs')):
+                                                RegCloseKey(subkey)
+                                                RegCloseKey(key)
+                                                return join(custpath, d, 'Logs')
+                        RegCloseKey(subkey)
+                    i += 1
+                RegCloseKey(key)
+
+            # https://support.elitedangerous.com/kb/faq.php?id=108
+            programs = ctypes.create_unicode_buffer(MAX_PATH)
+            ctypes.windll.shell32.SHGetSpecialFolderPathW(0, programs, CSIDL_PROGRAM_FILESX86, 0)
+            applocal = ctypes.create_unicode_buffer(MAX_PATH)
+            ctypes.windll.shell32.SHGetSpecialFolderPathW(0, applocal, CSIDL_LOCAL_APPDATA, 0)
+            for base in [join(programs.value, 'Steam', 'steamapps', 'common', 'Elite Dangerous', 'Products'),
+                         join(programs.value, 'Frontier', 'Products'),
+                         join(applocal.value, 'Frontier_Developments', 'Products')]:
+                if isdir(base):
+                    for d in listdir(base):
+                        if d.startswith('FORC-FDEV-D-1') and isfile(join(base, d, 'AppConfig.xml')) and isdir(join(base, d, 'Logs')):
+                            return join(base, d, 'Logs')
+
+            return None
+
+        def _logging_enabled(self):
+            return self.logdir and (self.logging_enabled_in_file(join(self.logdir, pardir, 'AppConfigLocal.xml')) or
+                                    self.logging_enabled_in_file(join(self.logdir, pardir, 'AppConfig.xml')))
+
+        def _enable_logging(self):
+            return self.logdir and self.enable_logging_in_file(isfile(join(self.logdir, pardir, 'AppConfigLocal.xml')) and join(self.logdir, pardir, 'AppConfigLocal.xml') or join(self.logdir, pardir, 'AppConfig.xml'))
+
+        def _ED_is_running(self):
+            retval = DWORD(0)
+            EnumWindows(EnumWindowsProc, ctypes.addressof(retval))
+            return bool(retval)
+
+    elif platform=='linux2':
+
+        def _logdir(self):
+            return None
+
+        def _logging_enabled(self):
+            return False
+
+        def _enable_logging(self):
+            return False
+
+        def _ED_is_running(self):
+            return False
+
+# singleton
+monitor = EDLogs()
diff --git a/prefs.py b/prefs.py
index 23f24e37..7511b1fe 100644
--- a/prefs.py
+++ b/prefs.py
@@ -11,6 +11,7 @@ from ttkHyperlinkLabel import HyperlinkLabel
 
 from config import applongname, config
 from hotkey import hotkeymgr
+from monitor import monitor
 
 
 if platform == 'darwin':
@@ -71,7 +72,7 @@ class PreferencesDialog(tk.Toplevel):
         frame.grid(sticky=tk.NSEW)
 
         credframe = ttk.LabelFrame(frame, text=_('Credentials'))	# Section heading in settings
-        credframe.grid(padx=10, pady=10, sticky=tk.NSEW)
+        credframe.grid(padx=10, pady=5, sticky=tk.NSEW)
         credframe.columnconfigure(1, weight=1)
 
         ttk.Label(credframe, text=_('Please log in with your Elite: Dangerous account details')).grid(row=0, columnspan=2, sticky=tk.W)	# Use same text as E:D Launcher's login dialog
@@ -90,30 +91,35 @@ class PreferencesDialog(tk.Toplevel):
             child.grid_configure(padx=5, pady=3)
 
         outframe = ttk.LabelFrame(frame, text=_('Output'))		# Section heading in settings
-        outframe.grid(padx=10, pady=10, sticky=tk.NSEW)
+        outframe.grid(padx=10, pady=5, sticky=tk.NSEW)
 
         output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP_EDS)
-        ttk.Label(outframe, text=_('Please choose what data to save')).grid(row=0, columnspan=2, padx=5, pady=3, sticky=tk.W)
-        self.out_eddn= tk.IntVar(value = (output & config.OUT_EDDN) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.out_eddn, command=self.outvarchanged).grid(row=1, columnspan=2, padx=5, sticky=tk.W)
-        self.out_csv = tk.IntVar(value = (output & config.OUT_CSV ) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(row=2, columnspan=2, padx=5, sticky=tk.W)
-        self.out_bpc = tk.IntVar(value = (output & config.OUT_BPC ) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_("Market data in Slopey's BPC format file"), variable=self.out_bpc, command=self.outvarchanged).grid(row=3, columnspan=2, padx=5, sticky=tk.W)
-        self.out_td  = tk.IntVar(value = (output & config.OUT_TD  ) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(row=4, columnspan=2, padx=5, sticky=tk.W)
-        self.out_ship_eds= tk.IntVar(value = (output & config.OUT_SHIP_EDS) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_('Ship loadout in E:D Shipyard format file'), variable=self.out_ship_eds, command=self.outvarchanged).grid(row=5, columnspan=2, padx=5, pady=(5,0), sticky=tk.W)
-        self.out_ship_coriolis= tk.IntVar(value = (output & config.OUT_SHIP_CORIOLIS) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_('Ship loadout in Coriolis format file'), variable=self.out_ship_coriolis, command=self.outvarchanged).grid(row=6, columnspan=2, padx=5, sticky=tk.W)
-        self.out_log_edsm = tk.IntVar(value = (output & config.OUT_LOG_EDSM ) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.out_log_edsm, command=self.outvarchanged).grid(row=7, columnspan=2, padx=5, pady=(5,0), sticky=tk.W)
-        self.out_log_file = tk.IntVar(value = (output & config.OUT_LOG_FILE ) and 1 or 0)
-        ttk.Checkbutton(outframe, text=_('Flight log in CSV format file'), variable=self.out_log_file, command=self.outvarchanged).grid(row=8, columnspan=2, padx=5, sticky=tk.W)
+        ttk.Label(outframe, text=_('Please choose what data to save')).grid(row=0, padx=5, pady=3, sticky=tk.W)
+        self.out_eddn= tk.IntVar(value = (output & config.OUT_EDDN) and 1)
+        ttk.Checkbutton(outframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.out_eddn, command=self.outvarchanged).grid(row=1, padx=5, sticky=tk.W)
+        self.out_csv = tk.IntVar(value = (output & config.OUT_CSV ) and 1)
+        ttk.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(row=2, padx=5, sticky=tk.W)
+        self.out_bpc = tk.IntVar(value = (output & config.OUT_BPC ) and 1)
+        ttk.Checkbutton(outframe, text=_("Market data in Slopey's BPC format file"), variable=self.out_bpc, command=self.outvarchanged).grid(row=3, padx=5, sticky=tk.W)
+        self.out_td  = tk.IntVar(value = (output & config.OUT_TD  ) and 1)
+        ttk.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(row=4, padx=5, sticky=tk.W)
+        self.out_ship_eds= tk.IntVar(value = (output & config.OUT_SHIP_EDS) and 1)
+        ttk.Checkbutton(outframe, text=_('Ship loadout in E:D Shipyard format file'), variable=self.out_ship_eds, command=self.outvarchanged).grid(row=5, padx=5, pady=(5,0), sticky=tk.W)
+        self.out_ship_coriolis= tk.IntVar(value = (output & config.OUT_SHIP_CORIOLIS) and 1)
+        ttk.Checkbutton(outframe, text=_('Ship loadout in Coriolis format file'), variable=self.out_ship_coriolis, command=self.outvarchanged).grid(row=6, padx=5, sticky=tk.W)
+        self.out_log_edsm = tk.IntVar(value = (output & config.OUT_LOG_EDSM) and 1)
+        ttk.Checkbutton(outframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.out_log_edsm, command=self.outvarchanged).grid(row=7, padx=5, pady=(5,0), sticky=tk.W)
+        self.out_log_file = tk.IntVar(value = (output & config.OUT_LOG_FILE) and 1)
+        ttk.Checkbutton(outframe, text=_('Flight log in CSV format file'), variable=self.out_log_file, command=self.outvarchanged).grid(row=8, padx=5, sticky=tk.W)
+        self.out_log_auto = tk.IntVar(value = monitor.logdir and (output & config.OUT_LOG_AUTO) and 1 or 0)
+        if monitor.logdir:
+            self.out_log_auto_button = ttk.Checkbutton(outframe, text=_('Automatically make a log entry on entering a system'), variable=self.out_log_auto, command=self.outvarchanged)	# Output setting
+            self.out_log_auto_button.grid(row=9, padx=5, sticky=tk.W)
+            self.out_log_auto_text = ttk.Label(outframe)
 
         self.dir_label = ttk.Label(frame, text=_('File location'), foreground=style.lookup('TLabelframe.Label', 'foreground'))	# Section heading in settings
         dirframe = ttk.LabelFrame(frame, labelwidget = self.dir_label)
-        dirframe.grid(padx=10, pady=10, sticky=tk.NSEW)
+        dirframe.grid(padx=10, pady=5, sticky=tk.NSEW)
         dirframe.columnconfigure(0, weight=1)
 
         self.outdir = ttk.Entry(dirframe, takefocus=False)
@@ -128,7 +134,7 @@ class PreferencesDialog(tk.Toplevel):
 
         self.edsm_label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), disabledforeground=style.lookup('TLabelframe.Label', 'foreground'), url='http://www.edsm.net/settings/api', underline=True)	# Section heading in settings
         edsmframe = ttk.LabelFrame(frame, labelwidget = self.edsm_label)
-        edsmframe.grid(padx=10, pady=10, sticky=tk.NSEW)
+        edsmframe.grid(padx=10, pady=5, sticky=tk.NSEW)
         edsmframe.columnconfigure(1, weight=1)
 
         ttk.Label(edsmframe, text=_('Cmdr name')).grid(row=0, sticky=tk.W)	# EDSM & privacy setting
@@ -151,7 +157,7 @@ class PreferencesDialog(tk.Toplevel):
             self.hotkey_play = tk.IntVar(value = not config.getint('hotkey_mute'))
             hotkeyframe = ttk.LabelFrame(frame, text=platform == 'darwin' and _('Keyboard shortcut') or	# Section heading in settings on OSX
                                          _('Hotkey'))	# Section heading in settings on Windows
-            hotkeyframe.grid(padx=10, pady=10, sticky=tk.NSEW)
+            hotkeyframe.grid(padx=10, pady=5, sticky=tk.NSEW)
             hotkeyframe.columnconfigure(1, weight=1)
             if platform == 'darwin' and not was_accessible_at_launch:
                 if AXIsProcessTrusted():
@@ -171,10 +177,10 @@ class PreferencesDialog(tk.Toplevel):
                 self.hotkey_play_btn.grid(row=2, columnspan=2, padx=5, sticky=tk.W)
 
         privacyframe = ttk.LabelFrame(frame, text=_('Privacy'))	# Section heading in settings
-        privacyframe.grid(padx=10, pady=10, sticky=tk.NSEW)
+        privacyframe.grid(padx=10, pady=5, sticky=tk.NSEW)
 
         self.out_anon= tk.IntVar(value = config.getint('anonymous') and 1)
-        ttk.Label(privacyframe, text=_('How do you want to be identified in the saved data')).grid(row=0, columnspan=2, padx=5, sticky=tk.W)
+        ttk.Label(privacyframe, text=_('How do you want to be identified in the saved data')).grid(row=0, padx=5, sticky=tk.W)
         ttk.Radiobutton(privacyframe, text=_('Cmdr name'), variable=self.out_anon, value=0).grid(padx=5, sticky=tk.W)	# Privacy setting
         ttk.Radiobutton(privacyframe, text=_('Pseudo-anonymized ID'), variable=self.out_anon, value=1).grid(padx=5, sticky=tk.W)	# Privacy setting
 
@@ -182,17 +188,20 @@ class PreferencesDialog(tk.Toplevel):
             self.protocol("WM_DELETE_WINDOW", self.apply)	# close button applies changes
         else:
             buttonframe = ttk.Frame(frame)
-            buttonframe.grid(padx=10, pady=10, sticky=tk.NSEW)
+            buttonframe.grid(padx=10, pady=5, sticky=tk.NSEW)
             buttonframe.columnconfigure(0, weight=1)
             ttk.Label(buttonframe).grid(row=0, column=0)	# spacer
-            ttk.Button(buttonframe, text=_('OK'), command=self.apply).grid(row=0, column=1, sticky=tk.E)
+            button = ttk.Button(buttonframe, text=_('OK'), command=self.apply)
+            button.grid(row=0, column=1, sticky=tk.E)
+            button.bind("<Return>", lambda event:self.apply())
             self.protocol("WM_DELETE_WINDOW", self._destroy)
 
         # Selectively disable buttons depending on output settings
         self.outvarchanged()
 
-        # disable hotkey for the duration
+        # disable hotkey and log monitoring for the duration
         hotkeymgr.unregister()
+        monitor.stop()
 
         # wait for window to appear on screen before calling grab_set
         self.wait_visibility()
@@ -205,6 +214,20 @@ class PreferencesDialog(tk.Toplevel):
         self.outbutton['state'] = local and tk.NORMAL  or tk.DISABLED
         self.outdir['state']    = local and 'readonly' or tk.DISABLED
 
+        if monitor.logdir:
+            log = self.out_log_edsm.get() or self.out_log_file.get()
+            self.out_log_auto_button['state']  = log and tk.NORMAL or tk.DISABLED
+
+            self.out_log_auto_text['text'] = ''
+            if log and self.out_log_auto.get():
+                if not monitor.enable_logging():
+                    self.out_log_auto_text['text'] = "Can't enable automatic logging!"	# Shouldn't happen - don't translate
+                    self.out_log_auto_text.grid(row=10, padx=(25,5), sticky=tk.EW)
+                elif monitor.restart_required():
+                    self.out_log_auto_text['text'] = _('Re-start Elite: Dangerous for automatic logging')	# Output settings prompt
+                    self.out_log_auto_text.grid(row=10, padx=(25,5), sticky=tk.EW)
+
+
         edsm = self.out_log_edsm.get()
         self.edsm_label['state']  = edsm and tk.NORMAL or tk.DISABLED
         self.edsm_cmdr['state']   = edsm and tk.NORMAL or tk.DISABLED
@@ -290,7 +313,7 @@ class PreferencesDialog(tk.Toplevel):
         config.set('username', self.username.get().strip())
         config.set('password', self.password.get().strip())
 
-        config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS or 0) + (self.out_log_file.get() and config.OUT_LOG_FILE or 0) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS or 0) + (self.out_log_edsm.get() and config.OUT_LOG_EDSM or 0))
+        config.set('output', (self.out_eddn.get() and config.OUT_EDDN) + (self.out_bpc.get() and config.OUT_BPC) + (self.out_td.get() and config.OUT_TD) + (self.out_csv.get() and config.OUT_CSV) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS) + (self.out_log_file.get() and config.OUT_LOG_FILE) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + (self.out_log_edsm.get() and config.OUT_LOG_EDSM) + (self.out_log_auto.get() and config.OUT_LOG_AUTO))
         config.set('outdir', expanduser(self.outdir.get()))
 
         config.set('edsm_cmdrname', self.edsm_cmdr.get().strip())
@@ -309,8 +332,11 @@ 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'))
+        if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_AUTO|config.OUT_LOG_EDSM)):
+            monitor.enable_logging()
+            monitor.start()
         self.destroy()
 
     if platform == 'darwin':
@@ -371,6 +397,7 @@ class AuthenticationDialog(tk.Toplevel):
         self.code.focus_set()
         ttk.Label(frame).grid(row=1, column=2)	# spacer
         self.button = ttk.Button(frame, text=_('OK'), command=self.apply, state=tk.DISABLED)
+        self.button.bind("<Return>", lambda event:self.apply())
         self.button.grid(row=1, column=3, sticky=tk.E)
 
         for child in frame.winfo_children():