diff --git a/monitor.py b/monitor.py index e3b78de1..eac9908e 100644 --- a/monitor.py +++ b/monitor.py @@ -125,6 +125,7 @@ class EDLogs(FileSystemEventHandler): if self.currentdir and self.currentdir != logdir: self.stop() + self.currentdir = logdir # Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically. @@ -133,6 +134,7 @@ class EDLogs(FileSystemEventHandler): logfiles = sorted([x for x in listdir(self.currentdir) if re.search(r'^Journal(Beta)?\.[0-9]{12}\.[0-9]{2}\.log$', x)], key=lambda x: x.split('.')[1:]) self.logfile = logfiles and join(self.currentdir, logfiles[-1]) or None + except: self.logfile = None return False @@ -146,6 +148,7 @@ class EDLogs(FileSystemEventHandler): self.observer = Observer() self.observer.daemon = True self.observer.start() + elif polling and self.observer: self.observer.stop() self.observer = None @@ -173,12 +176,14 @@ class EDLogs(FileSystemEventHandler): if self.observed: self.observed = None self.observer.unschedule_all() + self.thread = None # Orphan the worker thread - will terminate at next poll def close(self): self.stop() if self.observer: self.observer.stop() + if self.observer: self.observer.join() self.observer = None @@ -202,13 +207,16 @@ class EDLogs(FileSystemEventHandler): loghandle = open(logfile, 'rb', 0) # unbuffered if platform == 'darwin': fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB + for line in loghandle: try: self.parse_entry(line) # Some events are of interest even in the past + except: if __debug__: print('Invalid journal entry "%s"' % repr(line)) logpos = loghandle.tell() + else: loghandle = None @@ -225,14 +233,19 @@ class EDLogs(FileSystemEventHandler): ('SystemAddress', self.systemaddress), ('Population', self.systempopulation), ]) + if self.planet: entry['Body'] = self.planet + entry['Docked'] = bool(self.station) + if self.station: entry['StationName'] = self.station entry['StationType'] = self.stationtype entry['MarketID'] = self.station_marketid + self.event_queue.append(json.dumps(entry, separators=(', ', ':'))) + else: self.event_queue.append(None) # Generate null event to update the display (with possibly out-of-date info) self.live = False @@ -251,19 +264,24 @@ class EDLogs(FileSystemEventHandler): logfiles = sorted([x for x in listdir(self.currentdir) if re.search(r'^Journal(Beta)?\.[0-9]{12}\.[0-9]{2}\.log$', x)], key=lambda x: x.split('.')[1:]) newlogfile = logfiles and join(self.currentdir, logfiles[-1]) or None + except: if __debug__: print_exc() + newlogfile = None if logfile != newlogfile: logfile = newlogfile if loghandle: loghandle.close() + if logfile: loghandle = open(logfile, 'rb', 0) # unbuffered if platform == 'darwin': fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB + logpos = 0 + if __debug__: print('New logfile "%s"' % logfile) @@ -272,8 +290,10 @@ class EDLogs(FileSystemEventHandler): loghandle.seek(logpos, SEEK_SET) # reset EOF flag for line in loghandle: self.event_queue.append(line) + if self.event_queue: self.root.event_generate('<>', when="tail") + logpos = loghandle.tell() sleep(self._POLL) @@ -287,6 +307,7 @@ class EDLogs(FileSystemEventHandler): self.event_queue.append('{ "timestamp":"%s", "event":"ShutDown" }' % strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())) self.root.event_generate('<>', when="tail") self.game_was_running = False + else: self.game_was_running = self.game_running() @@ -339,8 +360,10 @@ class EDLogs(FileSystemEventHandler): 'Rebuy' : None, 'Modules' : None, } + elif entry['event'] == 'Commander': self.live = True # First event in 3.0 + elif entry['event'] == 'LoadGame': self.cmdr = entry['Commander'] self.mode = entry.get('GameMode') # 'Open', 'Solo', 'Group', or None for CQC (and Training - but no LoadGame event) @@ -366,15 +389,19 @@ class EDLogs(FileSystemEventHandler): 'Statistics' : {}, 'Role' : None, }) + elif entry['event'] == 'NewCommander': self.cmdr = entry['Name'] self.group = None + elif entry['event'] == 'SetUserShipName': self.state['ShipID'] = entry['ShipID'] if 'UserShipId' in entry: # Only present when changing the ship's ident self.state['ShipIdent'] = entry['UserShipId'] + self.state['ShipName'] = entry.get('UserShipName') self.state['ShipType'] = self.canonicalise(entry['Ship']) + elif entry['event'] == 'ShipyardBuy': self.state['ShipID'] = None self.state['ShipIdent'] = None @@ -384,6 +411,7 @@ class EDLogs(FileSystemEventHandler): self.state['ModulesValue'] = None self.state['Rebuy'] = None self.state['Modules'] = None + elif entry['event'] == 'ShipyardSwap': self.state['ShipID'] = entry['ShipID'] self.state['ShipIdent'] = None @@ -393,9 +421,11 @@ class EDLogs(FileSystemEventHandler): self.state['ModulesValue'] = None self.state['Rebuy'] = None self.state['Modules'] = None + elif (entry['event'] == 'Loadout' and not 'fighter' in self.canonicalise(entry['Ship']) and not 'buggy' in self.canonicalise(entry['Ship'])): + self.state['ShipID'] = entry['ShipID'] self.state['ShipIdent'] = entry['ShipIdent'] @@ -420,7 +450,9 @@ class EDLogs(FileSystemEventHandler): module.get('AmmoInClip') == module.get('AmmoInHopper') == 1): # lasers module.pop('AmmoInClip') module.pop('AmmoInHopper') + self.state['Modules'][module['Slot']] = module + elif entry['event'] == 'ModuleBuy': self.state['Modules'][entry['Slot']] = { 'Slot' : entry['Slot'], @@ -430,29 +462,38 @@ class EDLogs(FileSystemEventHandler): 'Health' : 1.0, 'Value' : entry['BuyPrice'], } + elif entry['event'] == 'ModuleSell': self.state['Modules'].pop(entry['Slot'], None) + elif entry['event'] == 'ModuleSwap': toitem = self.state['Modules'].get(entry['ToSlot']) self.state['Modules'][entry['ToSlot']] = self.state['Modules'][entry['FromSlot']] if toitem: self.state['Modules'][entry['FromSlot']] = toitem + else: self.state['Modules'].pop(entry['FromSlot'], None) + elif entry['event'] in ['Undocked']: self.station = None self.station_marketid = None self.stationtype = None self.stationservices = None + elif entry['event'] in ['Location', 'FSDJump', 'Docked', 'CarrierJump']: if entry['event'] in ('Location', 'CarrierJump'): self.planet = entry.get('Body') if entry.get('BodyType') == 'Planet' else None + elif entry['event'] == 'FSDJump': self.planet = None + if 'StarPos' in entry: self.coordinates = tuple(entry['StarPos']) + elif self.system != entry['StarSystem']: self.coordinates = None # Docked event doesn't include coordinates + self.systemaddress = entry.get('SystemAddress') if entry['event'] in ['Location', 'FSDJump', 'CarrierJump']: @@ -463,8 +504,10 @@ class EDLogs(FileSystemEventHandler): self.station_marketid = entry.get('MarketID') # May be None self.stationtype = entry.get('StationType') # May be None self.stationservices = entry.get('StationServices') # None under E:D < 2.4 + elif entry['event'] == 'ApproachBody': self.planet = entry['Body'] + elif entry['event'] in ['LeaveBody', 'SupercruiseEntry']: self.planet = None @@ -474,10 +517,12 @@ class EDLogs(FileSystemEventHandler): payload.pop('timestamp') for k,v in payload.items(): self.state['Rank'][k] = (v,0) + elif entry['event'] == 'Progress': for k,v in entry.items(): if k in self.state['Rank']: self.state['Rank'][k] = (self.state['Rank'][k][0], min(v, 100)) # perhaps not taken promotion mission yet + elif entry['event'] in ['Reputation', 'Statistics']: payload = OrderedDict(entry) payload.pop('event') @@ -487,6 +532,7 @@ class EDLogs(FileSystemEventHandler): elif entry['event'] == 'EngineerProgress': if 'Engineers' in entry: # Startup summary self.state['Engineers'] = { e['Engineer']: (e['Rank'], e.get('RankProgress', 0)) if 'Rank' in e else e['Progress'] for e in entry['Engineers'] } + else: # Promotion self.state['Engineers'][entry['Engineer']] = (entry['Rank'], entry.get('RankProgress', 0)) if 'Rank' in entry else entry['Progress'] @@ -495,15 +541,19 @@ class EDLogs(FileSystemEventHandler): if 'Inventory' not in entry: # From 3.3 full Cargo event (after the first one) is written to a separate file with open(join(self.currentdir, 'Cargo.json'), 'rb') as h: entry = json.load(h, object_pairs_hook=OrderedDict) # Preserve property order because why not? + self.state['Cargo'].update({ self.canonicalise(x['Name']): x['Count'] for x in entry['Inventory'] }) + elif entry['event'] in ['CollectCargo', 'MarketBuy', 'BuyDrones', 'MiningRefined']: commodity = self.canonicalise(entry['Type']) self.state['Cargo'][commodity] += entry.get('Count', 1) + elif entry['event'] in ['EjectCargo', 'MarketSell', 'SellDrones']: commodity = self.canonicalise(entry['Type']) self.state['Cargo'][commodity] -= entry.get('Count', 1) if self.state['Cargo'][commodity] <= 0: self.state['Cargo'].pop(commodity) + elif entry['event'] == 'SearchAndRescue': for item in entry.get('Items', []): commodity = self.canonicalise(item['Name']) @@ -515,14 +565,17 @@ class EDLogs(FileSystemEventHandler): for category in ['Raw', 'Manufactured', 'Encoded']: self.state[category] = defaultdict(int) self.state[category].update({ self.canonicalise(x['Name']): x['Count'] for x in entry.get(category, []) }) + elif entry['event'] == 'MaterialCollected': material = self.canonicalise(entry['Name']) self.state[entry['Category']][material] += entry['Count'] + elif entry['event'] in ['MaterialDiscarded', 'ScientificResearch']: material = self.canonicalise(entry['Name']) self.state[entry['Category']][material] -= entry['Count'] if self.state[entry['Category']][material] <= 0: self.state[entry['Category']].pop(material) + elif entry['event'] == 'Synthesis': for category in ['Raw', 'Manufactured', 'Encoded']: for x in entry['Materials']: @@ -531,6 +584,7 @@ class EDLogs(FileSystemEventHandler): self.state[category][material] -= x['Count'] if self.state[category][material] <= 0: self.state[category].pop(material) + elif entry['event'] == 'MaterialTrade': category = self.category(entry['Paid']['Category']) self.state[category][entry['Paid']['Material']] -= entry['Paid']['Quantity'] @@ -547,6 +601,7 @@ class EDLogs(FileSystemEventHandler): self.state[category][material] -= x['Count'] if self.state[category][material] <= 0: self.state[category].pop(material) + module = self.state['Modules'][entry['Slot']] assert(module['Item'] == self.canonicalise(entry['Module'])) module['Engineering'] = { @@ -558,9 +613,11 @@ class EDLogs(FileSystemEventHandler): 'Quality' : entry['Quality'], 'Modifiers' : entry['Modifiers'], } + if 'ExperimentalEffect' in entry: module['Engineering']['ExperimentalEffect'] = entry['ExperimentalEffect'] module['Engineering']['ExperimentalEffect_Localised'] = entry['ExperimentalEffect_Localised'] + else: module['Engineering'].pop('ExperimentalEffect', None) module['Engineering'].pop('ExperimentalEffect_Localised', None) @@ -569,17 +626,20 @@ class EDLogs(FileSystemEventHandler): for reward in entry.get('CommodityReward', []): commodity = self.canonicalise(reward['Name']) self.state['Cargo'][commodity] += reward.get('Count', 1) + for reward in entry.get('MaterialsReward', []): if 'Category' in reward: # Category not present in E:D 3.0 category = self.category(reward['Category']) material = self.canonicalise(reward['Name']) self.state[category][material] += reward.get('Count', 1) + elif entry['event'] == 'EngineerContribution': commodity = self.canonicalise(entry.get('Commodity')) if commodity: self.state['Cargo'][commodity] -= entry['Quantity'] if self.state['Cargo'][commodity] <= 0: self.state['Cargo'].pop(commodity) + material = self.canonicalise(entry.get('Material')) if material: for category in ['Raw', 'Manufactured', 'Encoded']: @@ -587,6 +647,7 @@ class EDLogs(FileSystemEventHandler): self.state[category][material] -= entry['Quantity'] if self.state[category][material] <= 0: self.state[category].pop(material) + elif entry['event'] == 'TechnologyBroker': for thing in entry.get('Ingredients', []): # 3.01 for category in ['Cargo', 'Raw', 'Manufactured', 'Encoded']: @@ -595,11 +656,13 @@ class EDLogs(FileSystemEventHandler): self.state[category][item] -= thing['Count'] if self.state[category][item] <= 0: self.state[category].pop(item) + for thing in entry.get('Commodities', []): # 3.02 commodity = self.canonicalise(thing['Name']) self.state['Cargo'][commodity] -= thing['Count'] if self.state['Cargo'][commodity] <= 0: self.state['Cargo'].pop(commodity) + for thing in entry.get('Materials', []): # 3.02 material = self.canonicalise(thing['Name']) category = thing['Category'] @@ -618,8 +681,10 @@ class EDLogs(FileSystemEventHandler): self.stationservices = None self.coordinates = None self.systemaddress = None + elif entry['event'] == 'ChangeCrewRole': self.state['Role'] = entry['Role'] + elif entry['event'] == 'QuitACrew': self.state['Captain'] = None self.state['Role'] = None @@ -643,6 +708,7 @@ class EDLogs(FileSystemEventHandler): if __debug__: print('Invalid journal entry "%s"' % repr(line)) print_exc() + return { 'event': None } # Commodities, Modules and Ships can appear in different forms e.g. "$HNShockMount_Name;", "HNShockMount", and "hnshockmount", @@ -661,6 +727,7 @@ class EDLogs(FileSystemEventHandler): def get_entry(self): if not self.event_queue: return None + else: entry = self.parse_entry(self.event_queue.pop(0)) if not self.live and entry['event'] not in [None, 'Fileheader']: @@ -678,6 +745,7 @@ class EDLogs(FileSystemEventHandler): ('StarPos', self.coordinates), ('SystemAddress', self.systemaddress), ]) + else: entry = OrderedDict([ ('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())), @@ -687,20 +755,21 @@ class EDLogs(FileSystemEventHandler): ('StarPos', self.coordinates), ('SystemAddress', self.systemaddress), ]) + self.event_queue.append(json.dumps(entry, separators=(', ', ':'))) + elif self.live and entry['event'] == 'Music' and entry.get('MusicTrack') == 'MainMenu': self.event_queue.append('{ "timestamp":"%s", "event":"ShutDown" }' % strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())) + return entry def game_running(self): - if platform == 'darwin': for app in NSWorkspace.sharedWorkspace().runningApplications(): if app.bundleIdentifier() == 'uk.co.frontier.EliteDangerous': return True elif platform == 'win32': - def WindowTitle(h): if h: l = GetWindowTextLength(h) + 1 @@ -716,6 +785,7 @@ class EDLogs(FileSystemEventHandler): if handle: # If GetProcessHandleFromHwnd succeeds then the app is already running as this user CloseHandle(handle) return False # stop enumeration + return True return not EnumWindows(EnumWindowsProc(callback), 0) @@ -733,30 +803,36 @@ class EDLogs(FileSystemEventHandler): d = OrderedDict() if timestamped: d['timestamp'] = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()) + d['event'] = 'Loadout' d['Ship'] = self.state['ShipType'] d['ShipID'] = self.state['ShipID'] + if self.state['ShipName']: d['ShipName'] = self.state['ShipName'] + if self.state['ShipIdent']: d['ShipIdent'] = self.state['ShipIdent'] + # sort modules by slot - hardpoints, standard, internal d['Modules'] = [] + for slot in sorted(self.state['Modules'], key=lambda x: ('Hardpoint' not in x, x not in standard_order and len(standard_order) or standard_order.index(x), 'Slot' not in x, x)): module = dict(self.state['Modules'][slot]) module.pop('Health', None) module.pop('Value', None) d['Modules'].append(module) + return d # Export ship loadout as a Loadout event def export_ship(self, filename=None): string = json.dumps(self.ship(False), ensure_ascii=False, indent=2, separators=(',', ': ')) # pretty print - if filename: with open(filename, 'wt') as h: h.write(string) + return ship = ship_file_name(self.state['ShipName'], self.state['ShipType'])