From 08ac054ccef1b7a8184e4223419e3ef04f05220a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 26 Oct 2020 13:27:07 +0000 Subject: [PATCH 1/6] requirements: py2exe now available from pypi, so no need for VCS --- requirements-dev.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7f756495..b50ae664 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,9 +20,8 @@ autopep8==1.5.4 grip>=4.5.2 # Packaging -# This isn't available via 'pip install', so has to be commented out in order for -# GitHub Action Workflows to not error out -#py2exe==0.9.3.2 +# We only need py2exe on windows. +py2exe==0.10.0.2; sys_platform == 'win32' # All of the normal requirements -r requirements.txt From 7be255b8acfafdf2a06508c28980e625b58f94b0 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 26 Oct 2020 13:49:13 +0000 Subject: [PATCH 2/6] Releasing: py2exe via pip / requirements-dev.txt now. --- docs/Releasing.md | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/docs/Releasing.md b/docs/Releasing.md index b340e256..fb0623a3 100644 --- a/docs/Releasing.md +++ b/docs/Releasing.md @@ -33,29 +33,16 @@ You will need several pieces of software installed, or the files from their [v3.7.9](https://www.python.org/downloads/release/python-379/) is the most recently tested version. You need the `Windows x86 executable installer` file, for the 32-bit version. -1. [py2exe](https://github.com/albertosottile/py2exe): - 1. Install the python module. There are two options here. - 1. You can use the latest release version [0.9.3.2](https://github.com/albertosottile/py2exe/releases/tag/v0.9.3.2) - and the current Marginal 'python3' branch as-is. This contains a - small hack in `setup.py` to ensure `sqlite3.dll` is packaged. - - pip install py2exe-0.9.3.2-cp37-none-win32.whl - 1. Or you can use a pre-release version, [0.9.4.0](https://bintray.com/alby128/py2exe/download_file?file_path=py2exe-0.9.4.0-cp37-none-win32.whl), see [this py2exe issue](https://github.com/albertosottile/py2exe/issues/23#issuecomment-541359225), - which packages that DLL file correctly. - - pip install py2exe-0.9.4.0-cp37-none-win32.whl - You can then edit out the following line from `setup.py`, but it - does no harm: - - %s/DLLs/sqlite3.dll' % (sys.base_prefix), +1. [py2exe](https://github.com/albertosottile/py2exe) - Now available via PyPi, + so will be picked up with the `pip install` below. Latest tested as per + `requirements-dev.txt`. 1. You'll now need to 'pip install' several python modules. 1. Ensure you have `pip` installed. If needs be see [Installing pip](https://pip.pypa.io/en/stable/installing/) 1. The easiest way is to utilise the `requirements-dev.txt` file: - `python -m pip install -r requirements-dev.txt`. This will install all - dependencies plus anything required for development *other than py2exe, see - above*. + `python -m pip install --user -r requirements-dev.txt`. This will install + all dependencies plus anything required for development. 1. Else check the contents of both `requirements.txt` and `requirements-dev.txt`, and ensure the modules listed there are installed as per the version requirements. From d650db5a1a0c3493ed7c96d68ccc36ac2e667c6e Mon Sep 17 00:00:00 2001 From: A_D Date: Sun, 18 Oct 2020 09:48:23 +0200 Subject: [PATCH 3/6] Added name mangling support to EDMCContextFilter Python name mangling is more about name collisions than actually making things private. This commit adds a check to resolve mangled names to their runtime names, and adds a just-in-case default to fetching the attribute. Fixes #764 --- EDMCLogging.py | 8 ++++++-- EDMarketConnector.py | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/EDMCLogging.py b/EDMCLogging.py index 246ee78d..846dba84 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -287,11 +287,15 @@ class EDMCContextFilter(logging.Filter): frame_info = inspect.getframeinfo(frame) args, _, _, value_dict = inspect.getargvalues(frame) if len(args) and args[0] in ('self', 'cls'): - frame_class = value_dict[args[0]] + frame_class: 'object' = value_dict[args[0]] if frame_class: + # See https://en.wikipedia.org/wiki/Name_mangling#Python for how name mangling works. + if (name := frame_info.function).startswith("__") and not name.endswith("__"): + name = f'_{frame_class.__class__.__name__}{frame_info.function}' + # Find __qualname__ of the caller - fn = getattr(frame_class, frame_info.function) + fn = getattr(frame_class, name, None) if fn and fn.__qualname__: caller_qualname = fn.__qualname__ diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 90ff631f..63cc2f6f 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1165,6 +1165,10 @@ sys.path: {sys.path}''' def __init__(self): logger.debug('A call from A.B.__init__') + self.__test() + + def __test(self): + logger.debug("A call from A.B.__test") # abinit = A.B() From 2b60220365cb723a63907e4eb6840b6c4fa6de70 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Thu, 15 Oct 2020 12:49:30 +0100 Subject: [PATCH 4/6] EDMarketConnector: popup for "already running" * This also refactors code around so that isort and flake8 are happy about the module level imports. --- EDMarketConnector.py | 114 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 63cc2f6f..564b35c8 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -17,8 +17,117 @@ from typing import TYPE_CHECKING from config import applongname, appname, appversion, appversion_nobuild, config, copyright -# TODO: Test: Make *sure* this redirect is working, else py2exe is going to cause an exit popup 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. + 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. + # 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 # output until after this redirect is done, if needed. if getattr(sys, 'frozen', False): @@ -36,6 +145,9 @@ if TYPE_CHECKING: from logging import trace, TRACE # type: ignore # noqa: F401 # isort: on + def _(x: str) -> str: + """Fake the l10n translation functions for typing.""" + return x if getattr(sys, 'frozen', False): # Under py2exe sys.path[0] is the executable name From 3ff9bdcc8818faae88aa8fcc3073fd1cb876ef32 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 8 Dec 2020 16:08:30 +0000 Subject: [PATCH 5/6] Fix Python 3.8 syntax use in backport to 4.1.x/Python 3.7 --- EDMCLogging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EDMCLogging.py b/EDMCLogging.py index 846dba84..308e85fc 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -291,7 +291,8 @@ class EDMCContextFilter(logging.Filter): if frame_class: # See https://en.wikipedia.org/wiki/Name_mangling#Python for how name mangling works. - if (name := frame_info.function).startswith("__") and not name.endswith("__"): + name = frame_info.function + if name.startswith("__") and not name.endswith("__"): name = f'_{frame_class.__class__.__name__}{frame_info.function}' # Find __qualname__ of the caller From 5cc4a6e80b2c76662569d4b803412b6e7742da26 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 13 Oct 2020 12:31:12 +0100 Subject: [PATCH 6/6] EDMarketConnector: Add detailed logging to shutdown sequence --- EDMarketConnector.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 564b35c8..b1b2a55f 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1059,18 +1059,43 @@ class AppWindow(object): if platform != 'darwin' or self.w.winfo_rooty() > 0: x, y = self.w.geometry().split('+')[1:3] # e.g. '212x170+2881+1267' config.set('geometry', f'+{x}+{y}') - self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen + + # Let the user know we're shutting down. + self.status['text'] = 'Shutting down...' + self.w.update_idletasks() + logger.info('Starting shutdown procedures...') + + logger.info('Closing protocol handler...') protocolhandler.close() + + logger.info('Unregistering hotkey manager...') hotkeymgr.unregister() + + logger.info('Closing dashboard...') dashboard.close() + + logger.info('Closing journal monitor...') monitor.close() + + logger.info('Notifying plugins to stop...') plug.notify_stop() + + logger.info('Closing update checker...') self.updater.close() + + logger.info('Closing Frontier CAPI sessions...') companion.session.close() + + logger.info('Closing config...') config.close() + + logger.info('Destroying app window...') self.w.destroy() - def drag_start(self, event): + logger.info('Done.') + + def drag_start(self, event) -> None: + """Initiate dragging the window.""" self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) def drag_continue(self, event):