diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 4d25fe17..a3e46aac 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -23,6 +23,7 @@ if __name__ == "__main__": if getattr(sys, 'frozen', False): # By default py2exe tries to write log to dirname(sys.executable) which fails when installed import tempfile + # unbuffered not allowed for text in python3, so use `1 for line buffering sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), f'{appname}.log'), mode='wt', buffering=1) @@ -45,6 +46,7 @@ if __debug__: if platform != 'win32': import pdb import signal + signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame)) import companion @@ -61,7 +63,6 @@ from protocol import protocolhandler from dashboard import dashboard from theme import theme - SERVER_RETRY = 5 # retry pause for Companion servers [s] SHIPYARD_HTML_TEMPLATE = """ @@ -81,7 +82,6 @@ SHIPYARD_HTML_TEMPLATE = """ class AppWindow(object): - # Tkinter Event types EVENT_KEYPRESS = 2 EVENT_BUTTON = 4 @@ -104,10 +104,14 @@ class AppWindow(object): if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: - self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501 - self.theme_icon = tk.PhotoImage(data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501 - self.theme_minimize = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 - self.theme_close = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 + self.w.tk.call('wm', 'iconphoto', self.w, '-default', + tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501 + self.theme_icon = tk.PhotoImage( + data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501 + self.theme_minimize = tk.BitmapImage( + data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 + self.theme_close = tk.BitmapImage( + data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501 frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) @@ -152,7 +156,8 @@ class AppWindow(object): row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) - theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501 + theme.register_alternate((self.button, self.theme_button, self.theme_button), + {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501 self.status.grid(columnspan=2, sticky=tk.EW) self.button.bind('<Button-1>', self.getandsend) theme.button_bind(self.theme_button, self.getandsend) @@ -291,6 +296,7 @@ class AppWindow(object): # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT + # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 # noqa: N806 if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), @@ -322,6 +328,7 @@ class AppWindow(object): # Load updater after UI creation (for WinSparkle) import update + if getattr(sys, 'frozen', False): # Running in frozen .exe, so use (Win)Sparkle self.updater = update.Updater(tkroot=self.w, provider='external') @@ -453,7 +460,7 @@ class AppWindow(object): if not retrying: if time() < self.holdofftime: # Was invoked by key while in cooldown self.status['text'] = '' - if play_sound and (self.holdofftime-time()) < companion.holdoff*0.75: + if play_sound and (self.holdofftime - time()) < companion.holdoff * 0.75: hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats return elif play_sound: @@ -493,7 +500,7 @@ class AppWindow(object): if isdir('dump'): with open('dump/{system}{station}.{timestamp}.json'.format( system=data['lastSystem']['name'], - station=data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', + station=data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '', timestamp=strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wb') as h: h.write(json.dumps(data, ensure_ascii=False, @@ -525,7 +532,7 @@ class AppWindow(object): self.status['text'] = _("You're not docked at a station!") play_bad = True # Ignore possibly missing shipyard info - elif (config.getint('output') & config.OUT_MKT_EDDN)\ + elif (config.getint('output') & config.OUT_MKT_EDDN) \ and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") @@ -585,12 +592,12 @@ class AppWindow(object): if not data['commander'].get('docked'): # might have un-docked while we were waiting for retry in which case station data is unreliable pass - elif (data.get('lastSystem', {}).get('name') == monitor.system and + elif (data.get('lastSystem', {}).get('name') == monitor.system and data.get('lastStarport', {}).get('name') == monitor.station and data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')): self.eddn.export_shipyard(data, monitor.is_beta) elif tries > 1: # bogus data - retry - self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries-1)) + self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries - 1)) except Exception: pass @@ -600,11 +607,11 @@ class AppWindow(object): def crewroletext(role): # Return translated crew role. Needs to be dynamic to allow for changing language. return { - None: '', - 'Idle': '', + None : '', + 'Idle' : '', 'FighterCon': _('Fighter'), # Multicrew role - 'FireCon': _('Gunner'), # Multicrew role - 'FlightCon': _('Helm'), # Multicrew role + 'FireCon' : _('Gunner'), # Multicrew role + 'FlightCon' : _('Helm'), # Multicrew role }.get(role, role) while True: @@ -626,8 +633,8 @@ class AppWindow(object): self.ship_label['text'] = _('Ship') + ':' # Main window self.ship.configure( text=monitor.state['ShipName'] - or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType']) - or '', + or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType']) + or '', url=self.shipyard_url) else: self.cmdr['text'] = '' @@ -673,7 +680,7 @@ class AppWindow(object): logger.info("Can't start Status monitoring") # Export loadout - if entry['event'] == 'Loadout' and not monitor.state['Captain']\ + if entry['event'] == 'Loadout' and not monitor.state['Captain'] \ and config.getint('output') & config.OUT_SHIP: monitor.export_ship() @@ -690,10 +697,10 @@ class AppWindow(object): hotkeymgr.play_bad() # Auto-Update after docking, but not if auth callback is pending - if entry['event'] in ('StartUp', 'Location', 'Docked')\ - and monitor.station\ - and not config.getint('output') & config.OUT_MKT_MANUAL\ - and config.getint('output') & config.OUT_STATION_ANY\ + if entry['event'] in ('StartUp', 'Location', 'Docked') \ + and monitor.station \ + and not config.getint('output') & config.OUT_MKT_MANUAL \ + and config.getint('output') & config.OUT_STATION_ANY \ and companion.session.state != companion.Session.STATE_AUTH: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) @@ -760,10 +767,10 @@ class AppWindow(object): return f'file://localhost/{file_name}' def system_url(self, system): - return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system) + return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system) def station_url(self, station): - return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) + return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) def cooldown(self): if time() < self.holdofftime: @@ -837,7 +844,7 @@ class AppWindow(object): ############################################################ # version <link to changelog> - ttk.Label(frame).grid(row=row, column=0) # spacer + ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 self.appversion_label = tk.Label(frame, text=appversion) self.appversion_label.grid(row=row, column=0, sticky=tk.E) @@ -855,7 +862,7 @@ class AppWindow(object): ############################################################ # <copyright> - ttk.Label(frame).grid(row=row, column=0) # spacer + ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 self.copyright = tk.Label(frame, text=copyright) self.copyright.grid(row=row, columnspan=3, sticky=tk.EW) @@ -864,7 +871,7 @@ class AppWindow(object): ############################################################ # OK button to close the window - ttk.Label(frame).grid(row=row, column=0) # spacer + ttk.Label(frame).grid(row=row, column=0) # spacer row += 1 button = ttk.Button(frame, text=_('OK'), command=self.apply) button.grid(row=row, column=2, sticky=tk.E) @@ -895,7 +902,7 @@ class AppWindow(object): last_system: str = data.get("lastSystem", {}).get("name", "Unknown") last_starport: str = '' if data['commander'].get('docked'): - last_starport = '.'+data.get('lastStarport', {}).get('name', 'Unknown') + last_starport = '.' + data.get('lastStarport', {}).get('name', 'Unknown') timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime()) f = tkinter.filedialog.asksaveasfilename(parent=self.w, defaultextension=default_extension, @@ -971,6 +978,7 @@ def enforce_single_instance() -> None: if platform == 'win32': import ctypes from ctypes.wintypes import HWND, LPWSTR, LPCWSTR, INT, BOOL, LPARAM + EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806 GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806 GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806 @@ -1004,9 +1012,9 @@ def enforce_single_instance() -> None: def enumwindowsproc(window_handle, l_param): # class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576 cls = ctypes.create_unicode_buffer(257) - if GetClassName(window_handle, cls, 257)\ - and cls.value == 'TkTopLevel'\ - and window_title(window_handle) == applongname\ + if GetClassName(window_handle, cls, 257) \ + and cls.value == 'TkTopLevel' \ + and window_title(window_handle) == applongname \ and GetProcessHandleFromHwnd(window_handle): # If GetProcessHandleFromHwnd succeeds then the app is already running as this user if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect): @@ -1028,38 +1036,52 @@ def test_logging(): logger.debug('Test from EDMarketConnector.py top-level test_logging()') -# Run the app -if __name__ == "__main__": - enforce_single_instance() - - from EDMCLogging import logger - logger.info(f'Startup v{appversion} : Running on Python v{sys.version}') - logger.debug(f'''Platform: {sys.platform} -argv[0]: {sys.argv[0]} -exec_prefix: {sys.exec_prefix} -executable: {sys.executable} -sys.path: {sys.path} +def log_locale(prefix: str) -> None: + logger.debug(f'''Locale: {prefix} Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)} Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)} Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)} Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)} Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' -) + ) + + +# Run the app +if __name__ == "__main__": + enforce_single_instance() + + from EDMCLogging import logger + + logger.info(f'Startup v{appversion} : Running on Python v{sys.version}') + logger.debug(f'''Platform: {sys.platform} +argv[0]: {sys.argv[0]} +exec_prefix: {sys.exec_prefix} +executable: {sys.executable} +sys.path: {sys.path}''' + ) # Change locale to a utf8 one - # First make sure the local is actually set as per locale's idea of defaults + # Log what we have at startup + log_locale('Initial Locale') + # Make sure the local is actually set as per locale's idea of defaults locale.setlocale(locale.LC_ALL, '') + log_locale('After LC_ALL defaults set') # Now find out the current locale, mostly the language locale_startup = locale.getlocale(locale.LC_ALL) + logger.debug(f'Locale LC_ALL: {locale_startup}') # Now set that same language, but utf8 encoding (it was probably cp1252 # or equivalent for other languages). locale.setlocale(locale.LC_ALL, (locale_startup[0], 'utf8')) + log_locale('After switching to utf8 encoding (same language)') # TODO: unittests in place of these # logger.debug('Test from __main__') # test_logging() + class A(object): + class B(object): + def __init__(self): logger.debug('A call from A.B.__init__') @@ -1102,7 +1124,8 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' # Now the string should match, so try translation popup_text = _(popup_text) # And substitute in the other words. - popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled') + popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), + DISABLED='.disabled') # And now we do need these to be actual \r\n popup_text = popup_text.replace('\\n', '\n') popup_text = popup_text.replace('\\r', '\r')