diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 1dfa1a53..9b1ad36c 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -40,6 +40,7 @@ import eddb import stats import prefs import plug +from edproxy import edproxy from hotkey import hotkeymgr from monitor import monitor @@ -195,10 +196,12 @@ class AppWindow: hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring - self.w.bind_all('<<Jump>>', self.system_change) # user-generated - if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_AUTO|config.OUT_LOG_EDSM)): + monitor.set_callback(self.system_change) + edproxy.set_callback(self.system_change) + if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_FILE|config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.w) + edproxy.start(self.w) # First run if not config.get('username') or not config.get('password'): @@ -437,13 +440,7 @@ class AppWindow: except: pass - def system_change(self, event): - - if not monitor.last_event: - if __debug__: print 'spurious system_change', event # eh? - return - - timestamp, system = monitor.last_event # would like to use event user_data to carry this, but not accessible in Tkinter + def system_change(self, timestamp, system): if self.system['text'] != system: self.system['text'] = system diff --git a/edproxy.py b/edproxy.py new file mode 100644 index 00000000..3e3b2531 --- /dev/null +++ b/edproxy.py @@ -0,0 +1,150 @@ +# +# Elite: Dangerous Netlog Proxy Server client - https://bitbucket.org/westokyo/edproxy/ +# + +import json +import socket +import struct +import sys +import threading +from time import time, strptime, mktime +from datetime import datetime + +if __debug__: + from traceback import print_exc + + +class _EDProxy: + + DISCOVERY_ADDR = '239.45.99.98' + DISCOVERY_PORT = 45551 + DISCOVERY_QUERY = 'Query' + DISCOVERY_ANNOUNCE = 'Announce' + + SERVICE_NAME = 'edproxy' + SERVICE_HEARTBEAT = 60 # [s] + SERVICE_TIMEOUT = 90 # [s] + + MESSAGE_MAX = 1024 # https://bitbucket.org/westokyo/edproxy/src/master/ednet.py?fileviewer=file-view-default#ednet.py-166 + MESSAGE_SYSTEM = 'System' + + + def __init__(self): + self.lock = threading.Lock() + self.addr = None + self.port = None + + thread = threading.Thread(target = self._listener) + thread.daemon = True + thread.start() + + self.callback = None + self.last_event = None # for communicating the Jump event + + # start + self.discover_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.discover() + + def set_callback(self, callback): + self.callback = callback + + def start(self, root): + self.root = root + self.root.bind_all('<<ProxyJump>>', self.jump) # user-generated + + def stop(self): + # Still listening, but stop callbacks + self.root.unbind_all('<<ProxyJump>>') + + def status(self): + self.lock.acquire() + if self.addr and self.port: + status = '%s:%d' % (self.addr, self.port) + self.lock.release() + return status + else: + self.lock.release() + self.discover() # Kick off discovery + return None + + def jump(self, event): + # Called from Tkinter's main loop + if self.callback and self.last_event: + self.callback(*self.last_event) + + def close(): + self.discover_sock.shutdown() + self.discover_sock = None + + # Send a query. _listener should get the response. + def discover(self): + self.discover_sock.sendto(json.dumps({ + 'type': self.DISCOVERY_QUERY, + 'name': self.SERVICE_NAME, + }), (self.DISCOVERY_ADDR, self.DISCOVERY_PORT)) + + def _listener(self): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if sys.platform == 'win32': + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + else: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + s.bind(('', self.DISCOVERY_PORT)) + s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, struct.pack('4sL', socket.inet_aton(self.DISCOVERY_ADDR), socket.INADDR_ANY)) + while True: + try: + (data, addr) = s.recvfrom(self.MESSAGE_MAX) + msg = json.loads(data) + if msg['name'] == self.SERVICE_NAME and msg['type'] == self.DISCOVERY_ANNOUNCE: + # ignore if already connected to a proxy + if not self.addr or not self.port: + thread = threading.Thread(target = self._worker, args = (msg['ipv4'], int(msg['port']))) + thread.daemon = True + thread.start() + except: + if __debug__: print_exc() + + def _worker(self, addr, port): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((addr, port)) + s.settimeout(0) + s.sendall(json.dumps({ + 'Type' : 'Init', + 'DateUtc' : datetime.now().isoformat(), + 'StartTime' : 'now', + 'Register' : [ 'System' ], + })) + except: + if __debug__: print_exc() + return + + self.lock.acquire() + self.addr = addr + self.port = port + self.lock.release() + + try: + s.settimeout(None) # was self.SERVICE_TIMEOUT, but heartbeat doesn't appear to work so wait indefinitely + while True: + msg = json.loads(s.recv(self.MESSAGE_MAX)) + if msg['Type'] == self.MESSAGE_SYSTEM: + if 'DateUtc' in msg: + timestamp = mktime(datetime.strptime(msg['DateUtc'], '%Y-%m-%d %H:%M:%S').utctimetuple()) + else: + timestamp = mktime(strptime(msg['Date'], '%Y-%m-%d %H:%M:%S')) # from local time + self.last_event = (timestamp, msg['System']) + self.root.event_generate('<<ProxyJump>>', when="tail") + except: + if __debug__: print_exc() + + self.lock.acquire() + self.addr = self.port = None + self.lock.release() + + # Kick off discovery for another proxy + self.discover() + + +# singleton +edproxy = _EDProxy() diff --git a/monitor.py b/monitor.py index f70e4002..90bb5b2f 100644 --- a/monitor.py +++ b/monitor.py @@ -81,6 +81,7 @@ class EDLogs(FileSystemEventHandler): self._restart_required = False self.observer = None self.thread = None + self.callback = None self.last_event = None # for communicating the Jump event def enable_logging(self): @@ -143,6 +144,9 @@ class EDLogs(FileSystemEventHandler): if __debug__: print_exc() return False + def set_callback(self, callback): + self.callback = callback + def start(self, root): self.root = root if not self.logdir: @@ -151,6 +155,8 @@ class EDLogs(FileSystemEventHandler): if self.running(): return True + self.root.bind_all('<<MonitorJump>>', self.jump) # 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__: @@ -239,7 +245,7 @@ class EDLogs(FileSystemEventHandler): 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 # Tk on Windows doesn't like to be called outside of an event handler, so generate an event self.last_event = (mktime(time_struct), system) - self.root.event_generate('<<Jump>>', when="tail") + self.root.event_generate('<<MonitorJump>>', when="tail") sleep(10) # New system gets posted to log file before hyperspace ends, so don't need to poll too often @@ -247,6 +253,10 @@ class EDLogs(FileSystemEventHandler): if threading.current_thread() != self.thread: return # Terminate + def jump(self, event): + # Called from Tkinter's main loop + if self.callback and self.last_event: + self.callback(*self.last_event) if platform=='darwin': diff --git a/prefs.py b/prefs.py index d7514bd2..a7405472 100644 --- a/prefs.py +++ b/prefs.py @@ -11,6 +11,7 @@ from ttkHyperlinkLabel import HyperlinkLabel import myNotebook as nb from config import applongname, config +from edproxy import edproxy from hotkey import hotkeymgr from monitor import monitor @@ -127,10 +128,9 @@ class PreferencesDialog(tk.Toplevel): nb.Checkbutton(outframe, text=_('Ship loadout in Coriolis format file'), variable=self.out_ship_coriolis, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_log_file = tk.IntVar(value = (output & config.OUT_LOG_FILE) and 1) nb.Checkbutton(outframe, text=_('Flight log in CSV format file'), variable=self.out_log_file, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), 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 = nb.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(columnspan=2, padx=BUTTONX, sticky=tk.W) + self.out_log_auto = tk.IntVar(value = output & config.OUT_LOG_AUTO and 1 or 0) + self.out_log_auto_button = nb.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(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_log_auto_text = nb.Label(outframe, foreground='firebrick') self.out_log_auto_text.grid(columnspan=2, padx=(30,0), sticky=tk.W) @@ -160,9 +160,8 @@ class PreferencesDialog(tk.Toplevel): self.edsm_autoopen = tk.BooleanVar(value = config.getint('edsm_autoopen')) self.edsm_autoopen_button = nb.Checkbutton(edsmframe, text=_(u"Automatically open uncharted systems’ EDSM pages"), variable=self.edsm_autoopen) self.edsm_autoopen_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) - if monitor.logdir: - self.edsm_log_auto_button = nb.Checkbutton(edsmframe, text=_('Automatically make a log entry on entering a system'), variable=self.out_log_auto, command=self.outvarchanged) # Output setting - self.edsm_log_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) + self.edsm_log_auto_button = nb.Checkbutton(edsmframe, text=_('Automatically make a log entry on entering a system'), variable=self.out_log_auto, command=self.outvarchanged) # Output setting + self.edsm_log_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.edsm_log_auto_text = nb.Label(edsmframe, foreground='firebrick') self.edsm_log_auto_text.grid(columnspan=2, padx=(30,0), sticky=tk.W) @@ -237,7 +236,7 @@ class PreferencesDialog(tk.Toplevel): self.protocol("WM_DELETE_WINDOW", self._destroy) # Selectively disable buttons depending on output settings - self.outvarchanged() + self.proxypoll() # disable hotkey for the duration hotkeymgr.unregister() @@ -247,6 +246,12 @@ class PreferencesDialog(tk.Toplevel): self.grab_set() #self.wait_window(self) # causes duplicate events on OSX + + def proxypoll(self): + self.outvarchanged() + self.after(1000, self.proxypoll) + + def outvarchanged(self): 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() or self.out_log_file.get() self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED @@ -261,26 +266,33 @@ class PreferencesDialog(tk.Toplevel): self.edsm_cmdr['state'] = edsm_state self.edsm_apikey['state'] = edsm_state - if monitor.logdir: + proxyaddr = edproxy.status() + self.out_log_auto_text['text'] = '' + self.edsm_log_auto_text['text'] = '' + if monitor.logdir or proxyaddr: log = 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(): + if proxyaddr: + self.out_log_auto_text['text'] = _('Connected to edproxy at {ADDR}').format(ADDR = proxyaddr) + elif not monitor.enable_logging(): self.out_log_auto_text['text'] = "Can't enable automatic logging!" # Shouldn't happen - don't translate elif monitor.restart_required(): self.out_log_auto_text['text'] = _('Re-start Elite: Dangerous to use this feature') # Output settings prompt self.edsm_log_auto_button['state'] = edsm_state - self.edsm_log_auto_text['text'] = '' if self.out_log_edsm.get() and self.out_log_auto.get(): - if not monitor.enable_logging(): + if proxyaddr: + self.edsm_log_auto_text['text'] = _('Connected to edproxy at {ADDR}').format(ADDR = proxyaddr) + elif not monitor.enable_logging(): self.edsm_log_auto_text['text'] = "Can't enable automatic logging!" # Shouldn't happen - don't translate elif monitor.restart_required(): self.edsm_log_auto_text['text'] = _('Re-start Elite: Dangerous to use this feature') # Output settings prompt - + else: + self.out_log_auto_button['state'] = tk.DISABLED + self.edsm_log_auto_button['state'] = tk.DISABLED def outbrowse(self): if platform != 'win32': @@ -393,11 +405,13 @@ class PreferencesDialog(tk.Toplevel): def _destroy(self): # 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)): + if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_FILE|config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.parent) + edproxy.start(self.parent) else: monitor.stop() + edproxy.stop() self.destroy() if platform == 'darwin':