diff --git a/src/pyclashbot/memu/configure.py b/src/pyclashbot/memu/configure.py index ea93ecef7..e4d6e67eb 100644 --- a/src/pyclashbot/memu/configure.py +++ b/src/pyclashbot/memu/configure.py @@ -1,13 +1,15 @@ -"""A module for configuring Memu VMs. -""" +"""A module for configuring Memu VMs.""" import logging import time -from pymemuc import ConfigKeys +from pymemuc import ConfigKeys, PyMemucError, PyMemucException from pyclashbot.memu.pmc import pmc +ANDROID_VERSION = "96" # android 9, 64 bit +EMULATOR_NAME = f"pyclashbot-{ANDROID_VERSION}" + # see https://pymemuc.readthedocs.io/pymemuc.html#the-vm-configuration-keys-table MEMU_CONFIGURATION: dict[ConfigKeys, str | int | float] = { "start_window_mode": 1, # remember window position @@ -53,5 +55,16 @@ def configure_vm(vm_index): logging.info("Configured VM %s", vm_index) +def get_vm_configuration(vm_index: int) -> dict[str, str]: + current_configuration = {} + for key in MEMU_CONFIGURATION: + try: + current_value = pmc.get_configuration_vm(key, vm_index=vm_index) + current_configuration[key] = current_value + except PyMemucError as e: + logging.exception("Failed to get configuration for key %s: %s", key, e) + return current_configuration + + if __name__ == "__main__": pass diff --git a/src/pyclashbot/memu/launcher.py b/src/pyclashbot/memu/launcher.py index 378135613..ebd417ad3 100644 --- a/src/pyclashbot/memu/launcher.py +++ b/src/pyclashbot/memu/launcher.py @@ -10,16 +10,18 @@ import psutil import PySimpleGUI as sg -from pymemuc import PyMemucError, VMInfo from pyclashbot.bot.nav import check_if_in_battle_at_start, check_if_on_clash_main_menu from pyclashbot.memu.client import click, screenshot -from pyclashbot.memu.configure import configure_vm -from pyclashbot.memu.pmc import pmc +from pyclashbot.memu.configure import EMULATOR_NAME, configure_vm +from pyclashbot.memu.pmc import get_vm_index, pmc from pyclashbot.utils.logger import Logger +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pymemuc import VMInfo + -ANDROID_VERSION = "96" # android 9, 64 bit -EMULATOR_NAME = f"pyclashbot-{ANDROID_VERSION}" APK_BASE_NAME = "com.supercell.clashroyale" @@ -169,7 +171,7 @@ def check_for_vm(logger: Logger) -> int: find_vm_tries = 0 while time.time() - find_vm_start_time < find_vm_timeout: find_vm_tries += 1 - vm_index = get_vm_index(logger, EMULATOR_NAME) + vm_index = get_vm_index(EMULATOR_NAME) if vm_index != -1: logger.change_status( @@ -243,35 +245,6 @@ def rename_vm( # emulator interaction methods - - -def get_vm_index(logger: Logger, name: str) -> int: - """Get the index of the vm with the given name""" - # get list of vms on machine - vms: list[VMInfo] = pmc.list_vm_info() - - # sorted by index, lowest to highest - vms.sort(key=lambda x: x["index"]) - - # get the indecies of all vms named pyclashbot - vm_indices: list[int] = [vm["index"] for vm in vms if vm["title"] == name] - - # delete all vms except the lowest index, keep looping until there is only one - while len(vm_indices) > 1: - # as long as no exception is raised, this while loop should exit on first iteration - for vm_index in vm_indices[1:]: - try: - pmc.delete_vm(vm_index) - vm_indices.remove(vm_index) - except PyMemucError as err: - logger.error(str(err)) - # don't raise error, just continue to loop until its deleted - # raise err # if program hangs on deleting vm then uncomment this line - - # return the index. if no vms named pyclashbot exist, return -1 - return vm_indices[0] if vm_indices else -1 - - def home_button_press(vm_index, clicks=4): """Method for skipping the memu ads that popip up when you start memu""" for _ in range(clicks): diff --git a/src/pyclashbot/memu/pmc.py b/src/pyclashbot/memu/pmc.py index f551e3e3d..5612204e5 100644 --- a/src/pyclashbot/memu/pmc.py +++ b/src/pyclashbot/memu/pmc.py @@ -1,10 +1,10 @@ -"""This module provides a PyMemuc singleton instance. -""" +"""This module provides a PyMemuc singleton instance.""" +import logging import sys from os.path import join -from pymemuc import PyMemuc +from pymemuc import PyMemuc, PyMemucError, VMInfo FROZEN = getattr(sys, "frozen", False) @@ -15,3 +15,28 @@ pmc._get_memu_top_level(), "adb.exe", ) + + +def get_vm_index(name: str) -> int: + """Get the index of the vm with the given name""" + # get list of vms on machine + vms: list[VMInfo] = pmc.list_vm_info() + + # sorted by index, lowest to highest + vms.sort(key=lambda x: x["index"]) + + # get the indecies of all vms named pyclashbot + vm_indices: list[int] = [vm["index"] for vm in vms if vm["title"] == name] + + # delete all vms except the lowest index, keep looping until there is only one + while len(vm_indices) > 1: + # as long as no exception is raised, this while loop should exit on first iteration + for vm_index in vm_indices[1:]: + try: + pmc.delete_vm(vm_index) + vm_indices.remove(vm_index) + except PyMemucError as err: + logging.exception(err) + + # return the index. if no vms named pyclashbot exist, return -1 + return vm_indices[0] if vm_indices else -1 diff --git a/src/pyclashbot/utils/logger.py b/src/pyclashbot/utils/logger.py index 1cdf33027..2a2e86d9d 100644 --- a/src/pyclashbot/utils/logger.py +++ b/src/pyclashbot/utils/logger.py @@ -10,6 +10,8 @@ from os import listdir, makedirs, remove from os.path import basename, exists, expandvars, getmtime, join +from pyclashbot.memu.configure import EMULATOR_NAME, get_vm_configuration +from pyclashbot.memu.pmc import get_vm_index from pyclashbot.utils.machine_info import MACHINE_INFO from pyclashbot.utils.pastebin import upload_pastebin from pyclashbot.utils.versioning import __version__ @@ -59,7 +61,15 @@ def initalize_pylogging() -> None: """, ) logging.info( - "Machine Info: \n%s", pprint.pformat(MACHINE_INFO, sort_dicts=False, indent=4), + "Machine Info: \n%s", + pprint.pformat(MACHINE_INFO, sort_dicts=False, indent=4), + ) + + vm_index = get_vm_index(EMULATOR_NAME) + + logging.info( + "VM Configuration: \n%s", + pprint.pformat(get_vm_configuration(vm_index), indent=4), ) compress_logs() diff --git a/src/pyclashbot/utils/machine_info.py b/src/pyclashbot/utils/machine_info.py index fabeb8a61..26dbd6dd2 100644 --- a/src/pyclashbot/utils/machine_info.py +++ b/src/pyclashbot/utils/machine_info.py @@ -2,12 +2,15 @@ import configparser import ctypes +import logging import platform from os.path import join +import subprocess import psutil from pyclashbot.memu.pmc import pmc +from pyclashbot.utils.subprocess import run user32 = ctypes.windll.user32 @@ -18,6 +21,23 @@ ) +def check_hyper_v_enabled() -> bool: + """Check if Hyper-V is enabled on the system.""" + try: + _, result = run( + [ + "powershell", + '"Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V"', + ], + ) + + # Check if Hyper-V is enabled based on the output + return "State : Enabled" in result + except subprocess.CalledProcessError as e: + logging.exception("Error executing command: %s", e) + return False + + MACHINE_INFO: dict[str, str | int | float] = { "os": platform.system(), "os_version": platform.version(), @@ -30,6 +50,7 @@ "cpu_count": int(psutil.cpu_count(logical=False)), "cpu_freq": float(psutil.cpu_freq().current), "memu_version": memu_config.get("reginfo", "version", fallback="unknown"), + "hyper-v_enabled": check_hyper_v_enabled(), } if __name__ == "__main__": diff --git a/src/pyclashbot/utils/subprocess.py b/src/pyclashbot/utils/subprocess.py new file mode 100644 index 000000000..377d89f3d --- /dev/null +++ b/src/pyclashbot/utils/subprocess.py @@ -0,0 +1,66 @@ +from os import name +from subprocess import PIPE, Popen, TimeoutExpired +from typing import Tuple + +# check if running on windows +WIN32 = name == "nt" +ST_INFO = None +if WIN32: + import ctypes + from subprocess import ( + CREATE_NO_WINDOW, + REALTIME_PRIORITY_CLASS, + STARTF_USESHOWWINDOW, + STARTF_USESTDHANDLES, + STARTUPINFO, + SW_HIDE, + ) + + ST_INFO = STARTUPINFO() # pyright: ignore [reportConstantRedefinition] + ST_INFO.dwFlags |= ( + STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES | REALTIME_PRIORITY_CLASS + ) + ST_INFO.wShowWindow = SW_HIDE + CR_FLAGS = CREATE_NO_WINDOW + subprocess_flags = { + "startupinfo": ST_INFO, + "creationflags": CR_FLAGS, + "start_new_session": True, + } +else: + subprocess_flags = {} + + +def _terminate_process( # pyright: ignore [reportUnusedFunction] + process: Popen[str], +) -> None: + """Terminate a process forcefully on Windows.""" + handle = ctypes.windll.kernel32.OpenProcess(1, False, process.pid) + ctypes.windll.kernel32.TerminateProcess(handle, -1) + ctypes.windll.kernel32.CloseHandle(handle) + + +def run( + args: list[str], +) -> Tuple[int, str]: + with Popen( + args, + shell=False, + bufsize=-1, + stdout=PIPE, + stderr=PIPE, + close_fds=True, + universal_newlines=True, + **subprocess_flags, + ) as process: + try: + result, _ = process.communicate(timeout=5) + except TimeoutExpired: + if WIN32: + # pylint: disable=protected-access + _terminate_process(process) + process.kill() + result, _ = process.communicate() + raise + + return (process.returncode, result)