From 17a7af959a29aa0c6a02a88aa8cf3c150e8ac8fb Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 10 Jun 2024 23:00:44 -0400 Subject: [PATCH 01/11] [#1805] pywin32 Handoff --- config/windows.py | 29 ++++------------ hotkey/windows.py | 83 +++++++++++++++++++++++--------------------- monitor.py | 18 +++------- prefs.py | 10 ++---- requirements-dev.txt | 2 -- requirements.txt | 2 ++ stats.py | 9 ++--- theme.py | 11 +++--- 8 files changed, 68 insertions(+), 96 deletions(-) diff --git a/config/windows.py b/config/windows.py index 75e3f6977..9f3304834 100644 --- a/config/windows.py +++ b/config/windows.py @@ -7,41 +7,23 @@ """ from __future__ import annotations -import ctypes import functools import pathlib import sys import uuid import winreg -from ctypes.wintypes import DWORD, HANDLE from typing import Literal from config import AbstractConfig, applongname, appname, logger +from win32comext.shell import shell assert sys.platform == 'win32' REG_RESERVED_ALWAYS_ZERO = 0 -# This is the only way to do this from python without external deps (which do this anyway). -FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}') -FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}') -FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}') -FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}') - -SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath -SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)] - -CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree -CoTaskMemFree.argtypes = [ctypes.c_void_p] - def known_folder_path(guid: uuid.UUID) -> str | None: """Look up a Windows GUID to actual folder path name.""" - buf = ctypes.c_wchar_p() - if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)): - return None - retval = buf.value # copy data - CoTaskMemFree(buf) # and free original - return retval + return shell.SHGetKnownFolderPath(guid, 0, 0) class WinConfig(AbstractConfig): @@ -49,7 +31,8 @@ class WinConfig(AbstractConfig): def __init__(self) -> None: super().__init__() - self.app_dir_path = pathlib.Path(known_folder_path(FOLDERID_LocalAppData)) / appname # type: ignore + if local_appdata := known_folder_path(shell.FOLDERID_LocalAppData): + self.app_dir_path = pathlib.Path(local_appdata) / appname self.app_dir_path.mkdir(exist_ok=True) self.plugin_dir_path = self.app_dir_path / 'plugins' @@ -65,7 +48,7 @@ def __init__(self) -> None: self.home_path = pathlib.Path.home() journal_dir_path = pathlib.Path( - known_folder_path(FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore + known_folder_path(shell.FOLDERID_SavedGames)) / 'Frontier Developments' / 'Elite Dangerous' # type: ignore self.default_journal_dir_path = journal_dir_path if journal_dir_path.is_dir() else None # type: ignore REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806 @@ -84,7 +67,7 @@ def __init__(self) -> None: self.identifier = applongname if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir(): - docs = known_folder_path(FOLDERID_Documents) + docs = known_folder_path(shell.FOLDERID_Documents) self.set("outdir", docs if docs is not None else self.home) def __get_regentry(self, key: str) -> None | list | str | int: diff --git a/hotkey/windows.py b/hotkey/windows.py index fa4cb3a65..cbcbd161b 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -8,7 +8,10 @@ import threading import tkinter as tk import winsound -from ctypes.wintypes import DWORD, HWND, LONG, LPWSTR, MSG, ULONG, WORD +from ctypes.wintypes import DWORD, LONG, MSG, ULONG, WORD +import pywintypes +import win32api +import win32gui from config import config from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr @@ -17,26 +20,20 @@ logger = get_main_logger() -RegisterHotKey = ctypes.windll.user32.RegisterHotKey -UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey +UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey # TODO: Coming Soon + MOD_ALT = 0x0001 MOD_CONTROL = 0x0002 MOD_SHIFT = 0x0004 MOD_WIN = 0x0008 MOD_NOREPEAT = 0x4000 -GetMessage = ctypes.windll.user32.GetMessageW -TranslateMessage = ctypes.windll.user32.TranslateMessage -DispatchMessage = ctypes.windll.user32.DispatchMessageW -PostThreadMessage = ctypes.windll.user32.PostThreadMessageW WM_QUIT = 0x0012 WM_HOTKEY = 0x0312 WM_APP = 0x8000 WM_SND_GOOD = WM_APP + 1 WM_SND_BAD = WM_APP + 2 -GetKeyState = ctypes.windll.user32.GetKeyState -MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW VK_BACK = 0x08 VK_CLEAR = 0x0c VK_RETURN = 0x0d @@ -60,10 +57,13 @@ VK_PROCESSKEY = 0xe5 VK_OEM_CLEAR = 0xfe -GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow -GetWindowText = ctypes.windll.user32.GetWindowTextW -GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] -GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW +# VirtualKey mapping values +# +MAPVK_VK_TO_VSC = 0 +MAPVK_VSC_TO_VK = 1 +MAPVK_VK_TO_CHAR = 2 +MAPVK_VSC_TO_VK_EX = 3 +MAPVK_VK_TO_VSC_EX = 4 def window_title(h) -> str: @@ -74,9 +74,9 @@ def window_title(h) -> str: :return: Window title. """ if h: - title_length = GetWindowTextLength(h) + 1 + title_length = win32gui.GetWindowTextLength(h) + 1 buf = ctypes.create_unicode_buffer(title_length) - if GetWindowText(h, buf, title_length): + if win32gui.GetWindowText(h, buf, title_length): return buf.value return '' @@ -197,7 +197,7 @@ def unregister(self) -> None: logger.debug('Thread is/was running') self.thread = None # type: ignore logger.debug('Telling thread WM_QUIT') - PostThreadMessage(thread.ident, WM_QUIT, 0, 0) + win32gui.PostThreadMessage(thread.ident, WM_QUIT, 0, 0) logger.debug('Joining thread') thread.join() # Wait for it to unregister hotkey and quit @@ -210,8 +210,10 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 """Handle hotkeys.""" logger.debug('Begin...') # Hotkey must be registered by the thread that handles it - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We're not the right thread?") + try: + win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) + except pywintypes.error: + logger.exception("We're not the right thread?") self.thread = None # type: ignore return @@ -219,14 +221,14 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 msg = MSG() logger.debug('Entering GetMessage() loop...') - while GetMessage(ctypes.byref(msg), None, 0, 0) != 0: + while win32gui.GetMessage(ctypes.byref(msg), None, 0, 0) != 0: logger.debug('Got message') if msg.message == WM_HOTKEY: logger.debug('WM_HOTKEY') if ( - config.get_int('hotkey_always') - or window_title(GetForegroundWindow()).startswith('Elite - Dangerous') + config.get_int('hotkey_always') + or window_title(win32gui.GetForegroundWindow()).startswith('Elite - Dangerous') ): if not config.shutting_down: logger.debug('Sending event <>') @@ -236,8 +238,10 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 logger.debug('Passing key on') UnregisterHotKey(None, 1) SendInput(1, fake, ctypes.sizeof(INPUT)) - if not RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode): - logger.debug("We aren't registered for this ?") + try: + win32gui.RegisterHotKey(None, 1, modifiers | MOD_NOREPEAT, keycode) + except pywintypes.error: + logger.exception("We aren't registered for this ?") break elif msg.message == WM_SND_GOOD: @@ -250,8 +254,8 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 else: logger.debug('Something else') - TranslateMessage(ctypes.byref(msg)) - DispatchMessage(ctypes.byref(msg)) + win32gui.TranslateMessage(ctypes.byref(msg)) + win32gui.DispatchMessage(ctypes.byref(msg)) logger.debug('Exited GetMessage() loop.') UnregisterHotKey(None, 1) @@ -266,7 +270,7 @@ def acquire_stop(self) -> None: """Stop acquiring hotkey state.""" pass - def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 + def fromevent(self, event) -> bool | tuple | None: """ Return configuration (keycode, modifiers) or None=clear or False=retain previous. @@ -277,11 +281,11 @@ def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 :param event: tk event ? :return: False to retain previous, None to not use, else (keycode, modifiers) """ - modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ - | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ - | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ - | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ - | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + modifiers = ((win32api.GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ + | ((win32api.GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ + | ((win32api.GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ + | ((win32api.GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ + | ((win32api.GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) keycode = event.keycode if keycode in (VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN): @@ -295,7 +299,7 @@ def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 return None if ( - keycode in (VK_RETURN, VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z') + keycode in (VK_RETURN, VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z') ): # don't allow keys needed for typing in System Map winsound.MessageBeep() return None @@ -305,12 +309,13 @@ def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 return 0, modifiers # See if the keycode is usable and available - if RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode): + try: + win32gui.RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode) UnregisterHotKey(None, 2) return keycode, modifiers - - winsound.MessageBeep() - return None + except pywintypes.error: + winsound.MessageBeep() + return None def display(self, keycode, modifiers) -> str: """ @@ -346,7 +351,7 @@ def display(self, keycode, modifiers) -> str: text += WindowsHotkeyMgr.DISPLAY[keycode] else: - c = MapVirtualKey(keycode, 2) # printable ? + c = win32api.MapVirtualKey(keycode, MAPVK_VK_TO_CHAR) if not c: # oops not printable text += '⁈' @@ -361,9 +366,9 @@ def display(self, keycode, modifiers) -> str: def play_good(self) -> None: """Play the 'good' sound.""" if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) + win32gui.PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) def play_bad(self) -> None: """Play the 'bad' sound.""" if self.thread: - PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) + win32gui.PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) diff --git a/monitor.py b/monitor.py index 9d33f5eb5..ba1a4d39e 100644 --- a/monitor.py +++ b/monitor.py @@ -36,23 +36,15 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR + from ctypes.wintypes import BOOL, HWND, LPARAM + import win32gui from watchdog.events import FileSystemEventHandler, FileSystemEvent from watchdog.observers import Observer from watchdog.observers.api import BaseObserver - EnumWindows = ctypes.windll.user32.EnumWindows EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) - CloseHandle = ctypes.windll.kernel32.CloseHandle - - GetWindowText = ctypes.windll.user32.GetWindowTextW - GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] - GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW - GetWindowTextLength.argtypes = [ctypes.wintypes.HWND] - GetWindowTextLength.restype = ctypes.c_int - GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd else: @@ -2131,9 +2123,9 @@ def game_running(self) -> bool: # noqa: CCR001 if sys.platform == 'win32': def WindowTitle(h): # noqa: N802 if h: - length = GetWindowTextLength(h) + 1 + length = win32gui.GetWindowTextLength(h) + 1 buf = ctypes.create_unicode_buffer(length) - if GetWindowText(h, buf, length): + if win32gui.GetWindowText(h, buf, length): return buf.value return None @@ -2147,7 +2139,7 @@ def callback(hWnd, lParam): # noqa: N803 return True - return not EnumWindows(EnumWindowsProc(callback), 0) + return not win32gui.EnumWindows(EnumWindowsProc(callback), 0) return False diff --git a/prefs.py b/prefs.py index 9f2062f40..e7ccef18f 100644 --- a/prefs.py +++ b/prefs.py @@ -188,7 +188,8 @@ def __exit__( if sys.platform == 'win32': import ctypes import winreg - from ctypes.wintypes import HINSTANCE, HWND, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT + from ctypes.wintypes import HINSTANCE, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT + import win32gui is_wine = False try: WINE_REGISTRY_KEY = r'HKEY_LOCAL_MACHINE\Software\Wine' @@ -219,11 +220,6 @@ def __exit__( ctypes.POINTER(RECT) ] - GetParent = ctypes.windll.user32.GetParent - GetParent.argtypes = [HWND] - GetWindowRect = ctypes.windll.user32.GetWindowRect - GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)] - SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)] @@ -314,7 +310,7 @@ def __init__(self, parent: tk.Tk, callback: Optional[Callable]): # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - GetWindowRect(GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()), position) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), SIZE(position.right - position.left, position.bottom - position.top), # type: ignore diff --git a/requirements-dev.txt b/requirements-dev.txt index 7c8e1ef5f..1e30ff465 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -42,8 +42,6 @@ pytest==8.2.0 pytest-cov==5.0.0 # Pytest code coverage support coverage[toml]==7.5.0 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs coverage-conditional-plugin==0.9.0 -# For manipulating folder permissions and the like. -pywin32==306; sys_platform == 'win32' # All of the normal requirements diff --git a/requirements.txt b/requirements.txt index 22f0a360f..b278cce5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ pillow==10.3.0 watchdog==4.0.0 simplesystray==0.1.0; sys_platform == 'win32' semantic-version==2.10.0 +# For manipulating folder permissions and the like. +pywin32==306; sys_platform == 'win32' \ No newline at end of file diff --git a/stats.py b/stats.py index db61f7892..965b7fc89 100644 --- a/stats.py +++ b/stats.py @@ -25,17 +25,14 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT + from ctypes.wintypes import POINT, RECT, SIZE, UINT + import win32gui try: CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition CalculatePopupWindowPosition.argtypes = [ ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT) ] - GetParent = ctypes.windll.user32.GetParent - GetParent.argtypes = [HWND] - GetWindowRect = ctypes.windll.user32.GetWindowRect - GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)] except Exception: # Not supported under Wine 4.0 CalculatePopupWindowPosition = None # type: ignore @@ -423,7 +420,7 @@ def __init__(self, parent: tk.Tk, data: dict[str, Any]) -> None: # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - GetWindowRect(GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()), position) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), # - is evidently supported on the C side diff --git a/theme.py b/theme.py index 94e99f7a6..dc48499e8 100644 --- a/theme.py +++ b/theme.py @@ -34,6 +34,7 @@ if sys.platform == 'win32': import ctypes from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR + import win32gui AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] # type: ignore FR_PRIVATE = 0x10 @@ -427,8 +428,6 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 GWL_EXSTYLE = -20 # noqa: N806 # ctypes WS_EX_APPWINDOW = 0x00040000 # noqa: N806 # ctypes WS_EX_LAYERED = 0x00080000 # noqa: N806 # ctypes - GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes - SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes # FIXME: Lose the "treat this like a boolean" bullshit if theme == self.THEME_DEFAULT: @@ -445,14 +444,14 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here - hwnd = ctypes.windll.user32.GetParent(root.winfo_id()) - SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize + hwnd = win32gui.GetParent(root.winfo_id()) + win32gui.SetWindowLong(hwnd, GWL_STYLE, win32gui.GetWindowLong(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize if theme == self.THEME_TRANSPARENT: - SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar + win32gui.SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar else: - SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar + win32gui.SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar root.deiconify() root.wait_visibility() # need main window to be displayed before returning From 256be4c8a9351729798382234c88cafa4abfeace Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 10 Jun 2024 23:03:20 -0400 Subject: [PATCH 02/11] [1805] Linter Cleanup --- hotkey/windows.py | 2 +- theme.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hotkey/windows.py b/hotkey/windows.py index cbcbd161b..188c89e9e 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -270,7 +270,7 @@ def acquire_stop(self) -> None: """Stop acquiring hotkey state.""" pass - def fromevent(self, event) -> bool | tuple | None: + def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 """ Return configuration (keycode, modifiers) or None=clear or False=retain previous. diff --git a/theme.py b/theme.py index dc48499e8..121bcb60e 100644 --- a/theme.py +++ b/theme.py @@ -445,7 +445,8 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here hwnd = win32gui.GetParent(root.winfo_id()) - win32gui.SetWindowLong(hwnd, GWL_STYLE, win32gui.GetWindowLong(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize + win32gui.SetWindowLong(hwnd, GWL_STYLE, + win32gui.GetWindowLong(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize if theme == self.THEME_TRANSPARENT: win32gui.SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar From b1ba45ab906c14a50c7a2b099624b7f4b29dce2c Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Mon, 10 Jun 2024 23:29:04 -0400 Subject: [PATCH 03/11] [1805] Update Prototypes --- hotkey/windows.py | 5 ++++- l10n.py | 1 - monitor.py | 4 +++- prefs.py | 5 ++++- stats.py | 12 ++++++------ 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/hotkey/windows.py b/hotkey/windows.py index 188c89e9e..671df1e81 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -8,7 +8,7 @@ import threading import tkinter as tk import winsound -from ctypes.wintypes import DWORD, LONG, MSG, ULONG, WORD +from ctypes.wintypes import DWORD, LONG, MSG, ULONG, WORD, HWND, BOOL, UINT import pywintypes import win32api import win32gui @@ -21,6 +21,8 @@ logger = get_main_logger() UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey # TODO: Coming Soon +UnregisterHotKey.argtypes = [HWND, ctypes.c_int] +UnregisterHotKey.restype = BOOL MOD_ALT = 0x0001 MOD_CONTROL = 0x0002 @@ -138,6 +140,7 @@ class INPUT(ctypes.Structure): SendInput = ctypes.windll.user32.SendInput SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int] +SendInput.restype = UINT INPUT_MOUSE = 0 INPUT_KEYBOARD = 1 diff --git a/l10n.py b/l10n.py index 8613244fa..29c18f750 100755 --- a/l10n.py +++ b/l10n.py @@ -50,7 +50,6 @@ GetUserPreferredUILanguages.argtypes = [ DWORD, ctypes.POINTER(ctypes.c_ulong), LPCVOID, ctypes.POINTER(ctypes.c_ulong) ] - GetUserPreferredUILanguages.restype = BOOL LOCALE_NAME_USER_DEFAULT = None diff --git a/monitor.py b/monitor.py index ba1a4d39e..5a971c694 100644 --- a/monitor.py +++ b/monitor.py @@ -36,7 +36,7 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import BOOL, HWND, LPARAM + from ctypes.wintypes import BOOL, HWND, LPARAM, HANDLE import win32gui from watchdog.events import FileSystemEventHandler, FileSystemEvent @@ -45,6 +45,8 @@ EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) CloseHandle = ctypes.windll.kernel32.CloseHandle + CloseHandle.argtypes = [HANDLE] + CloseHandle.restype = BOOL GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd else: diff --git a/prefs.py b/prefs.py index e7ccef18f..89e107fb2 100644 --- a/prefs.py +++ b/prefs.py @@ -188,7 +188,7 @@ def __exit__( if sys.platform == 'win32': import ctypes import winreg - from ctypes.wintypes import HINSTANCE, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT + from ctypes.wintypes import HINSTANCE, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL import win32gui is_wine = False try: @@ -204,6 +204,8 @@ def __exit__( if not is_wine: try: CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition + CalculatePopupWindowPosition.argtypes = [POINT, SIZE, UINT, RECT, RECT] + CalculatePopupWindowPosition.restype = BOOL except AttributeError as e: logger.error( @@ -225,6 +227,7 @@ def __exit__( LoadString = ctypes.windll.user32.LoadStringW LoadString.argtypes = [HINSTANCE, UINT, LPWSTR, ctypes.c_int] + LoadString.restype = ctypes.c_int class PreferencesDialog(tk.Toplevel): diff --git a/stats.py b/stats.py index 965b7fc89..b8a6531a7 100644 --- a/stats.py +++ b/stats.py @@ -25,7 +25,7 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import POINT, RECT, SIZE, UINT + from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL import win32gui try: @@ -33,6 +33,7 @@ CalculatePopupWindowPosition.argtypes = [ ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT) ] + CalculatePopupWindowPosition.restype = BOOL except Exception: # Not supported under Wine 4.0 CalculatePopupWindowPosition = None # type: ignore @@ -240,7 +241,7 @@ def ships(companion_data: dict[str, Any]) -> list[ShipRet]: """ Return a list of 5 tuples of ship information. - :param data: [description] + :param companion_data: [description] :return: A 5 tuple of strings containing: Ship ID, Ship Type Name (internal), Ship Name, System, Station, and Value """ ships: list[dict[str, Any]] = companion.listify(cast(list, companion_data.get('ships'))) @@ -250,16 +251,15 @@ def ships(companion_data: dict[str, Any]) -> list[ShipRet]: ships.insert(0, ships.pop(current)) # Put current ship first if not companion_data['commander'].get('docked'): - out: list[ShipRet] = [] # Set current system, not last docked - out.append(ShipRet( + out: list[ShipRet] = [ShipRet( id=str(ships[0]['id']), type=ship_name_map.get(ships[0]['name'].lower(), ships[0]['name']), name=str(ships[0].get('shipName', '')), system=companion_data['lastSystem']['name'], station='', value=str(ships[0]['value']['total']) - )) + )] out.extend( ShipRet( id=str(ship['id']), @@ -299,7 +299,7 @@ def export_ships(companion_data: dict[str, Any], filename: AnyStr) -> None: h.writerow(list(thing)) -class StatsDialog(): +class StatsDialog: """Status dialog containing all of the current cmdr's stats.""" def __init__(self, parent: tk.Tk, status: tk.Label) -> None: From f8d354a4dd408fad5641ca7c5dc9dc93d8e4c41b Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 11 Jun 2024 10:42:57 -0400 Subject: [PATCH 04/11] [1805] Implement win32con in Hotkey --- EDMarketConnector.py | 42 ++++++++++-------------- hotkey/windows.py | 76 ++++++++++++++------------------------------ monitor.py | 2 +- 3 files changed, 42 insertions(+), 78 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index cd76ccc07..e48a927c4 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -253,23 +253,17 @@ def handle_edmc_callback_or_foregrounding() -> None: # noqa: CCR001 logger.trace_if('frontier-auth.windows', 'Begin...') if sys.platform == 'win32': - # If *this* instance hasn't locked, then another already has and we # now need to do the edmc:// checks for auth callback if locked != JournalLockResult.LOCKED: - from ctypes import windll, c_int, create_unicode_buffer, WINFUNCTYPE - from ctypes.wintypes import BOOL, HWND, INT, LPARAM, LPCWSTR, LPWSTR - - EnumWindows = windll.user32.EnumWindows # noqa: N806 - GetClassName = windll.user32.GetClassNameW # noqa: N806 - GetClassName.argtypes = [HWND, LPWSTR, c_int] - GetWindowText = windll.user32.GetWindowTextW # noqa: N806 - GetWindowText.argtypes = [HWND, LPWSTR, c_int] - GetWindowTextLength = windll.user32.GetWindowTextLengthW # noqa: N806 + from ctypes import windll, create_unicode_buffer, WINFUNCTYPE + from ctypes.wintypes import BOOL, HWND, LPARAM + import win32gui + import win32api + GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 SW_RESTORE = 9 # noqa: N806 - SetForegroundWindow = windll.user32.SetForegroundWindow # noqa: N806 ShowWindow = windll.user32.ShowWindow # noqa: N806 ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806 @@ -278,14 +272,11 @@ def handle_edmc_callback_or_foregrounding() -> None: # noqa: CCR001 COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806 CoInitializeEx = windll.ole32.CoInitializeEx # noqa: N806 - ShellExecute = windll.shell32.ShellExecuteW # noqa: N806 - ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT] - def window_title(h: int) -> str | None: if h: - text_length = GetWindowTextLength(h) + 1 + text_length = win32gui.GetWindowTextLength(h) + 1 buf = create_unicode_buffer(text_length) - if GetWindowText(h, buf, text_length): + if win32gui.GetWindowText(h): return buf.value return None @@ -309,7 +300,7 @@ def enumwindowsproc(window_handle, l_param): # noqa: CCR001 # class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576 cls = create_unicode_buffer(257) # This conditional is exploded to make debugging slightly easier - if GetClassName(window_handle, cls, 257): + if win32gui.GetClassName(window_handle, cls, 257): if cls.value == 'TkTopLevel': if window_title(window_handle) == applongname: if GetProcessHandleFromHwnd(window_handle): @@ -318,11 +309,11 @@ def enumwindowsproc(window_handle, l_param): # noqa: CCR001 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) + win32api.ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE) else: ShowWindowAsync(window_handle, SW_RESTORE) - SetForegroundWindow(window_handle) + win32gui.SetForegroundWindow(window_handle) return False # Indicate window found, so stop iterating @@ -334,7 +325,7 @@ def enumwindowsproc(window_handle, l_param): # noqa: CCR001 # enumwindwsproc() on each. When an invocation returns False it # stops iterating. # Ref: - EnumWindows(enumwindowsproc, 0) + win32gui.EnumWindows(enumwindowsproc, 0) def already_running_popup(): """Create the "already running" popup.""" @@ -701,13 +692,14 @@ def open_window(systray: 'SysTrayIcon') -> None: if match: if sys.platform == 'win32': # Check that the titlebar will be at least partly on screen - import ctypes - from ctypes.wintypes import POINT + import win32api + import win32con + x = int(match.group(1)) + 16 + y = int(match.group(2)) + 16 + point = (x, y) # 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), - MONITOR_DEFAULTTONULL): + if win32api.MonitorFromPoint(point, win32con.MONITOR_DEFAULTTONULL): self.w.geometry(config.get_str('geometry')) else: self.w.geometry(config.get_str('geometry')) diff --git a/hotkey/windows.py b/hotkey/windows.py index 671df1e81..51da80be6 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -12,6 +12,7 @@ import pywintypes import win32api import win32gui +import win32con from config import config from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr @@ -24,40 +25,10 @@ UnregisterHotKey.argtypes = [HWND, ctypes.c_int] UnregisterHotKey.restype = BOOL -MOD_ALT = 0x0001 -MOD_CONTROL = 0x0002 -MOD_SHIFT = 0x0004 -MOD_WIN = 0x0008 MOD_NOREPEAT = 0x4000 - -WM_QUIT = 0x0012 -WM_HOTKEY = 0x0312 -WM_APP = 0x8000 -WM_SND_GOOD = WM_APP + 1 -WM_SND_BAD = WM_APP + 2 - -VK_BACK = 0x08 -VK_CLEAR = 0x0c -VK_RETURN = 0x0d -VK_SHIFT = 0x10 -VK_CONTROL = 0x11 -VK_MENU = 0x12 -VK_CAPITAL = 0x14 -VK_MODECHANGE = 0x1f -VK_ESCAPE = 0x1b -VK_SPACE = 0x20 -VK_DELETE = 0x2e -VK_LWIN = 0x5b -VK_RWIN = 0x5c -VK_NUMPAD0 = 0x60 -VK_DIVIDE = 0x6f -VK_F1 = 0x70 -VK_F24 = 0x87 +WM_SND_GOOD = win32con.WM_APP + 1 +WM_SND_BAD = win32con.WM_APP + 2 VK_OEM_MINUS = 0xbd -VK_NUMLOCK = 0x90 -VK_SCROLL = 0x91 -VK_PROCESSKEY = 0xe5 -VK_OEM_CLEAR = 0xfe # VirtualKey mapping values # @@ -200,7 +171,7 @@ def unregister(self) -> None: logger.debug('Thread is/was running') self.thread = None # type: ignore logger.debug('Telling thread WM_QUIT') - win32gui.PostThreadMessage(thread.ident, WM_QUIT, 0, 0) + win32gui.PostThreadMessage(thread.ident, win32con.WM_QUIT, 0, 0) logger.debug('Joining thread') thread.join() # Wait for it to unregister hotkey and quit @@ -226,7 +197,7 @@ def worker(self, keycode, modifiers) -> None: # noqa: CCR001 logger.debug('Entering GetMessage() loop...') while win32gui.GetMessage(ctypes.byref(msg), None, 0, 0) != 0: logger.debug('Got message') - if msg.message == WM_HOTKEY: + if msg.message == win32con.WM_HOTKEY: logger.debug('WM_HOTKEY') if ( @@ -284,31 +255,32 @@ def fromevent(self, event) -> bool | tuple | None: # noqa: CCR001 :param event: tk event ? :return: False to retain previous, None to not use, else (keycode, modifiers) """ - modifiers = ((win32api.GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ - | ((win32api.GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ - | ((win32api.GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ - | ((win32api.GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ - | ((win32api.GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + modifiers = ((win32api.GetKeyState(win32con.VK_MENU) & 0x8000) and win32con.MOD_ALT) \ + | ((win32api.GetKeyState(win32con.VK_CONTROL) & 0x8000) and win32con.MOD_CONTROL) \ + | ((win32api.GetKeyState(win32con.VK_SHIFT) & 0x8000) and win32con.MOD_SHIFT) \ + | ((win32api.GetKeyState(win32con.VK_LWIN) & 0x8000) and win32con.MOD_WIN) \ + | ((win32api.GetKeyState(win32con.VK_RWIN) & 0x8000) and win32con.MOD_WIN) keycode = event.keycode - if keycode in (VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN): + if keycode in (win32con.VK_SHIFT, win32con.VK_CONTROL, win32con.VK_MENU, win32con.VK_LWIN, win32con.VK_RWIN): return 0, modifiers if not modifiers: - if keycode == VK_ESCAPE: # Esc = retain previous + if keycode == win32con.VK_ESCAPE: # Esc = retain previous return False - if keycode in (VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR): # BkSp, Del, Clear = clear hotkey + if keycode in (win32con.VK_BACK, win32con.VK_DELETE, + win32con.VK_CLEAR, win32con.VK_OEM_CLEAR): # BkSp, Del, Clear = clear hotkey return None if ( - keycode in (VK_RETURN, VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z') + keycode in (win32con.VK_RETURN, win32con.VK_SPACE, VK_OEM_MINUS) or ord('A') <= keycode <= ord('Z') ): # don't allow keys needed for typing in System Map winsound.MessageBeep() return None - if (keycode in (VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY) - or VK_CAPITAL <= keycode <= VK_MODECHANGE): # ignore unmodified mode switch keys + if (keycode in (win32con.VK_NUMLOCK, win32con.VK_SCROLL, win32con.VK_PROCESSKEY) + or win32con.VK_CAPITAL <= keycode <= win32con.VK_MODECHANGE): # ignore unmodified mode switch keys return 0, modifiers # See if the keycode is usable and available @@ -329,26 +301,26 @@ def display(self, keycode, modifiers) -> str: :return: string form """ text = '' - if modifiers & MOD_WIN: + if modifiers & win32con.MOD_WIN: text += '❖+' - if modifiers & MOD_CONTROL: + if modifiers & win32con.MOD_CONTROL: text += 'Ctrl+' - if modifiers & MOD_ALT: + if modifiers & win32con.MOD_ALT: text += 'Alt+' - if modifiers & MOD_SHIFT: + if modifiers & win32con.MOD_SHIFT: text += '⇧+' - if VK_NUMPAD0 <= keycode <= VK_DIVIDE: + if win32con.VK_NUMPAD0 <= keycode <= win32con.VK_DIVIDE: text += '№' if not keycode: pass - elif VK_F1 <= keycode <= VK_F24: - text += f'F{keycode + 1 - VK_F1}' + elif win32con.VK_F1 <= keycode <= win32con.VK_F24: + text += f'F{keycode + 1 - win32con.VK_F1}' elif keycode in WindowsHotkeyMgr.DISPLAY: # specials text += WindowsHotkeyMgr.DISPLAY[keycode] diff --git a/monitor.py b/monitor.py index 5a971c694..2f508bf9d 100644 --- a/monitor.py +++ b/monitor.py @@ -2127,7 +2127,7 @@ def WindowTitle(h): # noqa: N802 if h: length = win32gui.GetWindowTextLength(h) + 1 buf = ctypes.create_unicode_buffer(length) - if win32gui.GetWindowText(h, buf, length): + if win32gui.GetWindowText(h): return buf.value return None From 571558daffe579f856cd1d1efc4af42f7caf3b17 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 11 Jun 2024 11:06:58 -0400 Subject: [PATCH 05/11] [1805] Additional Handover --- EDMarketConnector.py | 8 ++++---- hotkey/windows.py | 2 +- protocol.py | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index e48a927c4..0cd344866 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -260,10 +260,10 @@ def handle_edmc_callback_or_foregrounding() -> None: # noqa: CCR001 from ctypes.wintypes import BOOL, HWND, LPARAM import win32gui import win32api + import win32con GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 - SW_RESTORE = 9 # noqa: N806 ShowWindow = windll.user32.ShowWindow # noqa: N806 ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806 @@ -308,11 +308,11 @@ def enumwindowsproc(window_handle, l_param): # noqa: CCR001 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) - win32api.ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE) + ShowWindow(window_handle, win32con.SW_RESTORE) + win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE) else: - ShowWindowAsync(window_handle, SW_RESTORE) + ShowWindowAsync(window_handle, win32con.SW_RESTORE) win32gui.SetForegroundWindow(window_handle) return False # Indicate window found, so stop iterating diff --git a/hotkey/windows.py b/hotkey/windows.py index 51da80be6..95b1a83af 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -49,7 +49,7 @@ def window_title(h) -> str: if h: title_length = win32gui.GetWindowTextLength(h) + 1 buf = ctypes.create_unicode_buffer(title_length) - if win32gui.GetWindowText(h, buf, title_length): + if win32gui.GetWindowText(h): return buf.value return '' diff --git a/protocol.py b/protocol.py index 1fb885957..eed14b440 100644 --- a/protocol.py +++ b/protocol.py @@ -76,6 +76,9 @@ def event(self, url: str) -> None: ATOM, BOOL, DWORD, HBRUSH, HGLOBAL, HICON, HINSTANCE, HMENU, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR, MSG, UINT, WPARAM ) + from win32con import CW_USEDEFAULT + import win32gui + import win32con class WNDCLASS(Structure): """ @@ -98,7 +101,6 @@ class WNDCLASS(Structure): ('lpszClassName', LPCWSTR) ] - CW_USEDEFAULT = 0x80000000 CreateWindowExW = windll.user32.CreateWindowExW CreateWindowExW.argtypes = [DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID] @@ -114,7 +116,6 @@ class WNDCLASS(Structure): paramflags = (1, "hWnd"), (1, "Msg"), (1, "wParam"), (1, "lParam") DefWindowProcW = prototype(("DefWindowProcW", windll.user32), paramflags) - GetParent = windll.user32.GetParent SetForegroundWindow = windll.user32.SetForegroundWindow # @@ -132,7 +133,6 @@ class WNDCLASS(Structure): PostMessageW = windll.user32.PostMessageW PostMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM] - WM_QUIT = 0x0012 # https://docs.microsoft.com/en-us/windows/win32/dataxchg/wm-dde-initiate WM_DDE_INITIATE = 0x03E0 WM_DDE_TERMINATE = 0x03E1 @@ -229,7 +229,7 @@ def close(self) -> None: thread = self.thread if thread: self.thread = None - PostThreadMessageW(thread.ident, WM_QUIT, 0, 0) + PostThreadMessageW(thread.ident, win32con.WM_QUIT, 0, 0) thread.join() # Wait for it to quit def worker(self) -> None: @@ -291,7 +291,7 @@ def worker(self) -> None: logger.debug(f'Message starts with {self.redirect}') self.event(url) - SetForegroundWindow(GetParent(self.master.winfo_id())) # raise app window + SetForegroundWindow(win32gui.GetParent(self.master.winfo_id())) # raise app window # Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam)) From 625856c31c871559bf02f4f64c0617c54fcad711 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 11 Jun 2024 11:36:43 -0400 Subject: [PATCH 06/11] [1805] Apply More Pywin32 --- EDMarketConnector.py | 4 +-- monitor.py | 8 ++--- prefs.py | 11 +++---- protocol.py | 75 ++++++++++++-------------------------------- stats.py | 2 +- theme.py | 15 +++------ 6 files changed, 34 insertions(+), 81 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 0cd344866..ecc84e1bf 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -263,8 +263,6 @@ def handle_edmc_callback_or_foregrounding() -> None: # noqa: CCR001 import win32con GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 - - ShowWindow = windll.user32.ShowWindow # noqa: N806 ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806 COINIT_MULTITHREADED = 0 # noqa: N806,F841 @@ -308,7 +306,7 @@ def enumwindowsproc(window_handle, l_param): # noqa: CCR001 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, win32con.SW_RESTORE) + win32gui.ShowWindow(window_handle, win32con.SW_RESTORE) win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE) else: diff --git a/monitor.py b/monitor.py index 2f508bf9d..8b55a2b92 100644 --- a/monitor.py +++ b/monitor.py @@ -36,17 +36,15 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import BOOL, HWND, LPARAM, HANDLE + from ctypes.wintypes import BOOL, HWND, LPARAM import win32gui + import win32api from watchdog.events import FileSystemEventHandler, FileSystemEvent from watchdog.observers import Observer from watchdog.observers.api import BaseObserver EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) - CloseHandle = ctypes.windll.kernel32.CloseHandle - CloseHandle.argtypes = [HANDLE] - CloseHandle.restype = BOOL GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd else: @@ -2136,7 +2134,7 @@ def callback(hWnd, lParam): # noqa: N803 if name and name.startswith('Elite - Dangerous'): handle = GetProcessHandleFromHwnd(hWnd) if handle: # If GetProcessHandleFromHwnd succeeds then the app is already running as this user - CloseHandle(handle) + win32api.CloseHandle(handle) return False # stop enumeration return True diff --git a/prefs.py b/prefs.py index 89e107fb2..7e4465d11 100644 --- a/prefs.py +++ b/prefs.py @@ -188,8 +188,9 @@ def __exit__( if sys.platform == 'win32': import ctypes import winreg - from ctypes.wintypes import HINSTANCE, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL + from ctypes.wintypes import LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL import win32gui + import win32api is_wine = False try: WINE_REGISTRY_KEY = r'HKEY_LOCAL_MACHINE\Software\Wine' @@ -225,10 +226,6 @@ def __exit__( SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)] - LoadString = ctypes.windll.user32.LoadStringW - LoadString.argtypes = [HINSTANCE, UINT, LPWSTR, ctypes.c_int] - LoadString.restype = ctypes.c_int - class PreferencesDialog(tk.Toplevel): """The EDMC preferences dialog.""" @@ -313,7 +310,7 @@ def __init__(self, parent: tk.Tk, callback: Optional[Callable]): # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), SIZE(position.right - position.left, position.bottom - position.top), # type: ignore @@ -1093,7 +1090,7 @@ def displaypath(self, pathvar: tk.StringVar, entryfield: tk.Entry) -> None: for i in range(start, len(components)): try: if (not SHGetLocalizedName('\\'.join(components[:i+1]), buf, MAX_PATH, ctypes.byref(pidsRes)) and - LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, pidsRes.value, buf, MAX_PATH)): + win32api.LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, pidsRes.value, buf, MAX_PATH)): display.append(buf.value) else: diff --git a/protocol.py b/protocol.py index eed14b440..a0f02f158 100644 --- a/protocol.py +++ b/protocol.py @@ -69,16 +69,16 @@ def event(self, url: str) -> None: # This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces assert sys.platform == 'win32' # spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL - from ctypes import ( # type: ignore - windll, POINTER, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at + from ctypes import ( + windll, WINFUNCTYPE, Structure, byref, c_long, c_void_p, create_unicode_buffer, wstring_at ) from ctypes.wintypes import ( - ATOM, BOOL, DWORD, HBRUSH, HGLOBAL, HICON, HINSTANCE, HMENU, HWND, INT, LPARAM, LPCWSTR, LPMSG, LPVOID, LPWSTR, + ATOM, HBRUSH, HICON, HINSTANCE, HWND, INT, LPARAM, LPCWSTR, LPWSTR, MSG, UINT, WPARAM ) - from win32con import CW_USEDEFAULT import win32gui import win32con + import win32api class WNDCLASS(Structure): """ @@ -101,37 +101,7 @@ class WNDCLASS(Structure): ('lpszClassName', LPCWSTR) ] - - CreateWindowExW = windll.user32.CreateWindowExW - CreateWindowExW.argtypes = [DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID] - CreateWindowExW.restype = HWND - RegisterClassW = windll.user32.RegisterClassW - RegisterClassW.argtypes = [POINTER(WNDCLASS)] - # DefWindowProcW - # Ref: - # LRESULT DefWindowProcW([in] HWND hWnd,[in] UINT Msg,[in] WPARAM wParam,[in] LPARAM lParam); - # As per example at - - prototype = WINFUNCTYPE(c_long, HWND, UINT, WPARAM, LPARAM) - paramflags = (1, "hWnd"), (1, "Msg"), (1, "wParam"), (1, "lParam") - DefWindowProcW = prototype(("DefWindowProcW", windll.user32), paramflags) - - SetForegroundWindow = windll.user32.SetForegroundWindow - - # - # NB: Despite 'BOOL' return type, it *can* be >0, 0 or -1, so is actually - # c_long - prototype = WINFUNCTYPE(c_long, LPMSG, HWND, UINT, UINT) - paramflags = (1, "lpMsg"), (1, "hWnd"), (1, "wMsgFilterMin"), (1, "wMsgFilterMax") - GetMessageW = prototype(("GetMessageW", windll.user32), paramflags) - TranslateMessage = windll.user32.TranslateMessage - DispatchMessageW = windll.user32.DispatchMessageW - PostThreadMessageW = windll.user32.PostThreadMessageW - SendMessageW = windll.user32.SendMessageW - SendMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM] - PostMessageW = windll.user32.PostMessageW - PostMessageW.argtypes = [HWND, UINT, WPARAM, LPARAM] # https://docs.microsoft.com/en-us/windows/win32/dataxchg/wm-dde-initiate WM_DDE_INITIATE = 0x03E0 @@ -148,12 +118,6 @@ class WNDCLASS(Structure): GlobalGetAtomNameW = windll.kernel32.GlobalGetAtomNameW GlobalGetAtomNameW.argtypes = [ATOM, LPWSTR, INT] GlobalGetAtomNameW.restype = UINT - GlobalLock = windll.kernel32.GlobalLock - GlobalLock.argtypes = [HGLOBAL] - GlobalLock.restype = LPVOID - GlobalUnlock = windll.kernel32.GlobalUnlock - GlobalUnlock.argtypes = [HGLOBAL] - GlobalUnlock.restype = BOOL # Windows Message handler stuff (IPC) # https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms633573(v=vs.85) @@ -171,7 +135,7 @@ def WndProc(hwnd: HWND, message: UINT, wParam: WPARAM, lParam: LPARAM) -> c_long if message != WM_DDE_INITIATE: # Not a DDE init message, bail and tell windows to do the default # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowproca?redirectedfrom=MSDN - return DefWindowProcW(hwnd, message, wParam, lParam) + return win32gui.DefWindowProc(hwnd, message, wParam, lParam) service = create_unicode_buffer(256) topic = create_unicode_buffer(256) @@ -196,7 +160,7 @@ def WndProc(hwnd: HWND, message: UINT, wParam: WPARAM, lParam: LPARAM) -> c_long if target_is_valid and topic_is_valid: # if everything is happy, send an acknowledgement of the DDE request - SendMessageW( + win32gui.SendMessage( wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, GlobalAddAtomW(appname), GlobalAddAtomW('System')) ) @@ -229,7 +193,7 @@ def close(self) -> None: thread = self.thread if thread: self.thread = None - PostThreadMessageW(thread.ident, win32con.WM_QUIT, 0, 0) + win32api.PostThreadMessage(thread.ident, win32con.WM_QUIT, 0, 0) thread.join() # Wait for it to quit def worker(self) -> None: @@ -239,24 +203,25 @@ def worker(self) -> None: wndclass.lpfnWndProc = WndProc wndclass.cbClsExtra = 0 wndclass.cbWndExtra = 0 - wndclass.hInstance = windll.kernel32.GetModuleHandleW(0) + wndclass.hInstance = win32gui.GetModuleHandle(0) wndclass.hIcon = None wndclass.hCursor = None wndclass.hbrBackground = None wndclass.lpszMenuName = None wndclass.lpszClassName = 'DDEServer' - if not RegisterClassW(byref(wndclass)): + if not win32gui.RegisterClass(byref(wndclass)): print('Failed to register Dynamic Data Exchange for cAPI') return # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw - hwnd = CreateWindowExW( + hwnd = win32gui.CreateWindowEx( 0, # dwExStyle wndclass.lpszClassName, # lpClassName "DDE Server", # lpWindowName 0, # dwStyle - CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, # X, Y, nWidth, nHeight + # X, Y, nWidth, nHeight + win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT, self.master.winfo_id(), # hWndParent # Don't use HWND_MESSAGE since the window won't get DDE broadcasts None, # hMenu wndclass.hInstance, # hInstance @@ -276,13 +241,13 @@ def worker(self) -> None: # # But it does actually work. Either getting a non-0 value and # entering the loop, or getting 0 and exiting it. - while GetMessageW(byref(msg), None, 0, 0) != 0: + while win32gui.GetMessage(byref(msg), None, 0, 0) != 0: logger.trace_if('frontier-auth.windows', f'DDE message of type: {msg.message}') if msg.message == WM_DDE_EXECUTE: # GlobalLock does some sort of "please dont move this?" # https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-globallock - args = wstring_at(GlobalLock(msg.lParam)).strip() - GlobalUnlock(msg.lParam) # Unlocks the GlobalLock-ed object + args = wstring_at(win32gui.GlobalLock(msg.lParam)).strip() + win32gui.GlobalUnlock(msg.lParam) # Unlocks the GlobalLock-ed object if args.lower().startswith('open("') and args.endswith('")'): logger.trace_if('frontier-auth.windows', f'args are: {args}') @@ -291,20 +256,20 @@ def worker(self) -> None: logger.debug(f'Message starts with {self.redirect}') self.event(url) - SetForegroundWindow(win32gui.GetParent(self.master.winfo_id())) # raise app window + win32gui.SetForegroundWindow(win32gui.GetParent(self.master.winfo_id())) # raise app window # Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE - PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam)) + win32gui.PostMessage(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0x80, msg.lParam)) else: # Send back a WM_DDE_ACK. this is _required_ with WM_DDE_EXECUTE - PostMessageW(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0, msg.lParam)) + win32gui.PostMessage(msg.wParam, WM_DDE_ACK, hwnd, PackDDElParam(WM_DDE_ACK, 0, msg.lParam)) elif msg.message == WM_DDE_TERMINATE: - PostMessageW(msg.wParam, WM_DDE_TERMINATE, hwnd, 0) + win32gui.PostMessage(msg.wParam, WM_DDE_TERMINATE, hwnd, 0) else: TranslateMessage(byref(msg)) # "Translates virtual key messages into character messages" ??? - DispatchMessageW(byref(msg)) + win32gui.DispatchMessage(byref(msg)) else: # Linux / Run from source diff --git a/stats.py b/stats.py index b8a6531a7..b0ba34b32 100644 --- a/stats.py +++ b/stats.py @@ -420,7 +420,7 @@ def __init__(self, parent: tk.Tk, data: dict[str, Any]) -> None: # Ensure fully on-screen if sys.platform == 'win32' and CalculatePopupWindowPosition: position = RECT() - win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()), position) + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), # - is evidently supported on the C side diff --git a/theme.py b/theme.py index 121bcb60e..6024bf4ff 100644 --- a/theme.py +++ b/theme.py @@ -422,12 +422,7 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 self.active = theme if sys.platform == 'win32': - GWL_STYLE = -16 # noqa: N806 # ctypes - WS_MAXIMIZEBOX = 0x00010000 # noqa: N806 # ctypes - # tk8.5.9/win/tkWinWm.c:342 - GWL_EXSTYLE = -20 # noqa: N806 # ctypes - WS_EX_APPWINDOW = 0x00040000 # noqa: N806 # ctypes - WS_EX_LAYERED = 0x00080000 # noqa: N806 # ctypes + import win32con # FIXME: Lose the "treat this like a boolean" bullshit if theme == self.THEME_DEFAULT: @@ -445,14 +440,14 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here hwnd = win32gui.GetParent(root.winfo_id()) - win32gui.SetWindowLong(hwnd, GWL_STYLE, - win32gui.GetWindowLong(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize + win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, + win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE) & ~win32con.WS_MAXIMIZEBOX) # disable maximize if theme == self.THEME_TRANSPARENT: - win32gui.SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar + win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW | win32con.WS_EX_LAYERED) # Add to taskbar else: - win32gui.SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar + win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW) # Add to taskbar root.deiconify() root.wait_visibility() # need main window to be displayed before returning From 85f5c3283dd50d35226990ab129d58fc4ebf4e66 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 11 Jun 2024 11:40:18 -0400 Subject: [PATCH 07/11] [1805] Shut UP flake8 --- prefs.py | 3 ++- theme.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/prefs.py b/prefs.py index 7e4465d11..e2afa2809 100644 --- a/prefs.py +++ b/prefs.py @@ -1090,7 +1090,8 @@ def displaypath(self, pathvar: tk.StringVar, entryfield: tk.Entry) -> None: for i in range(start, len(components)): try: if (not SHGetLocalizedName('\\'.join(components[:i+1]), buf, MAX_PATH, ctypes.byref(pidsRes)) and - win32api.LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, pidsRes.value, buf, MAX_PATH)): + win32api.LoadString(ctypes.WinDLL(expandvars(buf.value))._handle, + pidsRes.value, buf, MAX_PATH)): display.append(buf.value) else: diff --git a/theme.py b/theme.py index 6024bf4ff..b638bea21 100644 --- a/theme.py +++ b/theme.py @@ -441,10 +441,12 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 root.update_idletasks() # Size and windows styles get recalculated here hwnd = win32gui.GetParent(root.winfo_id()) win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, - win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE) & ~win32con.WS_MAXIMIZEBOX) # disable maximize + win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE) + & ~win32con.WS_MAXIMIZEBOX) # disable maximize if theme == self.THEME_TRANSPARENT: - win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW | win32con.WS_EX_LAYERED) # Add to taskbar + win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, + win32con.WS_EX_APPWINDOW | win32con.WS_EX_LAYERED) # Add to taskbar else: win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW) # Add to taskbar From 81a3b4fd5f0cbad5f23dcd5d93b20523733461ee Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 18 Jun 2024 09:51:36 -0400 Subject: [PATCH 08/11] [1805] Pull In Code from 1808 --- monitor.py | 75 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/monitor.py b/monitor.py index 8b55a2b92..85f56a4cf 100644 --- a/monitor.py +++ b/monitor.py @@ -15,7 +15,7 @@ import threading from calendar import timegm from collections import defaultdict -from os import SEEK_END, SEEK_SET, listdir +from os import SEEK_END, SEEK_SET, listdir, environ from os.path import basename, expanduser, getctime, isdir, join from time import gmtime, localtime, mktime, sleep, strftime, strptime, time from typing import TYPE_CHECKING, Any, BinaryIO, MutableMapping @@ -35,18 +35,16 @@ MAX_FCMATERIALS_DISCREPANCY = 5 # Timestamp difference in seconds if sys.platform == 'win32': - import ctypes - from ctypes.wintypes import BOOL, HWND, LPARAM + import win32process + import win32con + import win32security import win32gui import win32api - + import pywintypes from watchdog.events import FileSystemEventHandler, FileSystemEvent from watchdog.observers import Observer from watchdog.observers.api import BaseObserver - EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) - GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd - else: # Linux's inotify doesn't work over CIFS or NFS, so poll FileSystemEventHandler = object # dummy @@ -113,6 +111,13 @@ def __init__(self) -> None: # be >= for Live, and < for Legacy. self.live_galaxy_base_version = semantic_version.Version('4.0.0') + if sys.platform == 'win32': + # Get the SID of the user we're running as for later use in + # `game_running()` + self.user_sid, self.user_domain, self.user_type = win32security.LookupAccountName( + None, environ['USERNAME'] + ) + self.__init_state() def __init_state(self) -> None: @@ -2123,23 +2128,57 @@ def game_running(self) -> bool: # noqa: CCR001 if sys.platform == 'win32': def WindowTitle(h): # noqa: N802 if h: - length = win32gui.GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(length) - if win32gui.GetWindowText(h): - return buf.value + return win32gui.GetWindowText(h) return None - def callback(hWnd, lParam): # noqa: N803 - name = WindowTitle(hWnd) + def callback(hwnd, hwnds): # noqa: N803 + name = WindowTitle(hwnd) if name and name.startswith('Elite - Dangerous'): - handle = GetProcessHandleFromHwnd(hWnd) - if handle: # If GetProcessHandleFromHwnd succeeds then the app is already running as this user - win32api.CloseHandle(handle) - return False # stop enumeration + # We've found a window that *looks* like an ED game process, but now we need to check + # if it's owned by the current user. + + # Get the process_id of the window we found + # + thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd) + + # Use that to get a process handle + # + # The first arg can't simply be `0`, and `win32con.PROCESS_TERMINATE` works + handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, process_id) + if handle: + # We got the handle OK, now we need a token for it + process_token = win32security.OpenProcessToken(handle, win32security.TOKEN_QUERY) + # So we can use that to get information about the User + token_information, i = win32security.GetTokenInformation( + process_token, win32security.TokenUser + ) + # And lastly check if token_information, which should be a PySID object, matches + # that of the current user we looked up in `__init__()`. + if token_information == self.user_sid: + # This can be used to convert the token to username, domain name, and account type + # user, domain, name_use = win32security.LookupAccountSid(None, token_information) + hwnds.append(hwnd) + return False # Indicate window found, so stop iterating return True - return not win32gui.EnumWindows(EnumWindowsProc(callback), 0) + # Ref: + ed_windows: list[int] = [] + try: + win32gui.EnumWindows(callback, ed_windows) + + except pywintypes.error as e: + # Ref: + # Because False is returned in the callback to indicate "found the window, stop + # processing", this causes EnumWindows() to return `0`, which is generically + # treated as an error, so exception is raised. + # So, check the exception's .winerror, and ignore if `0`. + if e.winerror != 0: + logger.exception("EnumWindows exception:") + + bacon = bool(ed_windows) + print(bacon) + return bacon return False From f1df1c7da77b7bef9d045fdd6b69b7df94af1fcc Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 18 Jun 2024 12:20:08 -0400 Subject: [PATCH 09/11] [#610] Enable game_running on Linux --- monitor.py | 105 ++++++++++++++++------------------------------------- 1 file changed, 31 insertions(+), 74 deletions(-) diff --git a/monitor.py b/monitor.py index 85f56a4cf..ee5eaa992 100644 --- a/monitor.py +++ b/monitor.py @@ -15,10 +15,11 @@ import threading from calendar import timegm from collections import defaultdict -from os import SEEK_END, SEEK_SET, listdir, environ +from os import SEEK_END, SEEK_SET, listdir from os.path import basename, expanduser, getctime, isdir, join from time import gmtime, localtime, mktime, sleep, strftime, strptime, time from typing import TYPE_CHECKING, Any, BinaryIO, MutableMapping +import psutil import semantic_version import util_ships from config import config @@ -35,12 +36,6 @@ MAX_FCMATERIALS_DISCREPANCY = 5 # Timestamp difference in seconds if sys.platform == 'win32': - import win32process - import win32con - import win32security - import win32gui - import win32api - import pywintypes from watchdog.events import FileSystemEventHandler, FileSystemEvent from watchdog.observers import Observer from watchdog.observers.api import BaseObserver @@ -60,7 +55,8 @@ class EDLogs(FileSystemEventHandler): """Monitoring of Journal files.""" # Magic with FileSystemEventHandler can confuse type checkers when they do not have access to every import - _POLL = 1 # Polling is cheap, so do it often + _POLL = 1 # Polling while running is cheap, so do it often + _INACTIVE_POLL = 10 # Polling while not running isn't as cheap, so do it less often _RE_CANONICALISE = re.compile(r'\$(.+)_name;') _RE_CATEGORY = re.compile(r'\$MICRORESOURCE_CATEGORY_(.+);') _RE_LOGFILE = re.compile(r'^Journal(Alpha|Beta)?\.[0-9]{2,4}(-)?[0-9]{2}(-)?[0-9]{2}(T)?[0-9]{2}[0-9]{2}[0-9]{2}' @@ -90,6 +86,7 @@ def __init__(self) -> None: self.catching_up = False self.game_was_running = False # For generation of the "ShutDown" event + self.running_process = None # Context for journal handling self.version: str | None = None @@ -111,13 +108,6 @@ def __init__(self) -> None: # be >= for Live, and < for Legacy. self.live_galaxy_base_version = semantic_version.Version('4.0.0') - if sys.platform == 'win32': - # Get the SID of the user we're running as for later use in - # `game_running()` - self.user_sid, self.user_domain, self.user_type = win32security.LookupAccountName( - None, environ['USERNAME'] - ) - self.__init_state() def __init_state(self) -> None: @@ -469,7 +459,10 @@ def worker(self) -> None: # noqa: C901, CCR001 loghandle = open(logfile, 'rb', 0) # unbuffered log_pos = 0 - sleep(self._POLL) + if self.game_was_running: + sleep(self._POLL) + else: + sleep(self._INACTIVE_POLL) # Check whether we're still supposed to be running if threading.current_thread() != self.thread: @@ -2117,70 +2110,34 @@ def get_entry(self) -> MutableMapping[str, Any] | None: return entry - def game_running(self) -> bool: # noqa: CCR001 + def game_running(self) -> bool: """ Determine if the game is currently running. - TODO: Implement on Linux - :return: bool - True if the game is running. """ - if sys.platform == 'win32': - def WindowTitle(h): # noqa: N802 - if h: - return win32gui.GetWindowText(h) - return None - - def callback(hwnd, hwnds): # noqa: N803 - name = WindowTitle(hwnd) - if name and name.startswith('Elite - Dangerous'): - # We've found a window that *looks* like an ED game process, but now we need to check - # if it's owned by the current user. - - # Get the process_id of the window we found - # - thread_id, process_id = win32process.GetWindowThreadProcessId(hwnd) - - # Use that to get a process handle - # - # The first arg can't simply be `0`, and `win32con.PROCESS_TERMINATE` works - handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, False, process_id) - if handle: - # We got the handle OK, now we need a token for it - process_token = win32security.OpenProcessToken(handle, win32security.TOKEN_QUERY) - # So we can use that to get information about the User - token_information, i = win32security.GetTokenInformation( - process_token, win32security.TokenUser - ) - # And lastly check if token_information, which should be a PySID object, matches - # that of the current user we looked up in `__init__()`. - if token_information == self.user_sid: - # This can be used to convert the token to username, domain name, and account type - # user, domain, name_use = win32security.LookupAccountSid(None, token_information) - hwnds.append(hwnd) - return False # Indicate window found, so stop iterating - - return True - - # Ref: - ed_windows: list[int] = [] + if self.running_process: + p = self.running_process try: - win32gui.EnumWindows(callback, ed_windows) - - except pywintypes.error as e: - # Ref: - # Because False is returned in the callback to indicate "found the window, stop - # processing", this causes EnumWindows() to return `0`, which is generically - # treated as an error, so exception is raised. - # So, check the exception's .winerror, and ignore if `0`. - if e.winerror != 0: - logger.exception("EnumWindows exception:") - - bacon = bool(ed_windows) - print(bacon) - return bacon - - return False + with p.oneshot(): + if p.status() not in [psutil.STATUS_RUNNING, psutil.STATUS_SLEEPING]: + raise psutil.NoSuchProcess + except psutil.NoSuchProcess: + # Process likely expired + self.running_process = None + if not self.running_process: + edmc_process = psutil.Process() + edmc_user = edmc_process.username() + try: + for pid in psutil.pids(): + proc = psutil.Process(pid) + if 'EliteDangerous' in proc.name() and proc.username() == edmc_user: + self.running_process = proc + return True + except psutil.NoSuchProcess: + pass + return False + return bool(self.running_process) def ship(self, timestamped=True) -> MutableMapping[str, Any] | None: """ From d319dd03b56f25a9e7932dc9ddffe2c8a5fdb3a9 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 18 Jun 2024 12:22:37 -0400 Subject: [PATCH 10/11] [610] Add to Requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b278cce5d..3c44ddec7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ watchdog==4.0.0 simplesystray==0.1.0; sys_platform == 'win32' semantic-version==2.10.0 # For manipulating folder permissions and the like. -pywin32==306; sys_platform == 'win32' \ No newline at end of file +pywin32==306; sys_platform == 'win32' +psutil==5.9.8 From c0deeeb4cfdd0bccd23216647e7e13d8a08bbc60 Mon Sep 17 00:00:00 2001 From: David Sangrey Date: Tue, 18 Jun 2024 13:09:21 -0400 Subject: [PATCH 11/11] [1805] Simplify Window Text Call --- EDMarketConnector.py | 6 +----- hotkey/windows.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index ecc84e1bf..864462188 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -272,11 +272,7 @@ def handle_edmc_callback_or_foregrounding() -> None: # noqa: CCR001 def window_title(h: int) -> str | None: if h: - text_length = win32gui.GetWindowTextLength(h) + 1 - buf = create_unicode_buffer(text_length) - if win32gui.GetWindowText(h): - return buf.value - + return win32gui.GetWindowText(h) return None @WINFUNCTYPE(BOOL, HWND, LPARAM) diff --git a/hotkey/windows.py b/hotkey/windows.py index 95b1a83af..ddccc3330 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -47,11 +47,7 @@ def window_title(h) -> str: :return: Window title. """ if h: - title_length = win32gui.GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(title_length) - if win32gui.GetWindowText(h): - return buf.value - + return win32gui.GetWindowText(h) return ''