import re import os import asyncio import logging as log import subprocess from time import time import pathlib import psutil from definitions import ClassicGame from consts import Platform, SYSTEM, AGENT_PATH from local_client_base import BaseLocalClient if SYSTEM == Platform.WINDOWS: import winreg import ctypes elif SYSTEM == Platform.MACOS: from Quartz import CGWindowListCopyWindowInfo, kCGNullWindowID, kCGWindowListExcludeDesktopElements from AppKit import NSWorkspace class WinUninstaller(object): def __init__(self, path): self.path = path if not os.path.exists(self.path): raise FileNotFoundError("Uninstaller not found") def uninstall_game(self, game, uninstall_tag, lang): if isinstance(game.info, ClassicGame): log.info(f"Uninstalling classic game by {uninstall_tag}") subprocess.Popen(uninstall_tag, shell=True) return args = [ str(self.path), f'--lang={lang}', f'--uid={uninstall_tag}', f'--displayname={game.info.name}' ] subprocess.Popen(args, cwd=os.path.dirname(self.path)) class WinLocalClient(BaseLocalClient): def __init__(self, update_statuses): self._WIN_REG_SHELL = (winreg.HKEY_CLASSES_ROOT, r"battlenet\shell\open\command") super().__init__(update_statuses) self.uninstaller = self.set_uninstaller() self._exe = self._find_exe() def set_uninstaller(self): try: if SYSTEM == Platform.WINDOWS and self.uninstaller is None: uninstaller_path = pathlib.Path(AGENT_PATH) / 'Blizzard Uninstaller.exe' return WinUninstaller(uninstaller_path) except FileNotFoundError as e: log.warning('uninstaller not found' + str(e)) def _find_exe(self): shell_reg_value = self.__search_registry_for_run_cmd(*self._WIN_REG_SHELL) if shell_reg_value is None: return None reg = re.compile("\"(.*?)\"") # any chars in double quotes return reg.search(shell_reg_value).groups()[0] def _find_main_renderer_window(self): """Get Blizzard renderer window (main window, not login) :return int number of window; 0 if window not found""" return ctypes.windll.user32.FindWindowW(None, "Blizzard Battle.net") or \ ctypes.windll.user32.FindWindowW(None, "Battle.net") or \ ctypes.windll.user32.FindWindowW(None, "暴雪战网") def _is_main_window_open(self): return bool(self._find_main_renderer_window()) @property def is_installed(self): return bool(self._exe) def close_window(self): """Closes Blizzard renderer using native API (but not login window)""" bnet_handle = self._find_main_renderer_window() if not bnet_handle: return False if ctypes.windll.user32.IsWindowVisible(bnet_handle): ctypes.windll.user32.CloseWindow(bnet_handle) if ctypes.windll.user32.IsWindowVisible(bnet_handle): return False return True return False async def prevent_battlenet_from_showing(self): client_popup_wait_time = 5 check_frequency_delay = 0.02 end_time = time() + client_popup_wait_time while not self.close_window(): if time() >= end_time: log.info("Timed out closing bnet popup") break await asyncio.sleep(check_frequency_delay) async def shutdown_platform_client(self): subprocess.Popen("taskkill.exe /im \"Battle.net.exe\"") # Battle.net probably never exits on WM_CLOSE so make sure it will be hidden await self.prevent_battlenet_from_showing() def _check_for_game_process(self, game): try: if not self.is_running(): return False with self._process.oneshot(): for proc in self._process.children(): if proc.exe() in game.execs: log.debug(f'Process has been found') return True except (psutil.AccessDenied, psutil.NoSuchProcess): pass except Exception as e: log.error(f'Error while waiting for process to be spawn: {repr(e)}') def __search_registry_for_run_cmd(self, *args): """ :param args - arguments as for winreg.OpenKey() :returns value of the first string-type key or False if given registry does not exists """ try: key = winreg.OpenKey(*args) for i in range(1024): try: _, exe_cmd, _type = winreg.EnumValue(key, i) if exe_cmd and _type == winreg.REG_SZ: # null-terminated string return exe_cmd except OSError: # no more data break except FileNotFoundError: return None class MacLocalClient(BaseLocalClient): _PATH = "/Applications/Battle.net.app/Contents/MacOS/Battle.net" def __init__(self, update_statuses): super().__init__(update_statuses) self.uninstaller = None self._exe = self._find_exe() def _find_exe(self): return self._PATH def _is_main_window_open(self): """Main window, not login one""" windows = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements, kCGNullWindowID) for window in windows: try: if 'Blizzard Battle.net' == window['kCGWindowName']: log.debug('Main Battle.net window was found') return True except KeyError: continue return False def close_window(self): workspace = NSWorkspace.sharedWorkspace() activeApps = workspace.runningApplications() for app in activeApps: if app.isActive() and app.localizedName() == "Blizzard Battle.net": app.hide() async def prevent_battlenet_from_showing(self): client_popup_wait_time = 5 check_frequency_delay = 0.02 workspace = NSWorkspace.sharedWorkspace() activeApps = workspace.runningApplications() end_time = time() + client_popup_wait_time while time() <= end_time: for app in activeApps: if app.isActive() and app.localizedName() == "Blizzard Battle.net": app.hide() return await asyncio.sleep(check_frequency_delay) log.info("Timed out on prevent battlenet from showing") async def shutdown_platform_client(self): subprocess.Popen("osascript -e 'quit app \"Battle.net\"'", shell=True) @property def is_installed(self): return os.path.exists(self._exe) def _check_for_game_process(self, game): """Check over all processes because on macOS games are spawn not as client children""" for proc in psutil.process_iter(attrs=['exe'], ad_value=''): if proc.info['exe'] in game.execs: return True return False if SYSTEM == Platform.WINDOWS: LocalClient = WinLocalClient elif SYSTEM == Platform.MACOS: LocalClient = MacLocalClient