"""Shared verified Desktop process and window discovery.""" import ctypes import hashlib from pathlib import Path SUPPORTED_BUILD = "2.2639.100.0" CORE_SHA256 = "c6da3c67eda66a8d48ff2ae11f68085f1f8250111138a6ae384d38eefb4da536" def check_core_build(path): if 'WhatsAppDesktop_' + SUPPORTED_BUILD + '_' not in str(path): raise ValueError('Unsupported WhatsApp build: ' + str(path) + '; supported build is ' + SUPPORTED_BUILD + '. No hook installed.') if hashlib.sha256(Path(path).read_bytes()).hexdigest() != CORE_SHA256: raise ValueError('Unknown WebView2 Core DLL hash. No hook installed.') def visible_ui_threads(pid): """Inspect window ownership; no clicks, navigation or window messages.""" import ctypes from ctypes import wintypes user32 = ctypes.WinDLL('user32', use_last_error=True) user32.GetWindowThreadProcessId.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.DWORD)] user32.GetWindowThreadProcessId.restype = wintypes.DWORD user32.IsWindowVisible.argtypes = [wintypes.HWND] user32.IsWindowVisible.restype = wintypes.BOOL callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) threads = set() def visit(hwnd, parameter): process = wintypes.DWORD() thread = user32.GetWindowThreadProcessId(hwnd, ctypes.byref(process)) if process.value == pid and user32.IsWindowVisible(hwnd): threads.add(thread) return True callback = callback_type(visit) user32.EnumWindows.argtypes = [callback_type, wintypes.LPARAM] user32.EnumWindows.restype = wintypes.BOOL if not user32.EnumWindows(callback, 0): raise ctypes.WinError(ctypes.get_last_error()) return threads def detect_desktop_pid(requested_pid=None): """Find the running supported Desktop host using Python and Windows APIs.""" import ctypes from ctypes import wintypes as w import hashlib class ProcessEntry(ctypes.Structure): _fields_ = [('dwSize',w.DWORD),('cntUsage',w.DWORD),('th32ProcessID',w.DWORD), ('th32DefaultHeapID',ctypes.c_size_t),('th32ModuleID',w.DWORD), ('cntThreads',w.DWORD),('th32ParentProcessID',w.DWORD), ('pcPriClassBase',w.LONG),('dwFlags',w.DWORD),('szExeFile',w.WCHAR*260)] class ModuleEntry(ctypes.Structure): _fields_ = [('dwSize',w.DWORD),('th32ModuleID',w.DWORD),('th32ProcessID',w.DWORD), ('GlblcntUsage',w.DWORD),('ProccntUsage',w.DWORD), ('modBaseAddr',ctypes.POINTER(w.BYTE)),('modBaseSize',w.DWORD), ('hModule',w.HMODULE),('szModule',w.WCHAR*256),('szExePath',w.WCHAR*260)] kernel = ctypes.WinDLL('kernel32',use_last_error=True) kernel.CreateToolhelp32Snapshot.argtypes = [w.DWORD,w.DWORD] kernel.CreateToolhelp32Snapshot.restype = w.HANDLE kernel.CloseHandle.argtypes = [w.HANDLE] kernel.CloseHandle.restype = w.BOOL for name, entry in [('Process32FirstW',ProcessEntry),('Process32NextW',ProcessEntry), ('Module32FirstW',ModuleEntry),('Module32NextW',ModuleEntry)]: fn = getattr(kernel,name) fn.argtypes = [w.HANDLE,ctypes.POINTER(entry)] fn.restype = w.BOOL invalid = ctypes.c_void_p(-1).value snapshot = kernel.CreateToolhelp32Snapshot(2,0) if snapshot == invalid: raise ctypes.WinError(ctypes.get_last_error()) roots = [] try: entry = ProcessEntry(dwSize=ctypes.sizeof(ProcessEntry)) exists = kernel.Process32FirstW(snapshot,ctypes.byref(entry)) while exists: if entry.szExeFile.casefold() == 'whatsapp.root.exe': roots.append(entry.th32ProcessID) exists = kernel.Process32NextW(snapshot,ctypes.byref(entry)) finally: kernel.CloseHandle(snapshot) candidates = [] rejected = [] for pid in roots: if requested_pid is not None and pid != requested_pid: continue snapshot = kernel.CreateToolhelp32Snapshot(8|16,pid) if snapshot == invalid: continue try: entry = ModuleEntry(dwSize=ctypes.sizeof(ModuleEntry)) exists = kernel.Module32FirstW(snapshot,ctypes.byref(entry)) while exists: if entry.szModule.casefold() == 'microsoft.web.webview2.core.dll': path = Path(entry.szExePath) if len(visible_ui_threads(pid)) == 1: try: check_core_build(path) candidates.append(pid) except (ValueError, OSError) as error: rejected.append(str(error)) break exists = kernel.Module32NextW(snapshot,ctypes.byref(entry)) except OSError: continue finally: kernel.CloseHandle(snapshot) if not candidates: raise ValueError('; '.join(rejected) if rejected else 'No supported running WhatsApp Desktop host with one identifiable UI thread found; start WhatsApp normally') if len(candidates) != 1: raise ValueError('Multiple active Desktop hosts found; specify --pid: '+', '.join(map(str,candidates))) return candidates[0]