diff --git a/EDMarketConnector.py b/EDMarketConnector.py index a87d7cd0b..516a816dc 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -255,24 +255,16 @@ 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 - GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 + from ctypes import windll, create_unicode_buffer, WINFUNCTYPE + from ctypes.wintypes import BOOL, HWND, LPARAM + import win32gui + import win32api + import win32con - SW_RESTORE = 9 # noqa: N806 - SetForegroundWindow = windll.user32.SetForegroundWindow # noqa: N806 - ShowWindow = windll.user32.ShowWindow # noqa: N806 + GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806 ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806 COINIT_MULTITHREADED = 0 # noqa: N806,F841 @@ -280,16 +272,9 @@ 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 - buf = create_unicode_buffer(text_length) - if GetWindowText(h, buf, text_length): - return buf.value - + return win32gui.GetWindowText(h) return None @WINFUNCTYPE(BOOL, HWND, LPARAM) @@ -311,7 +296,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): @@ -319,12 +304,12 @@ 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) - ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE) + win32gui.ShowWindow(window_handle, win32con.SW_RESTORE) + win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE) else: - ShowWindowAsync(window_handle, SW_RESTORE) - SetForegroundWindow(window_handle) + ShowWindowAsync(window_handle, win32con.SW_RESTORE) + win32gui.SetForegroundWindow(window_handle) return False # Indicate window found, so stop iterating @@ -336,7 +321,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.""" @@ -703,13 +688,14 @@ def open_window(systray: 'SysTrayIcon', *args) -> 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/config/windows.py b/config/windows.py index c0d25b71c..550b0e4a7 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,6 +31,7 @@ class WinConfig(AbstractConfig): def __init__(self) -> None: super().__init__() + REGISTRY_SUBKEY = r'Software\Marginal\EDMarketConnector' # noqa: N806 create_key_defaults = functools.partial( winreg.CreateKeyEx, @@ -63,7 +46,8 @@ def __init__(self) -> None: logger.exception('Could not create required registry keys') raise - 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.default_plugin_dir_path = self.app_dir_path / 'plugins' @@ -83,12 +67,12 @@ 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 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..ddccc3330 100644 --- a/hotkey/windows.py +++ b/hotkey/windows.py @@ -8,7 +8,11 @@ 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, HWND, BOOL, UINT +import pywintypes +import win32api +import win32gui +import win32con from config import config from EDMCLogging import get_main_logger from hotkey import AbstractHotkeyMgr @@ -17,53 +21,22 @@ logger = get_main_logger() -RegisterHotKey = ctypes.windll.user32.RegisterHotKey -UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey -MOD_ALT = 0x0001 -MOD_CONTROL = 0x0002 -MOD_SHIFT = 0x0004 -MOD_WIN = 0x0008 -MOD_NOREPEAT = 0x4000 +UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey # TODO: Coming Soon +UnregisterHotKey.argtypes = [HWND, ctypes.c_int] +UnregisterHotKey.restype = BOOL -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 -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 +MOD_NOREPEAT = 0x4000 +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 -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,11 +47,7 @@ def window_title(h) -> str: :return: Window title. """ if h: - title_length = GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(title_length) - if GetWindowText(h, buf, title_length): - return buf.value - + return win32gui.GetWindowText(h) return '' @@ -138,6 +107,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 @@ -197,7 +167,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, win32con.WM_QUIT, 0, 0) logger.debug('Joining thread') thread.join() # Wait for it to unregister hotkey and quit @@ -210,8 +180,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 +191,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: + if msg.message == win32con.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 +208,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 +224,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) @@ -277,40 +251,42 @@ 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(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 - 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: """ @@ -321,32 +297,32 @@ 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] else: - c = MapVirtualKey(keycode, 2) # printable ? + c = win32api.MapVirtualKey(keycode, MAPVK_VK_TO_CHAR) if not c: # oops not printable text += '⁈' @@ -361,9 +337,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/l10n.py b/l10n.py index ce1ab6a71..6557169ed 100755 --- a/l10n.py +++ b/l10n.py @@ -49,7 +49,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 cd0285824..d0a79716f 100644 --- a/monitor.py +++ b/monitor.py @@ -18,6 +18,7 @@ from os import SEEK_END, SEEK_SET, listdir 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, appname, appversion @@ -35,26 +36,10 @@ MAX_FCMATERIALS_DISCREPANCY = 5 # Timestamp difference in seconds if sys.platform == 'win32': - import ctypes - from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR - 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: # Linux's inotify doesn't work over CIFS or NFS, so poll FileSystemEventHandler = object # dummy @@ -70,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}' @@ -100,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 @@ -474,7 +461,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: @@ -2150,36 +2140,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: - length = GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(length) - if GetWindowText(h, buf, length): - return buf.value - return None - - def callback(hWnd, lParam): # 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 - CloseHandle(handle) - return False # stop enumeration - - return True - - return not EnumWindows(EnumWindowsProc(callback), 0) - - return False + if self.running_process: + p = self.running_process + try: + 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: """ diff --git a/prefs.py b/prefs.py index 69e6a8402..29e1e7b13 100644 --- a/prefs.py +++ b/prefs.py @@ -186,7 +186,9 @@ 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 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' @@ -201,6 +203,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( @@ -217,17 +221,9 @@ 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)] - LoadString = ctypes.windll.user32.LoadStringW - LoadString.argtypes = [HINSTANCE, UINT, LPWSTR, ctypes.c_int] - class PreferencesDialog(tk.Toplevel): """The EDMC preferences dialog.""" @@ -313,7 +309,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())) if CalculatePopupWindowPosition( POINT(parent.winfo_rootx(), parent.winfo_rooty()), SIZE(position.right - position.left, position.bottom - position.top), # type: ignore @@ -1110,7 +1106,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 - 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 1fb885957..a0f02f158 100644 --- a/protocol.py +++ b/protocol.py @@ -69,13 +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 ) + import win32gui + import win32con + import win32api class WNDCLASS(Structure): """ @@ -98,41 +101,8 @@ 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] - 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) - - GetParent = windll.user32.GetParent - 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] - - WM_QUIT = 0x0012 + # https://docs.microsoft.com/en-us/windows/win32/dataxchg/wm-dde-initiate WM_DDE_INITIATE = 0x03E0 WM_DDE_TERMINATE = 0x03E1 @@ -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, 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(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/requirements-dev.txt b/requirements-dev.txt index fe1459932..49d215623 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -42,8 +42,6 @@ pytest==8.2.2 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 877950654..23e90a95d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,6 @@ pillow==10.3.0 watchdog==4.0.1 simplesystray==0.1.0; sys_platform == 'win32' semantic-version==2.10.0 +# For manipulating folder permissions and the like. +pywin32==306; sys_platform == 'win32' +psutil==5.9.8 diff --git a/stats.py b/stats.py index db61f7892..b0ba34b32 100644 --- a/stats.py +++ b/stats.py @@ -25,17 +25,15 @@ if sys.platform == 'win32': import ctypes - from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT + from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL + 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)] + CalculatePopupWindowPosition.restype = BOOL except Exception: # Not supported under Wine 4.0 CalculatePopupWindowPosition = None # type: ignore @@ -243,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'))) @@ -253,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']), @@ -302,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: @@ -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())) 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 c00189129..5eed9fe14 100644 --- a/theme.py +++ b/theme.py @@ -33,6 +33,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 @@ -421,14 +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 - GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes - SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes + import win32con # FIXME: Lose the "treat this like a boolean" bullshit if theme == self.THEME_DEFAULT: @@ -445,14 +439,17 @@ 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, win32con.GWL_STYLE, + win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE) + & ~win32con.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, win32con.GWL_EXSTYLE, + win32con.WS_EX_APPWINDOW | win32con.WS_EX_LAYERED) # Add to taskbar else: - SetWindowLongW(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