diff --git a/EDMarketConnector.py b/EDMarketConnector.py index cd764e05..fffb6d00 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -18,75 +18,96 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, Tuple from constants import applongname, appname, protocolhandler_redirect -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__": + 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) + + return True + + def already_running_popup(): + """Create the "already running" popup.""" + import tkinter as tk + from tkinter import ttk + + root = tk.Tk(className=appname.lower()) + + frame = tk.Frame(root) + frame.grid(row=1, column=0, sticky=tk.NSEW) + + label = tk.Label(frame) + label['text'] = 'An EDMarketConnector.exe process was already running, exiting.' + label.grid(row=1, column=0, sticky=tk.NSEW) + + button = ttk.Button(frame, text='OK', command=lambda: sys.exit(0)) + button.grid(row=2, column=0, sticky=tk.S) + + root.mainloop() + 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. @@ -97,9 +118,15 @@ if __name__ == "__main__": # 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 + # Logging isn't set up yet. + # We'll keep this print, but it will be over-written by any subsequent + # write by the already-running process. print("An EDMarketConnector.exe process was already running, exiting.") + # To be sure the user knows, we need a popup + already_running_popup() + # If the user closes the popup with the 'X', not the 'OK' button we'll + # reach here. sys.exit(0) # Keep this as the very first code run to be as sure as possible of no @@ -306,7 +333,6 @@ if TYPE_CHECKING: """Fake the l10n translation functions for typing.""" return x - if getattr(sys, 'frozen', False): # Under py2exe sys.path[0] is the executable name if platform == 'win32':