From a081b6b637ab86057660e89f4efe9475933da26d Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Mon, 29 Jan 2018 01:34:30 +0000 Subject: [PATCH] Monitor player status, and call new "status" plugin callback --- EDMarketConnector.py | 19 +++++++ PLUGINS.md | 14 ++++- plug.py | 21 +++++++ status.py | 131 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 status.py diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 672ab03c..1a1fd7e7 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -57,6 +57,7 @@ import prefs import plug from hotkey import hotkeymgr from monitor import monitor +from status import status from theme import theme @@ -269,6 +270,7 @@ class AppWindow: self.w.bind('', self.getandsend) self.w.bind_all('<>', self.getandsend) # Hotkey monitoring self.w.bind_all('<>', self.journal_event) # Journal monitoring + self.w.bind_all('<>', self.status_event) # Cmdr Status monitoring self.w.bind_all('<>', self.plugin_error) # Statusbar self.w.bind_all('<>', self.onexit) # Updater @@ -627,6 +629,11 @@ class AppWindow: if not config.getint('hotkey_mute'): hotkeymgr.play_bad() + if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: + # Can start status monitoring + if not status.start(self.w, monitor.started): + print "Can't start Status monitoring" + # Don't send to EDDN while on crew if monitor.state['Captain']: return @@ -678,6 +685,17 @@ class AppWindow: if not config.getint('hotkey_mute'): hotkeymgr.play_bad() + # Handle Status event + def status_event(self, event): + entry = status.status + if entry: + # Currently we don't do anything with these events + err = plug.notify_status(monitor.cmdr, monitor.is_beta, entry) + if err: + self.status['text'] = err + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() + # Display asynchronous error from plugin def plugin_error(self, event=None): if plug.last_error.get('msg'): @@ -779,6 +797,7 @@ class AppWindow: config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen hotkeymgr.unregister() + status.close() monitor.close() plug.notify_stop() self.eddn.close() diff --git a/PLUGINS.md b/PLUGINS.md index f1c5ef8f..c85a5fe5 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -106,7 +106,7 @@ this.status["text"] = "Happy!" ## Events -Once you have created your plugin and EDMC has loaded it there are two other functions you can define to be notified by EDMC when something happens: `journal_entry()` and `cmdr_data()`. +Once you have created your plugin and EDMC has loaded it there are three other functions you can define to be notified by EDMC when something happens: `journal_entry()`, `status()` and `cmdr_data()`. Your events all get called on the main tkinter loop so be sure not to block for very long or the EDMC will appear to freeze. If you have a long running operation then you should take a look at how to do background updates in tkinter - http://effbot.org/zone/tkinter-threads.htm @@ -128,6 +128,16 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) ``` +### Player Status + +This gets called periodically - typically about once a second - whith the players live status + +```python +def status(cmdr, is_beta, entry): + deployed = entry['Flags'] & 1<<6 + sys.stderr.write("Hardpoints {}\n", deployed and "deployed" or "stowed") +``` + ### Getting Commander Data This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers. @@ -144,7 +154,7 @@ The data is a dictionary and full of lots of wonderful stuff! ## Error messages -You can display an error in EDMC's status area by returning a string from your `journal_entry()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound). +You can display an error in EDMC's status area by returning a string from your `journal_entry()`, `status()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound). The status area is shared between EDMC itself and all other plugins, so your message won't be displayed for very long. Create a dedicated widget if you need to display routine status information. diff --git a/plug.py b/plug.py index 9ffc29a6..045a59f3 100644 --- a/plug.py +++ b/plug.py @@ -226,6 +226,27 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state): return error +def notify_status(cmdr, is_beta, entry): + """ + Send a status entry to each plugin. + :param cmdr: The piloting Cmdr name + :param is_beta: whether the player is in a Beta universe. + :param entry: The status entry as a dictionary + :return: Error message from the first plugin that returns one (if any) + """ + error = None + for plugin in PLUGINS: + status = plugin._get_func('status') + if status: + try: + # Pass a copy of the status entry in case the callee modifies it + newerror = status(cmdr, is_beta, dict(entry)) + error = error or newerror + except: + print_exc() + return error + + def notify_system_changed(timestamp, system, coordinates): """ Send notification data to each plugin when we arrive at a new system. diff --git a/status.py b/status.py new file mode 100644 index 00000000..466b30fe --- /dev/null +++ b/status.py @@ -0,0 +1,131 @@ +import json +from calendar import timegm +from operator import itemgetter +from os import listdir, stat +from os.path import getmtime, isdir, join +from sys import platform +import time + +if __debug__: + from traceback import print_exc + +from config import config + + +if platform=='darwin': + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler + +elif platform=='win32': + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler + +else: + # Linux's inotify doesn't work over CIFS or NFS, so poll + FileSystemEventHandler = object # dummy + + +# Status.json handler +class Status(FileSystemEventHandler): + + _POLL = 1 # Fallback polling interval + + def __init__(self): + FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog + self.root = None + self.currentdir = None # The actual logdir that we're monitoring + self.observer = None + self.observed = None # a watchdog ObservedWatch, or None if polling + self.status = {} # Current status for communicating status back to main thread + + def start(self, root, started): + self.root = root + self.session_start = started + + logdir = config.get('journaldir') or config.default_journal_dir + if not logdir or not isdir(logdir): + self.stop() + return False + + if self.currentdir and self.currentdir != logdir: + self.stop() + self.currentdir = logdir + + # Set up a watchdog observer. + # 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('statusdir')) and platform != 'win32' + if not polling and not self.observer: + self.observer = Observer() + self.observer.daemon = True + self.observer.start() + elif polling and self.observer: + self.observer.stop() + self.observer = None + + if not self.observed and not polling: + self.observed = self.observer.schedule(self, self.currentdir) + + if __debug__: + print '%s status "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) + + # Even if we're not intending to poll, poll at least once to process pre-existing + # data and to check whether the watchdog thread has crashed due to events not + # being supported on this filesystem. + self.root.after(self._POLL * 1000/2, self.poll, True) + + return True + + def stop(self): + if __debug__: + print 'Stopping monitoring Status' + self.currentdir = None + if self.observed: + self.observed = None + self.observer.unschedule_all() + self.status = {} + + def close(self): + self.stop() + if self.observer: + self.observer.stop() + if self.observer: + self.observer.join() + self.observer = None + + def poll(self, first_time=False): + if not self.currentdir: + # Stopped + self.status = {} + else: + self.process() + + if first_time: + # Watchdog thread + emitter = self.observed and self.observer._emitter_for_watch[self.observed] # Note: Uses undocumented attribute + if emitter and emitter.is_alive(): + return # Watchdog thread still running - stop polling + + self.root.after(self._POLL * 1000, self.poll) # keep polling + + def on_modified(self, event): + # watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows + if event.is_directory or stat(event.src_path).st_size: # Can get on_modified events when the file is emptied + self.process(event.src_path if not event.is_directory else None) + + # Can be called either in watchdog thread or, if polling, in main thread. + def process(self, logfile=None): + try: + with open(join(self.currentdir, 'Status.json'), 'rb') as h: + entry = json.load(h) + + # Status file is shared between beta and live. So filter out status not in this game session. + if timegm(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) >= self.session_start and self.status != entry: + self.status = entry + self.root.event_generate('<>', when="tail") + except: + if __debug__: print_exc() + +# singleton +status = Status()