Auto-sync: 2026-02-16 21:44
This commit is contained in:
parent
5e44355e52
commit
70b7e9b237
|
|
@ -7,9 +7,11 @@ Windows-specific implementation using ctypes (no external dependencies).
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
from typing import Optional, Tuple, Dict, Any
|
from typing import Optional, Tuple, Dict, Any
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
# Platform detection
|
# Platform detection
|
||||||
IS_WINDOWS = sys.platform == 'win32'
|
IS_WINDOWS = sys.platform == 'win32'
|
||||||
|
|
@ -26,6 +28,13 @@ if IS_WINDOWS:
|
||||||
else:
|
else:
|
||||||
WINDOWS_AVAILABLE = False
|
WINDOWS_AVAILABLE = False
|
||||||
|
|
||||||
|
# Optional psutil import for fast process detection
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
PSUTIL_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PSUTIL_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WindowInfo:
|
class WindowInfo:
|
||||||
|
|
@ -50,6 +59,41 @@ class ProcessInfo:
|
||||||
cpu_percent: Optional[float]
|
cpu_percent: Optional[float]
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowCache:
|
||||||
|
"""Thread-safe cache for window information."""
|
||||||
|
|
||||||
|
def __init__(self, ttl_seconds: float = 3.0):
|
||||||
|
self._cache: Dict[str, Any] = {}
|
||||||
|
self._timestamps: Dict[str, float] = {}
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._ttl = ttl_seconds
|
||||||
|
|
||||||
|
def get(self, key: str) -> Any:
|
||||||
|
"""Get cached value if not expired."""
|
||||||
|
with self._lock:
|
||||||
|
if key not in self._cache:
|
||||||
|
return None
|
||||||
|
timestamp = self._timestamps.get(key, 0)
|
||||||
|
if time.time() - timestamp > self._ttl:
|
||||||
|
# Expired
|
||||||
|
del self._cache[key]
|
||||||
|
del self._timestamps[key]
|
||||||
|
return None
|
||||||
|
return self._cache[key]
|
||||||
|
|
||||||
|
def set(self, key: str, value: Any):
|
||||||
|
"""Set cached value."""
|
||||||
|
with self._lock:
|
||||||
|
self._cache[key] = value
|
||||||
|
self._timestamps[key] = time.time()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clear all cached values."""
|
||||||
|
with self._lock:
|
||||||
|
self._cache.clear()
|
||||||
|
self._timestamps.clear()
|
||||||
|
|
||||||
|
|
||||||
class WindowManager:
|
class WindowManager:
|
||||||
"""
|
"""
|
||||||
Singleton Window Manager for EU-Utility.
|
Singleton Window Manager for EU-Utility.
|
||||||
|
|
@ -59,6 +103,7 @@ class WindowManager:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
# Window search criteria - look for actual game client, not the loader
|
# Window search criteria - look for actual game client, not the loader
|
||||||
EU_WINDOW_TITLE = "Entropia Universe Client" # Matches "Entropia Universe Client (64-bit) PlanetName"
|
EU_WINDOW_TITLE = "Entropia Universe Client" # Matches "Entropia Universe Client (64-bit) PlanetName"
|
||||||
|
|
@ -66,32 +111,49 @@ class WindowManager:
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
with cls._lock:
|
||||||
cls._instance._initialized = False
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._initialized = False
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Core state (lazy initialization)
|
||||||
self._window_handle: Optional[int] = None
|
self._window_handle: Optional[int] = None
|
||||||
self._window_info: Optional[WindowInfo] = None
|
self._window_info: Optional[WindowInfo] = None
|
||||||
self._process_info: Optional[ProcessInfo] = None
|
self._process_info: Optional[ProcessInfo] = None
|
||||||
self._last_update: float = 0
|
self._last_update: float = 0
|
||||||
self._update_interval: float = 1.0 # seconds
|
self._update_interval: float = 3.0 # Increased to 3 seconds
|
||||||
|
|
||||||
# Windows API constants
|
# Caching
|
||||||
if IS_WINDOWS and WINDOWS_AVAILABLE:
|
self._cache = _WindowCache(ttl_seconds=3.0)
|
||||||
self._setup_windows_api()
|
self._window_handle_cache: Optional[int] = None
|
||||||
|
self._window_handle_cache_time: float = 0
|
||||||
|
self._window_handle_ttl: float = 5.0 # Cache window handle for 5 seconds
|
||||||
|
|
||||||
|
# Windows API (lazy initialization)
|
||||||
|
self._windows_api_ready = False
|
||||||
|
self._user32 = None
|
||||||
|
self._kernel32 = None
|
||||||
|
self._psutil_initialized = False
|
||||||
|
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
self._available = IS_WINDOWS and WINDOWS_AVAILABLE
|
self._available = IS_WINDOWS and WINDOWS_AVAILABLE
|
||||||
|
|
||||||
if not self._available:
|
if not self._available:
|
||||||
print("[WindowManager] Windows API not available - running in limited mode")
|
pass # Don't print here, print on first actual use if needed
|
||||||
|
|
||||||
|
def _ensure_windows_api(self):
|
||||||
|
"""Lazy initialization of Windows API."""
|
||||||
|
if self._windows_api_ready:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not IS_WINDOWS or not WINDOWS_AVAILABLE:
|
||||||
|
return
|
||||||
|
|
||||||
def _setup_windows_api(self):
|
|
||||||
"""Setup Windows API constants and functions."""
|
|
||||||
# Window styles
|
# Window styles
|
||||||
self.GWL_STYLE = -16
|
self.GWL_STYLE = -16
|
||||||
self.GWL_EXSTYLE = -20
|
self.GWL_EXSTYLE = -20
|
||||||
|
|
@ -99,8 +161,8 @@ class WindowManager:
|
||||||
self.WS_MINIMIZE = 0x20000000
|
self.WS_MINIMIZE = 0x20000000
|
||||||
|
|
||||||
# Load user32.dll functions
|
# Load user32.dll functions
|
||||||
self.user32 = ctypes.windll.user32
|
self._user32 = ctypes.windll.user32
|
||||||
self.kernel32 = ctypes.windll.kernel32
|
self._kernel32 = ctypes.windll.kernel32
|
||||||
|
|
||||||
# EnumWindows callback type
|
# EnumWindows callback type
|
||||||
self.EnumWindowsProc = ctypes.WINFUNCTYPE(
|
self.EnumWindowsProc = ctypes.WINFUNCTYPE(
|
||||||
|
|
@ -109,12 +171,70 @@ class WindowManager:
|
||||||
wintypes.LPARAM
|
wintypes.LPARAM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._windows_api_ready = True
|
||||||
|
|
||||||
|
def _ensure_psutil(self):
|
||||||
|
"""Lazy initialization of psutil-based process detection."""
|
||||||
|
if self._psutil_initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
if PSUTIL_AVAILABLE:
|
||||||
|
# Pre-cache EU process info for fast lookups
|
||||||
|
self._refresh_eu_process_cache()
|
||||||
|
|
||||||
|
self._psutil_initialized = True
|
||||||
|
|
||||||
|
def _refresh_eu_process_cache(self):
|
||||||
|
"""Refresh cached EU process information."""
|
||||||
|
if not PSUTIL_AVAILABLE:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'exe']):
|
||||||
|
try:
|
||||||
|
proc_name = proc.info['name'].lower()
|
||||||
|
if any(eu_name.lower() == proc_name for eu_name in self.EU_PROCESS_NAMES):
|
||||||
|
self._cache.set('eu_pid', proc.info['pid'])
|
||||||
|
self._cache.set('eu_process_name', proc.info['name'])
|
||||||
|
break
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ========== Public API ==========
|
# ========== Public API ==========
|
||||||
|
|
||||||
def is_available(self) -> bool:
|
def is_available(self) -> bool:
|
||||||
"""Check if window manager is fully functional."""
|
"""Check if window manager is fully functional."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
|
def find_eu_window_cached(self) -> Optional[WindowInfo]:
|
||||||
|
"""
|
||||||
|
Find EU window with aggressive caching for performance.
|
||||||
|
Returns cached result if available and recent.
|
||||||
|
"""
|
||||||
|
if not self._available:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check cache first
|
||||||
|
cached = self._cache.get('eu_window_info')
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
# Check if we have a cached window handle that's still valid
|
||||||
|
if self._window_handle_cache:
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self._window_handle_cache_time < self._window_handle_ttl:
|
||||||
|
# Validate handle is still valid
|
||||||
|
if self._is_window_valid(self._window_handle_cache):
|
||||||
|
info = self._get_window_info_fast(self._window_handle_cache)
|
||||||
|
if info:
|
||||||
|
self._cache.set('eu_window_info', info)
|
||||||
|
return info
|
||||||
|
|
||||||
|
# Fall back to full search
|
||||||
|
return self.find_eu_window()
|
||||||
|
|
||||||
def find_eu_window(self) -> Optional[WindowInfo]:
|
def find_eu_window(self) -> Optional[WindowInfo]:
|
||||||
"""
|
"""
|
||||||
Find the Entropia Universe game window.
|
Find the Entropia Universe game window.
|
||||||
|
|
@ -125,24 +245,92 @@ class WindowManager:
|
||||||
if not self._available:
|
if not self._available:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Try by window title first
|
self._ensure_windows_api()
|
||||||
hwnd = self._find_window_by_title(self.EU_WINDOW_TITLE)
|
|
||||||
|
# Try by window title first (faster)
|
||||||
|
hwnd = self._find_window_by_title_fast(self.EU_WINDOW_TITLE)
|
||||||
|
|
||||||
if hwnd:
|
if hwnd:
|
||||||
self._window_handle = hwnd
|
self._window_handle = hwnd
|
||||||
self._window_info = self._get_window_info(hwnd)
|
self._window_handle_cache = hwnd
|
||||||
|
self._window_handle_cache_time = time.time()
|
||||||
|
self._window_info = self._get_window_info_fast(hwnd)
|
||||||
|
if self._window_info:
|
||||||
|
self._cache.set('eu_window_info', self._window_info)
|
||||||
return self._window_info
|
return self._window_info
|
||||||
|
|
||||||
# Try by process name
|
# Try by process name if title fails
|
||||||
for proc_name in self.EU_PROCESS_NAMES:
|
for proc_name in self.EU_PROCESS_NAMES:
|
||||||
hwnd = self._find_window_by_process(proc_name)
|
hwnd = self._find_window_by_process_fast(proc_name)
|
||||||
if hwnd:
|
if hwnd:
|
||||||
self._window_handle = hwnd
|
self._window_handle = hwnd
|
||||||
self._window_info = self._get_window_info(hwnd)
|
self._window_handle_cache = hwnd
|
||||||
|
self._window_handle_cache_time = time.time()
|
||||||
|
self._window_info = self._get_window_info_fast(hwnd)
|
||||||
|
if self._window_info:
|
||||||
|
self._cache.set('eu_window_info', self._window_info)
|
||||||
return self._window_info
|
return self._window_info
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def is_eu_focused_fast(self) -> bool:
|
||||||
|
"""
|
||||||
|
Ultra-fast check if EU is focused using psutil + cached window handle.
|
||||||
|
Target: <10ms execution time.
|
||||||
|
"""
|
||||||
|
if not self._available:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._ensure_windows_api()
|
||||||
|
self._ensure_psutil()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fast path: Check if our cached window is still focused
|
||||||
|
if self._window_handle_cache:
|
||||||
|
# Quick foreground window check (no window enumeration)
|
||||||
|
foreground_hwnd = self._user32.GetForegroundWindow()
|
||||||
|
if foreground_hwnd == self._window_handle_cache:
|
||||||
|
# Verify window is still valid
|
||||||
|
if self._is_window_valid(self._window_handle_cache):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Cache is stale, clear it
|
||||||
|
self._window_handle_cache = None
|
||||||
|
|
||||||
|
# Medium path: Use psutil to find EU process and check its windows
|
||||||
|
if PSUTIL_AVAILABLE:
|
||||||
|
eu_pid = self._cache.get('eu_pid')
|
||||||
|
if eu_pid:
|
||||||
|
try:
|
||||||
|
proc = psutil.Process(eu_pid)
|
||||||
|
# Check if process still exists
|
||||||
|
if proc.is_running():
|
||||||
|
# Get foreground window
|
||||||
|
foreground_hwnd = self._user32.GetForegroundWindow()
|
||||||
|
# Get PID of foreground window
|
||||||
|
fg_pid = wintypes.DWORD()
|
||||||
|
self._user32.GetWindowThreadProcessId(foreground_hwnd, ctypes.byref(fg_pid))
|
||||||
|
if fg_pid.value == eu_pid:
|
||||||
|
# Found it! Cache the handle
|
||||||
|
self._window_handle_cache = foreground_hwnd
|
||||||
|
self._window_handle_cache_time = time.time()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Process died, refresh cache
|
||||||
|
self._refresh_eu_process_cache()
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
self._refresh_eu_process_cache()
|
||||||
|
|
||||||
|
# Slow path: Find window and check focus
|
||||||
|
window = self.find_eu_window_cached()
|
||||||
|
if window:
|
||||||
|
return window.is_focused
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def get_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
|
def get_window_rect(self) -> Optional[Tuple[int, int, int, int]]:
|
||||||
"""
|
"""
|
||||||
Get the window rectangle (left, top, right, bottom).
|
Get the window rectangle (left, top, right, bottom).
|
||||||
|
|
@ -167,18 +355,7 @@ class WindowManager:
|
||||||
Returns:
|
Returns:
|
||||||
True if EU window is active, False otherwise
|
True if EU window is active, False otherwise
|
||||||
"""
|
"""
|
||||||
if not self._available:
|
return self.is_eu_focused_fast()
|
||||||
return False
|
|
||||||
|
|
||||||
if not self._window_handle:
|
|
||||||
self.find_eu_window()
|
|
||||||
|
|
||||||
if not self._window_handle:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Get foreground window
|
|
||||||
foreground_hwnd = self.user32.GetForegroundWindow()
|
|
||||||
return foreground_hwnd == self._window_handle
|
|
||||||
|
|
||||||
def is_window_visible(self) -> bool:
|
def is_window_visible(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -207,6 +384,8 @@ class WindowManager:
|
||||||
if not self._available:
|
if not self._available:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self._ensure_windows_api()
|
||||||
|
|
||||||
if not self._window_handle:
|
if not self._window_handle:
|
||||||
self.find_eu_window()
|
self.find_eu_window()
|
||||||
|
|
||||||
|
|
@ -215,13 +394,13 @@ class WindowManager:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Show window if minimized
|
# Show window if minimized
|
||||||
self.user32.ShowWindow(self._window_handle, 9) # SW_RESTORE = 9
|
self._user32.ShowWindow(self._window_handle, 9) # SW_RESTORE = 9
|
||||||
|
|
||||||
# Bring to front
|
# Bring to front
|
||||||
result = self.user32.SetForegroundWindow(self._window_handle)
|
result = self._user32.SetForegroundWindow(self._window_handle)
|
||||||
|
|
||||||
# Force window to top
|
# Force window to top
|
||||||
self.user32.SetWindowPos(
|
self._user32.SetWindowPos(
|
||||||
self._window_handle,
|
self._window_handle,
|
||||||
-1, # HWND_TOPMOST
|
-1, # HWND_TOPMOST
|
||||||
0, 0, 0, 0,
|
0, 0, 0, 0,
|
||||||
|
|
@ -229,7 +408,7 @@ class WindowManager:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove topmost flag but keep on top
|
# Remove topmost flag but keep on top
|
||||||
self.user32.SetWindowPos(
|
self._user32.SetWindowPos(
|
||||||
self._window_handle,
|
self._window_handle,
|
||||||
-2, # HWND_NOTOPMOST
|
-2, # HWND_NOTOPMOST
|
||||||
0, 0, 0, 0,
|
0, 0, 0, 0,
|
||||||
|
|
@ -238,7 +417,6 @@ class WindowManager:
|
||||||
|
|
||||||
return bool(result)
|
return bool(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WindowManager] Failed to bring window to front: {e}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_eu_process_info(self) -> Optional[ProcessInfo]:
|
def get_eu_process_info(self) -> Optional[ProcessInfo]:
|
||||||
|
|
@ -251,6 +429,13 @@ class WindowManager:
|
||||||
if not self._available:
|
if not self._available:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
self._ensure_windows_api()
|
||||||
|
|
||||||
|
# Try cache first
|
||||||
|
cached = self._cache.get('eu_process_info')
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
|
||||||
if not self._window_handle:
|
if not self._window_handle:
|
||||||
self.find_eu_window()
|
self.find_eu_window()
|
||||||
|
|
||||||
|
|
@ -259,20 +444,24 @@ class WindowManager:
|
||||||
|
|
||||||
# Get PID from window
|
# Get PID from window
|
||||||
pid = wintypes.DWORD()
|
pid = wintypes.DWORD()
|
||||||
self.user32.GetWindowThreadProcessId(self._window_handle, ctypes.byref(pid))
|
self._user32.GetWindowThreadProcessId(self._window_handle, ctypes.byref(pid))
|
||||||
|
|
||||||
if pid.value == 0:
|
if pid.value == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self._process_info = self._get_process_info(pid.value)
|
self._process_info = self._get_process_info_fast(pid.value)
|
||||||
|
if self._process_info:
|
||||||
|
self._cache.set('eu_process_info', self._process_info)
|
||||||
return self._process_info
|
return self._process_info
|
||||||
|
|
||||||
def get_window_handle(self) -> Optional[int]:
|
def get_window_handle(self) -> Optional[int]:
|
||||||
"""Get the current window handle."""
|
"""Get the current window handle."""
|
||||||
return self._window_handle
|
return self._window_handle_cache or self._window_handle
|
||||||
|
|
||||||
def refresh(self) -> Optional[WindowInfo]:
|
def refresh(self) -> Optional[WindowInfo]:
|
||||||
"""Force refresh of window information."""
|
"""Force refresh of window information."""
|
||||||
|
self._cache.clear()
|
||||||
|
self._window_handle_cache = None
|
||||||
self._last_update = 0
|
self._last_update = 0
|
||||||
return self.find_eu_window()
|
return self.find_eu_window()
|
||||||
|
|
||||||
|
|
@ -282,118 +471,179 @@ class WindowManager:
|
||||||
"""Update cached window info if needed."""
|
"""Update cached window info if needed."""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if current_time - self._last_update > self._update_interval:
|
if current_time - self._last_update > self._update_interval:
|
||||||
if self._window_handle:
|
if self._window_handle_cache and self._is_window_valid(self._window_handle_cache):
|
||||||
self._window_info = self._get_window_info(self._window_handle)
|
self._window_info = self._get_window_info_fast(self._window_handle_cache)
|
||||||
else:
|
else:
|
||||||
self.find_eu_window()
|
self.find_eu_window()
|
||||||
self._last_update = current_time
|
self._last_update = current_time
|
||||||
|
|
||||||
def _find_window_by_title(self, title: str) -> Optional[int]:
|
def _is_window_valid(self, hwnd: int) -> bool:
|
||||||
"""Find window by title (partial match) - with timeout protection."""
|
"""Check if a window handle is still valid (fast)."""
|
||||||
|
if not self._available or not hwnd:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return bool(self._user32.IsWindow(hwnd))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _find_window_by_title_fast(self, title: str) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Find window by title with strict timeout and limits.
|
||||||
|
Target: <50ms execution time.
|
||||||
|
"""
|
||||||
found_hwnd = [None]
|
found_hwnd = [None]
|
||||||
start_time = time.time()
|
start_time = time.perf_counter()
|
||||||
window_count = [0]
|
window_count = [0]
|
||||||
MAX_WINDOWS = 500 # Limit windows to check
|
MAX_WINDOWS = 200 # Reduced from 500
|
||||||
MAX_TIME = 0.1 # 100ms timeout
|
MAX_TIME = 0.025 # 25ms timeout (reduced from 100ms)
|
||||||
|
|
||||||
|
title_lower = title.lower()
|
||||||
|
|
||||||
def callback(hwnd, extra):
|
def callback(hwnd, extra):
|
||||||
# Check timeout
|
# Check timeout first
|
||||||
if time.time() - start_time > MAX_TIME:
|
if time.perf_counter() - start_time > MAX_TIME:
|
||||||
return False # Stop enumeration - timeout
|
return False
|
||||||
|
|
||||||
# Check window limit
|
|
||||||
window_count[0] += 1
|
window_count[0] += 1
|
||||||
if window_count[0] > MAX_WINDOWS:
|
if window_count[0] > MAX_WINDOWS:
|
||||||
return False # Stop enumeration - too many windows
|
return False
|
||||||
|
|
||||||
if not self.user32.IsWindowVisible(hwnd):
|
# Quick visibility check
|
||||||
|
if not self._user32.IsWindowVisible(hwnd):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Quick check - skip windows with no title
|
# Get window text with smaller buffer
|
||||||
text = ctypes.create_unicode_buffer(256)
|
text = ctypes.create_unicode_buffer(128) # Reduced from 256
|
||||||
self.user32.GetWindowTextW(hwnd, text, 256)
|
length = self._user32.GetWindowTextW(hwnd, text, 128)
|
||||||
window_title = text.value
|
|
||||||
|
|
||||||
if not window_title: # Skip empty titles
|
if length == 0: # Skip empty titles quickly
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Fast case-insensitive check
|
# Fast lowercase comparison
|
||||||
if title.lower() in window_title.lower():
|
if title_lower in text.value.lower():
|
||||||
found_hwnd[0] = hwnd
|
found_hwnd[0] = hwnd
|
||||||
return False # Stop enumeration - found!
|
return False # Stop enumeration - found!
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
proc = self.EnumWindowsProc(callback)
|
proc = self.EnumWindowsProc(callback)
|
||||||
self.user32.EnumWindows(proc, 0)
|
self._user32.EnumWindows(proc, 0)
|
||||||
|
|
||||||
return found_hwnd[0]
|
return found_hwnd[0]
|
||||||
|
|
||||||
def _find_window_by_process(self, process_name: str) -> Optional[int]:
|
def _find_window_by_process_fast(self, process_name: str) -> Optional[int]:
|
||||||
"""Find window by process name - with timeout protection."""
|
"""
|
||||||
|
Find window by process name with strict timeout.
|
||||||
|
Uses psutil if available for faster process detection.
|
||||||
|
"""
|
||||||
|
# Try psutil first for faster process-based lookup
|
||||||
|
if PSUTIL_AVAILABLE:
|
||||||
|
try:
|
||||||
|
for proc in psutil.process_iter(['pid', 'name']):
|
||||||
|
try:
|
||||||
|
if process_name.lower() in proc.info['name'].lower():
|
||||||
|
# Found process, now find its window
|
||||||
|
target_pid = proc.info['pid']
|
||||||
|
# Look for window with this PID
|
||||||
|
found = self._find_window_by_pid(target_pid)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fall back to enumeration
|
||||||
found_hwnd = [None]
|
found_hwnd = [None]
|
||||||
start_time = time.time()
|
start_time = time.perf_counter()
|
||||||
window_count = [0]
|
window_count = [0]
|
||||||
MAX_WINDOWS = 300 # Lower limit for process check (slower)
|
MAX_WINDOWS = 150 # Reduced from 300
|
||||||
MAX_TIME = 0.15 # 150ms timeout
|
MAX_TIME = 0.030 # 30ms timeout (reduced from 150ms)
|
||||||
|
|
||||||
|
process_name_lower = process_name.lower()
|
||||||
|
|
||||||
def callback(hwnd, extra):
|
def callback(hwnd, extra):
|
||||||
# Check timeout
|
if time.perf_counter() - start_time > MAX_TIME:
|
||||||
if time.time() - start_time > MAX_TIME:
|
return False
|
||||||
return False # Stop enumeration - timeout
|
|
||||||
|
|
||||||
# Check window limit
|
|
||||||
window_count[0] += 1
|
window_count[0] += 1
|
||||||
if window_count[0] > MAX_WINDOWS:
|
if window_count[0] > MAX_WINDOWS:
|
||||||
return False # Stop enumeration - too many windows
|
return False
|
||||||
|
|
||||||
if not self.user32.IsWindowVisible(hwnd):
|
if not self._user32.IsWindowVisible(hwnd):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Get process ID
|
# Get process ID
|
||||||
pid = wintypes.DWORD()
|
pid = wintypes.DWORD()
|
||||||
self.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
self._user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||||
|
|
||||||
if pid.value == 0:
|
if pid.value == 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check process name (this is slower, so we limit more)
|
# Use cached process info if available
|
||||||
try:
|
cache_key = f"proc_{pid.value}"
|
||||||
proc_info = self._get_process_info(pid.value)
|
proc_info = self._cache.get(cache_key)
|
||||||
if proc_info and process_name.lower() in proc_info.name.lower():
|
|
||||||
found_hwnd[0] = hwnd
|
if proc_info is None:
|
||||||
return False # Stop enumeration - found!
|
# Get process info (cached)
|
||||||
except Exception:
|
proc_info = self._get_process_info_fast(pid.value)
|
||||||
pass # Skip on error
|
if proc_info:
|
||||||
|
self._cache.set(cache_key, proc_info)
|
||||||
|
|
||||||
|
if proc_info and process_name_lower in proc_info.name.lower():
|
||||||
|
found_hwnd[0] = hwnd
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
proc = self.EnumWindowsProc(callback)
|
proc = self.EnumWindowsProc(callback)
|
||||||
self.user32.EnumWindows(proc, 0)
|
self._user32.EnumWindows(proc, 0)
|
||||||
|
|
||||||
return found_hwnd[0]
|
return found_hwnd[0]
|
||||||
|
|
||||||
def _get_window_info(self, hwnd: int) -> Optional[WindowInfo]:
|
def _find_window_by_pid(self, target_pid: int) -> Optional[int]:
|
||||||
"""Get detailed information about a window."""
|
"""Find a window by its process ID."""
|
||||||
|
found_hwnd = [None]
|
||||||
|
|
||||||
|
def callback(hwnd, extra):
|
||||||
|
if not self._user32.IsWindowVisible(hwnd):
|
||||||
|
return True
|
||||||
|
|
||||||
|
pid = wintypes.DWORD()
|
||||||
|
self._user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||||
|
|
||||||
|
if pid.value == target_pid:
|
||||||
|
found_hwnd[0] = hwnd
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
proc = self.EnumWindowsProc(callback)
|
||||||
|
self._user32.EnumWindows(proc, 0)
|
||||||
|
|
||||||
|
return found_hwnd[0]
|
||||||
|
|
||||||
|
def _get_window_info_fast(self, hwnd: int) -> Optional[WindowInfo]:
|
||||||
|
"""Get window info with minimal overhead and timeout protection."""
|
||||||
try:
|
try:
|
||||||
# Get window rect
|
# Get window rect
|
||||||
rect = wintypes.RECT()
|
rect = wintypes.RECT()
|
||||||
if not self.user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
if not self._user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get window text
|
# Get window text (smaller buffer for speed)
|
||||||
text = ctypes.create_unicode_buffer(256)
|
text = ctypes.create_unicode_buffer(128)
|
||||||
self.user32.GetWindowTextW(hwnd, text, 256)
|
self._user32.GetWindowTextW(hwnd, text, 128)
|
||||||
|
|
||||||
# Get PID
|
# Get PID
|
||||||
pid = wintypes.DWORD()
|
pid = wintypes.DWORD()
|
||||||
self.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
self._user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||||
|
|
||||||
# Check visibility
|
# Check visibility
|
||||||
is_visible = bool(self.user32.IsWindowVisible(hwnd))
|
is_visible = bool(self._user32.IsWindowVisible(hwnd))
|
||||||
|
|
||||||
# Check if focused
|
# Check if focused
|
||||||
foreground = self.user32.GetForegroundWindow()
|
foreground = self._user32.GetForegroundWindow()
|
||||||
is_focused = (foreground == hwnd)
|
is_focused = (foreground == hwnd)
|
||||||
|
|
||||||
return WindowInfo(
|
return WindowInfo(
|
||||||
|
|
@ -406,26 +656,38 @@ class WindowManager:
|
||||||
is_visible=is_visible,
|
is_visible=is_visible,
|
||||||
is_focused=is_focused
|
is_focused=is_focused
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"[WindowManager] Error getting window info: {e}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_process_info(self, pid: int) -> Optional[ProcessInfo]:
|
def _get_process_info_fast(self, pid: int) -> Optional[ProcessInfo]:
|
||||||
"""Get process information."""
|
"""Get process info with caching and minimal overhead."""
|
||||||
try:
|
# Try psutil first (much faster)
|
||||||
# Use Windows WMI or tasklist for process info
|
if PSUTIL_AVAILABLE:
|
||||||
import subprocess
|
try:
|
||||||
|
proc = psutil.Process(pid)
|
||||||
|
info = proc.as_dict(attrs=['pid', 'name', 'exe', 'memory_info'])
|
||||||
|
return ProcessInfo(
|
||||||
|
pid=info['pid'],
|
||||||
|
name=info['name'],
|
||||||
|
executable_path=info.get('exe'),
|
||||||
|
memory_usage=info['memory_info'].rss if info.get('memory_info') else None,
|
||||||
|
cpu_percent=None
|
||||||
|
)
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Try tasklist first
|
# Fall back to tasklist (slower)
|
||||||
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['tasklist', '/FI', f'PID eq {pid}', '/FO', 'CSV', '/NH'],
|
['tasklist', '/FI', f'PID eq {pid}', '/FO', 'CSV', '/NH'],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=5
|
timeout=2 # Reduced from 5
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0 and result.stdout.strip():
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
# Parse CSV output
|
|
||||||
lines = result.stdout.strip().split('\n')
|
lines = result.stdout.strip().split('\n')
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if str(pid) in line:
|
if str(pid) in line:
|
||||||
|
|
@ -447,16 +709,9 @@ class WindowManager:
|
||||||
memory_usage=None,
|
memory_usage=None,
|
||||||
cpu_percent=None
|
cpu_percent=None
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"[WindowManager] Error getting process info: {e}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _check_window_exists(self, hwnd: int) -> bool:
|
|
||||||
"""Check if a window handle is still valid."""
|
|
||||||
if not self._available:
|
|
||||||
return False
|
|
||||||
return bool(self.user32.IsWindow(hwnd))
|
|
||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
_window_manager = None
|
_window_manager = None
|
||||||
|
|
@ -473,7 +728,7 @@ def get_window_manager() -> WindowManager:
|
||||||
def is_eu_running() -> bool:
|
def is_eu_running() -> bool:
|
||||||
"""Quick check if Entropia Universe is running."""
|
"""Quick check if Entropia Universe is running."""
|
||||||
wm = get_window_manager()
|
wm = get_window_manager()
|
||||||
return wm.find_eu_window() is not None
|
return wm.find_eu_window_cached() is not None
|
||||||
|
|
||||||
|
|
||||||
def get_eu_window_rect() -> Optional[Tuple[int, int, int, int]]:
|
def get_eu_window_rect() -> Optional[Tuple[int, int, int, int]]:
|
||||||
|
|
@ -496,7 +751,7 @@ def wait_for_eu(timeout: float = 30.0) -> bool:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
while time.time() - start_time < timeout:
|
while time.time() - start_time < timeout:
|
||||||
if wm.find_eu_window():
|
if wm.find_eu_window_cached():
|
||||||
return True
|
return True
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue