From d485a7324337e14d3856d1318548f62823a2cb47 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sat, 31 Oct 2015 01:25:18 +0000 Subject: [PATCH 1/2] 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('<>', 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('') + 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('\n\t\n\t\n\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('= 0: + f.write(content[:start+8] + '\n\t\tVerboseLogging="1"' + content[start+8:]) + else: + start = content.lower().find("") + if start >= 0: + f.write(content[:start] + '\t\n\t\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("", 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("", lambda event:self.apply()) self.button.grid(row=1, column=3, sticky=tk.E) for child in frame.winfo_children(): From de1b790d4431022b6e9b8aedd845053c43e8f925 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sun, 1 Nov 2015 14:06:00 +0000 Subject: [PATCH 2/2] Automatically make a log entry on entering a system. --- EDMarketConnector.py | 27 ++++++++++++++++++++----- edsm.py | 7 ++++++- flightlog.py | 47 +++++++++++++++++++++++++++++++++++++------- monitor.py | 22 ++++++++++++++------- prefs.py | 7 ++++--- 5 files changed, 87 insertions(+), 23 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 83107598..011f4789 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -369,12 +369,28 @@ class AppWindow: except: pass - def system_change(self, system, timestamp): + def system_change(self, timestamp, system): 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') + try: + self.system['text'] = system + self.system['image'] = '' + self.station['text'] = EDDB.system(system) and self.STATION_UNDOCKED or '' + if config.getint('output') & config.OUT_LOG_FILE: + flightlog.writelog(timestamp, system) + if config.getint('output') & config.OUT_LOG_EDSM: + self.status['text'] = _('Sending data to EDSM...') + self.w.update_idletasks() + edsm.writelog(timestamp, system, lambda:self.edsm.lookup(system, EDDB.system(system))) # Do EDSM lookup during EDSM export + else: + self.edsm.start_lookup(system, EDDB.system(system)) + self.edsmpoll() + 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') + except Exception as e: + if __debug__: print_exc() + self.status['text'] = unicode(e) + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() + def edsmpoll(self): result = self.edsm.result @@ -412,6 +428,7 @@ class AppWindow: self.w.clipboard_append(self.station['text'] == self.STATION_UNDOCKED and self.system['text'] or '%s,%s' % (self.system['text'], self.station['text'])) def onexit(self, event=None): + flightlog.close() if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.close() diff --git a/edsm.py b/edsm.py index f10523b0..a3fab9f1 100644 --- a/edsm.py +++ b/edsm.py @@ -109,11 +109,16 @@ def export(data, edsmlookupfn): querytime = config.getint('querytime') or int(time.time()) + writelog(querytime, data['lastSystem']['name'], edsmlookupfn) + + +def writelog(timestamp, system, edsmlookupfn): + try: # Look up the system before adding it to the log, since adding it to the log has the side-effect of creating it edsmlookupfn() - r = requests.get('http://www.edsm.net/api-logs-v1/set-log?commanderName=%s&apiKey=%s&systemName=%s&dateVisited=%s' % (urllib.quote(config.get('edsm_cmdrname')), urllib.quote(config.get('edsm_apikey')), urllib.quote(data['lastSystem']['name']), urllib.quote(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(querytime)))), timeout=EDSM._TIMEOUT) + r = requests.get('http://www.edsm.net/api-logs-v1/set-log?commanderName=%s&apiKey=%s&systemName=%s&dateVisited=%s' % (urllib.quote(config.get('edsm_cmdrname')), urllib.quote(config.get('edsm_apikey')), urllib.quote(system), urllib.quote(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(timestamp)))), timeout=EDSM._TIMEOUT) r.raise_for_status() reply = r.json() (msgnum, msg) = reply['msgnum'], reply['msg'] diff --git a/flightlog.py b/flightlog.py index d5ff7292..5d3c0c02 100644 --- a/flightlog.py +++ b/flightlog.py @@ -12,6 +12,8 @@ from companion import ship_map, commodity_map logfile = None +last_timestamp = last_system = last_ship = None +last_commodities = {} def openlog(): @@ -41,24 +43,55 @@ def export(data): querytime = config.getint('querytime') or int(time.time()) - openlog() - commodities = defaultdict(int) for item in data['ship'].get('cargo',{}).get('items',[]): if item['commodity'] != 'drones': commodities[commodity_map.get(item['commodity'], item['commodity'])] += item['qty'] + writelog(querytime, + data['lastSystem']['name'], + data['commander']['docked'] and data['lastStarport']['name'], + ship_map.get(data['ship']['name'].lower(), data['ship']['name']), + commodities) + + +def writelog(timestamp, system, station=None, ship=None, commodities={}): + + global last_timestamp, last_system, last_ship, last_commodities + if last_system and last_system != system: + _writelog(last_timestamp, last_system, None, last_ship, last_commodities) + + if not station: + # If not docked, hold off writing log entry until docked or until system changes + last_timestamp, last_system, last_ship, last_commodities = timestamp, system, ship, commodities + else: + last_system = None + _writelog(timestamp, system, station, ship, commodities) + + +def _writelog(timestamp, system, station=None, ship=None, commodities={}): + + openlog() + logfile.write('%s,%s,%s,%s,%s,%s\r\n' % ( - time.strftime('%Y-%m-%d', time.localtime(querytime)), - time.strftime('%H:%M:%S', time.localtime(querytime)), - data['lastSystem']['name'], - data['commander']['docked'] and data['lastStarport']['name'] or '', - ship_map.get(data['ship']['name'].lower(), data['ship']['name']), + time.strftime('%Y-%m-%d', time.localtime(timestamp)), + time.strftime('%H:%M:%S', time.localtime(timestamp)), + system, + station or '', + ship or '', ','.join([('%d %s' % (commodities[k], k)) for k in sorted(commodities)]))) logfile.flush() +def close(): + global last_timestamp, last_system + + if last_system: + _writelog(last_timestamp, last_system, None, last_ship, last_commodities) + last_system = None + + # return log as list of (timestamp, system_name) def logs(): entries = [] diff --git a/monitor.py b/monitor.py index 14869100..bc5b265f 100644 --- a/monitor.py +++ b/monitor.py @@ -133,17 +133,21 @@ class EDLogs: return False def start(self): - self.stop() if not self.logdir or not self.callback: + self.stop() return False + if self.running(): + return True self.observer = threading.Thread(target = self.worker, name = 'netLog worker') self.observer.daemon = True self.observer.start() + return True def stop(self): - if self.observer: - self.observer.stop() - self.observer = None + self.observer = None # Orphan the worker thread + + def running(self): + return self.observer and self.observer.is_alive() def worker(self): regexp = re.compile('{(.+)} System:[^\(]*\(([^\)]+)') @@ -158,6 +162,10 @@ class EDLogs: loghandle = None while True: + # Check whether we're still supposed to be running + if threading.current_thread() != self.observer: + return # Terminate + # 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 @@ -183,9 +191,9 @@ class EDLogs: 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)) + now = localtime(time()-12*60%60) # yesterday + time_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(mktime(time_struct), system) sleep(10) # New system gets posted to log file before hyperspace ends, so don't need to poll too often diff --git a/prefs.py b/prefs.py index 7511b1fe..78621b3e 100644 --- a/prefs.py +++ b/prefs.py @@ -199,9 +199,8 @@ class PreferencesDialog(tk.Toplevel): # Selectively disable buttons depending on output settings self.outvarchanged() - # disable hotkey and log monitoring for the duration + # disable hotkey for the duration hotkeymgr.unregister() - monitor.stop() # wait for window to appear on screen before calling grab_set self.wait_visibility() @@ -224,7 +223,7 @@ class PreferencesDialog(tk.Toplevel): 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['text'] = _('Re-start Elite: Dangerous to use this feature') # Output settings prompt self.out_log_auto_text.grid(row=10, padx=(25,5), sticky=tk.EW) @@ -337,6 +336,8 @@ class PreferencesDialog(tk.Toplevel): 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() + else: + monitor.stop() self.destroy() if platform == 'darwin':