import json from operator import itemgetter from os import listdir, stat from os.path import getmtime, isdir, join from sys import platform 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 # CommanderHistory handler class Interactions(FileSystemEventHandler): _POLL = 5 # 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.seen = [] # interactions that we've already processed self.interaction_queue = [] # For communicating interactions back to main thread def start(self, root, started): self.root = root self.session_start = started logdir = config.get('interactiondir') or config.default_interaction_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('interactiondir')) 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 interactions "%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 interactions' self.currentdir = None if self.observed: self.observed = None self.observer.unschedule_all() def close(self): self.stop() if self.observer: self.observer.stop() self.observer.join() self.observer = None def poll(self, first_time=False): 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. The code assumes not both. def process(self, logfile=None): if not logfile: for logfile in [x for x in listdir(self.currentdir) if x.endswith('.cmdrHistory')]: if self.session_start and getmtime(join(self.currentdir, logfile)) >= self.session_start: break else: return try: # cmdrHistory file is shared between beta and live. So filter out interactions not in this game session. start = self.session_start + 11644473600 # Game time is 369 years in the future with open(join(self.currentdir, logfile), 'rb') as h: current = [x for x in json.load(h)['Interactions'] if x['Epoch'] >= start] new = [x for x in current if x not in self.seen] # O(n^2) comparison but currently limited to 10x10 self.interaction_queue.extend(sorted(new, key=itemgetter('Epoch'))) # sort by time self.seen = current if self.interaction_queue: self.root.event_generate('<>', when="tail") except: if __debug__: print_exc() def get_entry(self): if not self.interaction_queue: return None else: return self.interaction_queue.pop(0) # singleton interactions = Interactions()