From 248493c16a00fa42dd3b03cfe3a183dbca683a68 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 22 Mar 2021 13:01:00 +0000 Subject: [PATCH] EDMarketConnector: Change 'no other .exe runnig?' checks * This is now done even before the stdout/err redirect. * The function is now called no_other_instance_running() to make the conditional use of it read more naturally. * For now this *append* logs to the plain log file. **BUT** any subsequent write by the already-running process will be over the top of this, so a future commit will use a popup instead. # Conflicts: # EDMarketConnector.py --- EDMarketConnector.py | 188 ++++++++++++++++++++++++++----------------- 1 file changed, 113 insertions(+), 75 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 5ad83f88..63d7b5f9 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -18,9 +18,96 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, Tuple from constants import applongname, appname, protocolhandler_redirect -# config will now cause an appname logger to be set up, so we need the -# console redirect before this -if __name__ == '__main__': +# TODO: Test: Make *sure* this redirect is working, else py2exe is going to cause an exit popup +# TODO: Move enforce_single_instance() call here ? But then how do we notify +# the user if they already have a process ? +# We could propagate what that found all the way up and then do the stderr/out redirect +# in a non-truncate manner. Need to double-check the existing process won't then overwrite +# due to its seek position. +def no_other_instance_running() -> bool: # noqa: CCR001 + """ + Ensure only one copy of the app is running under this user account. + + OSX does this automatically. + + :returns: True if we are the single instance, else False. + """ + # TODO: Linux implementation + if platform == 'win32': + import ctypes + from ctypes.wintypes import BOOL, HWND, INT, LPARAM, LPCWSTR, LPWSTR + + EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806 + GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806 + GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] + GetWindowText = ctypes.windll.user32.GetWindowTextW # noqa: N806 + GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] + GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW # noqa: N806 + GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 + + SW_RESTORE = 9 # noqa: N806 + SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow # noqa: N806 + ShowWindow = ctypes.windll.user32.ShowWindow # noqa: N806 + ShowWindowAsync = ctypes.windll.user32.ShowWindowAsync # noqa: N806 + + COINIT_MULTITHREADED = 0 # noqa: N806,F841 + COINIT_APARTMENTTHREADED = 0x2 # noqa: N806 + COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806 + CoInitializeEx = ctypes.windll.ole32.CoInitializeEx # noqa: N806 + + ShellExecute = ctypes.windll.shell32.ShellExecuteW # noqa: N806 + ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT] + + def window_title(h): + if h: + text_length = GetWindowTextLength(h) + 1 + buf = ctypes.create_unicode_buffer(text_length) + if GetWindowText(h, buf, text_length): + return buf.value + + return None + + @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) + 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 \ + 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): + CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) + # Wait for it to be responsive to avoid ShellExecute recursing + ShowWindow(window_handle, SW_RESTORE) + ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE) + + else: + ShowWindowAsync(window_handle, SW_RESTORE) + SetForegroundWindow(window_handle) + + return False + + return True + + return EnumWindows(enumwindowsproc, 0) + +if __name__ == "__main__": + if not no_other_instance_running(): + # There's a copy already running. We want to inform the user by + # **appending** to the log file, not truncating it. + 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='a', buffering=1) + + # Logging isn't set up yet + print("An EDMarketConnector.exe process was already running, exiting.") + + sys.exit(0) + # Keep this as the very first code run to be as sure as possible of no # output until after this redirect is done, if needed. if getattr(sys, 'frozen', False): @@ -1306,77 +1393,6 @@ class AppWindow(object): self.blank_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) -def enforce_single_instance() -> None: # noqa: CCR001 - """ - Ensure only one copy of the app is running under this user account. - - OSX does this automatically. - """ - # TODO: Linux implementation - if platform == 'win32': - import ctypes - from ctypes.wintypes import BOOL, HWND, INT, LPARAM, LPCWSTR, LPWSTR - - EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806 - GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806 - GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] - GetWindowText = ctypes.windll.user32.GetWindowTextW # noqa: N806 - GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] - GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW # noqa: N806 - GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 - - SW_RESTORE = 9 # noqa: N806 - SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow # noqa: N806 - ShowWindow = ctypes.windll.user32.ShowWindow # noqa: N806 - ShowWindowAsync = ctypes.windll.user32.ShowWindowAsync # noqa: N806 - - COINIT_MULTITHREADED = 0 # noqa: N806,F841 - COINIT_APARTMENTTHREADED = 0x2 # noqa: N806 - COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806 - CoInitializeEx = ctypes.windll.ole32.CoInitializeEx # noqa: N806 - - ShellExecute = ctypes.windll.shell32.ShellExecuteW # noqa: N806 - ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT] - - def window_title(h): - if h: - text_length = GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(text_length) - if GetWindowText(h, buf, text_length): - return buf.value - - return None - - @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) - 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 \ - 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): - # logger.debug('Browser invoked us directly with auth response. ' - # 'Forwarding the response to the other app instance.') - CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) - # Wait for it to be responsive to avoid ShellExecute recursing - ShowWindow(window_handle, SW_RESTORE) - ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE) - - else: - ShowWindowAsync(window_handle, SW_RESTORE) - SetForegroundWindow(window_handle) - - # logger.info(f'A running {applongname} process was found, exiting.') - return False - - return True - - if not EnumWindows(enumwindowsproc, 0): - sys.exit(0) - - def test_logging() -> None: """Simple test of top level logging.""" logger.debug('Test from EDMarketConnector.py top-level test_logging()') @@ -1394,7 +1410,29 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}''' # Run the app -if __name__ == "__main__": # noqa C901 +if __name__ == "__main__": + # Command-line arguments + parser = argparse.ArgumentParser( + prog=appname, + description="Utilises Elite Dangerous Journal files and the Frontier " + "Companion API (CAPI) service to gather data about a " + "player's state and actions to upload to third-party sites " + "such as EDSM, Inara.cz and EDDB." + ) + + parser.add_argument('--trace', + help='Set the Debug logging loglevel to TRACE', + action='store_true', + ) + + args = parser.parse_args() + + if args.trace: + logger.setLevel(logging.TRACE) + edmclogger.set_channels_loglevel(logging.TRACE) + else: + edmclogger.set_channels_loglevel(logging.DEBUG) + logger.info(f'Startup v{appversion} : Running on Python v{sys.version}') logger.debug(f'''Platform: {sys.platform} {sys.platform == "win32" and sys.getwindowsversion()} argv[0]: {sys.argv[0]}