Example #1
0
class BaseLocalClient(abc.ABC):

    PRODUCT_DB_PATH = Path(AGENT_PATH) / 'product.db'
    CONFIG_PATH = CONFIG_PATH

    def __init__(self, update_statuses):
        self._update_statuses = update_statuses
        self._process_provider = ProcessProvider()
        self._process = None
        self._exe = self._find_exe()
        self._games_provider = LocalGames()

        self.database_parser = None
        self.config_parser = None
        self.uninstaller = None
        self.installed_games_cache = self.get_installed_games()

        loop = asyncio.get_event_loop()
        loop.create_task(self._register_local_data_watcher())
        loop.create_task(self._register_classic_games_updater())
        self.classic_games_parsing_task = None

    @abc.abstractproperty
    def is_installed(self):
        pass

    @abc.abstractmethod
    def _find_exe(self):
        """Returns Battlenet main executable"""
        pass

    @abc.abstractmethod
    def _is_main_window_open(self):
        """Return True if Blizzard main renderer window is present (main window, not login)"""
        pass

    @abc.abstractmethod
    def _check_for_game_process(self, game):
        """Returns True if process matching game if found"""
        pass

    def refresh(self):
        self._exe = self._find_exe()

    def is_running(self):
        if self._process and self._process.is_running():
            return True
        else:
            self._process = self._process_provider.get_process_by_path(
                self._exe)
            return bool(self._process)

    async def _prepare_to_launch(self, uid, timeout):
        """launches the client and waits till proper renderer is opened
        :param uid      str of game uid. Makes login window game oriented
        :param timeout  timestamp when a watch should be stopped
        """
        if self.is_running() and self._is_main_window_open():
            return

        subprocess.Popen([self._exe, f'--game={uid}'],
                         cwd=os.path.dirname(self._exe))
        while time() < timeout:
            if self._is_main_window_open():
                log.debug(
                    'Preparing to launch ended {:.2f}s before timeout'.format(
                        timeout - time()))
                return
            await asyncio.sleep(0.2)
        raise TimeoutError(
            f'Timeout reached when waiting for gameview from Battle.net')

    def install_game(self, id):
        if not self.is_installed:
            raise ClientNotInstalledError()
        game = Blizzard[id]
        args = [self._exe, "--install", f"--game={game.uid}"]
        subprocess.Popen(args, cwd=os.path.dirname(self._exe))

    def open_battlenet(self, id=None):
        if not self.is_installed or not self._exe:
            raise ClientNotInstalledError()
        if id:
            game = Blizzard[id]
            args = {self._exe, f"--game={game.uid}"}
        else:
            args = {self._exe}
        subprocess.Popen(args, cwd=os.path.dirname(self._exe))

    async def wait_until_game_stops(self, game: InstalledGame):
        if not self.is_running():
            return 'Client not running'
        for child in self._process.children():
            if child.exe() in game.execs:
                game_process = child
                break
        else:
            return 'No subprocess matches'
        while True:
            if not game_process.is_running():
                return 'Game process is no longer running'
            await asyncio.sleep(1)

    async def launch_game(self, game: InstalledGame, wait_sec):
        if not self.is_installed:
            raise ClientNotInstalledError()
        timeout = time() + wait_sec

        if game.info.family == 'WoW_wow_classic':
            if SYSTEM == Platform.WINDOWS:
                cmd = f"\"{Path(game.install_path)/'World of Warcraft Launcher.exe'}\" --productcode=wow_classic"
            else:
                cmd = f"open \"{Path(game.install_path)/'World of Warcraft Launcher.app'}\" --args productcode=wow_classic"
            subprocess.Popen(cmd, shell=True)
        else:
            await self._prepare_to_launch(game.info.uid, timeout)
            cmd = f'"{self._exe}" --exec="launch {game.info.family}"'
            subprocess.Popen(cmd, cwd=os.path.dirname(self._exe), shell=True)
        log.info(f"Launch game and start waiting for game process")
        while time() < timeout:
            if self._check_for_game_process(game):
                return
            await asyncio.sleep(0.5)
        raise TimeoutError(f"Game process has not appear within {wait_sec}s")

    def _load_local_files(self):
        try:
            product_db = load_product_db(self.PRODUCT_DB_PATH)
            self.database_parser = DatabaseParser(product_db)
        except FileNotFoundError as e:
            log.warning(f"product.db not found: {repr(e)}")
            return False
        except WindowsError as e:
            # 5 WindowsError access denied
            if e.winerror == 5:
                log.warning(f"product.db not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        except OSError as e:
            if e.errno == errno.EACCES:
                log.warning(f"product.db not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        else:
            if self.is_installed != self.database_parser.battlenet_present:
                self.refresh()

        try:
            config = load_config(self.CONFIG_PATH)
            self.config_parser = ConfigParser(config)
        except FileNotFoundError as e:
            log.warning(f"config file not found: {repr(e)}")
            self.config_parser = ConfigParser(None)
            return False
        except WindowsError as e:
            # 5 WindowsError access denied
            if e.winerror == 5:
                log.warning(f"config file not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        except OSError as e:
            if e.errno == errno.EACCES:
                log.warning(f"config file not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        return True

    async def _register_local_data_watcher(self):
        parse_local_data_event = asyncio.Event()
        FileWatcher(self.CONFIG_PATH, parse_local_data_event, interval=1)
        FileWatcher(self.PRODUCT_DB_PATH, parse_local_data_event, interval=2.5)
        parse_local_data_event.set()
        while True:
            await parse_local_data_event.wait()

            if not self._load_local_files():
                parse_local_data_event.clear()
                continue
            if self.is_installed != self.database_parser.battlenet_present:
                self.refresh()

            self._games_provider.parse_local_battlenet_games(
                self.database_parser.games, self.config_parser.games)
            refreshed_games = self.get_installed_games()

            self._update_statuses(refreshed_games, self.installed_games_cache)
            self.installed_games_cache = refreshed_games
            parse_local_data_event.clear()

    async def _register_classic_games_updater(self):
        tick_count = 0
        while True:
            tick_count += 1
            if tick_count % 30 == 0:
                if not self.classic_games_parsing_task or self.classic_games_parsing_task.done(
                ):
                    self.classic_games_parsing_task = asyncio.create_task(
                        self._games_provider.parse_local_classic_games())
                    refreshed_games = self.get_installed_games()
                    self._update_statuses(refreshed_games,
                                          self.installed_games_cache)
                    self.installed_games_cache = refreshed_games
            await asyncio.sleep(1)

    def games_finished_parsing(self):
        return self._games_provider.parsed_classics and self._games_provider.parsed_battlenet

    def get_installed_games(self, timeout=1):
        games = {}

        if self._games_provider.installed_battlenet_games_lock.acquire(
                True, timeout):
            games = self._games_provider.installed_battlenet_games
            self._games_provider.installed_battlenet_games_lock.release()

        if self._games_provider.installed_classic_games_lock.acquire(
                True, timeout):
            games = {**games, **self._games_provider.installed_classic_games}
            self._games_provider.installed_classic_games_lock.release()

        return games

    def get_running_games(self):
        return ProcessProvider().update_games_processes(
            self.get_installed_games().values())
Example #2
0
class _LocalClient(abc.ABC):
    def __init__(self):
        self._process_provider = ProcessProvider()
        self._process = None
        self._exe = self._find_exe()

    @abc.abstractproperty
    def is_installed(self):
        pass

    @abc.abstractmethod
    def _find_exe(self):
        """Returns Battlenet main executable"""
        pass

    @abc.abstractmethod
    def _is_main_window_open(self):
        """Return True if Blizzard main renderer window is present (main window, not login)"""
        pass

    @abc.abstractmethod
    def _check_for_game_process(self, game):
        """Returns True if process matching game if found"""
        pass

    def refresh(self):
        self._exe = self._find_exe()

    def is_running(self):
        if self._process and self._process.is_running():
            return True
        else:
            self._process = self._process_provider.get_process_by_path(self._exe)
            return bool(self._process)

    async def _prepare_to_launch(self, uid, timeout):
        """launches the client and waits till proper renderer is opened
        :param uid      str of game uid. Makes login window game oriented
        :param timeout  timestamp when a watch should be stopped
        """
        if self.is_running() and self._is_main_window_open():
            return

        subprocess.Popen([self._exe, f'--game={uid}'], cwd=os.path.dirname(self._exe))
        while time() < timeout:
            if self._is_main_window_open():
                log.debug('Preparing to launch ended {:.2f}s before timeout'.format(timeout - time()))
                return
            await asyncio.sleep(0.2)
        raise TimeoutError(f'Timeout reached when waiting for gameview from Battle.net')

    def install_game(self, id):
        if not self.is_installed:
            raise ClientNotInstalledError()
        game = Blizzard[id]
        args = [
            self._exe,
            "--install",
            f"--game={game.uid}"
        ]
        subprocess.Popen(args, cwd=os.path.dirname(self._exe))

    async def wait_until_game_stops(self, game: InstalledGame):
        if not self.is_running():
            return 'Client not running'
        for child in self._process.children():
            if child.exe() in game.execs:
                game_process = child
                break
        else:
            return 'No subprocess matches'
        while True:
            if not game_process.is_running():
                return 'Game process is no longer running'
            await asyncio.sleep(1)

    async def launch_game(self, game: InstalledGame, wait_sec):
        if not self.is_installed:
            raise ClientNotInstalledError()
        timeout = time() + wait_sec

        await self._prepare_to_launch(game.info.uid, timeout)

        cmd = f'"{self._exe}" --exec="launch {game.info.family}"'
        subprocess.Popen(cmd, cwd=os.path.dirname(self._exe), shell=True)
        log.info(f"Launch game and start waiting for game process")

        while time() < timeout:
            if self._check_for_game_process(game):
                return
            await asyncio.sleep(0.5)
        raise TimeoutError(f"Game process has not appear within {wait_sec}s")