109 lines
5.2 KiB
Python
109 lines
5.2 KiB
Python
"""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]
|
||
|
|
|