diff --git a/module/device/platform/emulator_base.py b/module/device/platform/emulator_base.py index ecd026ba1..94483e9f9 100644 --- a/module/device/platform/emulator_base.py +++ b/module/device/platform/emulator_base.py @@ -36,6 +36,21 @@ def get_serial_pair(serial): return None, None +def remove_duplicated_path(paths): + """ + Args: + paths (list[str]): + + Returns: + list[str]: + """ + paths = sorted(set(paths)) + dic = {} + for path in paths: + dic.setdefault(path.lower(), path) + return list(dic.values()) + + @dataclass class EmulatorInstanceBase: # Serial for adb connection @@ -205,6 +220,14 @@ class EmulatorBase: class EmulatorManagerBase: + @staticmethod + def iter_running_emulator(): + """ + Yields: + str: Path to emulator executables, may contains duplicate values + """ + return + @cached_property def all_emulators(self) -> t.List[EmulatorBase]: """ diff --git a/module/device/platform/emulator_windows.py b/module/device/platform/emulator_windows.py index 142ad3e30..f7a5e54bc 100644 --- a/module/device/platform/emulator_windows.py +++ b/module/device/platform/emulator_windows.py @@ -8,7 +8,8 @@ from dataclasses import dataclass # module/device/platform/emulator_base.py # module/device/platform/emulator_windows.py # Will be used in Alas Easy Install, they shouldn't import any Alas modules. -from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase +from module.device.platform.emulator_base import EmulatorBase, EmulatorInstanceBase, EmulatorManagerBase, \ + remove_duplicated_path from module.device.platform.utils import cached_property, iter_folder @@ -70,7 +71,7 @@ class Emulator(EmulatorBase): def path_to_type(cls, path: str) -> str: """ Args: - path: Path to .exe file + path: Path to .exe file, case insensitive Returns: str: Emulator type, such as Emulator.NoxPlayer @@ -78,46 +79,49 @@ class Emulator(EmulatorBase): folder, exe = os.path.split(path) folder, dir1 = os.path.split(folder) folder, dir2 = os.path.split(folder) - if exe == 'Nox.exe': - if dir2 == 'Nox': + exe = exe.lower() + dir1 = dir1.lower() + dir2 = dir2.lower() + if exe == 'nox.exe': + if dir2 == 'nox': return cls.NoxPlayer - elif dir2 == 'Nox64': + elif dir2 == 'nox64': return cls.NoxPlayer64 else: return cls.NoxPlayer - if exe == 'Bluestacks.exe': - if dir1 in ['BlueStacks', 'BlueStacks_cn']: + if exe == 'bluestacks.exe': + if dir1 in ['bluestacks', 'bluestacks_cn']: return cls.BlueStacks4 - elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']: + elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']: return cls.BlueStacks5 else: return cls.BlueStacks4 - if exe == 'HD-Player.exe': - if dir1 in ['BlueStacks', 'BlueStacks_cn']: + if exe == 'hd-player.exe': + if dir1 in ['bluestacks', 'bluestacks_cn']: return cls.BlueStacks4 - elif dir1 in ['BlueStacks_nxt', 'BlueStacks_nxt_cn']: + elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']: return cls.BlueStacks5 else: return cls.BlueStacks5 if exe == 'dnplayer.exe': - if dir1 == 'LDPlayer': + if dir1 == 'ldplayer': return cls.LDPlayer3 - elif dir1 == 'LDPlayer4': + elif dir1 == 'ldplayer4': return cls.LDPlayer4 - elif dir1 == 'LDPlayer9': + elif dir1 == 'ldplayer9': return cls.LDPlayer9 else: return cls.LDPlayer3 - if exe == 'NemuPlayer.exe': + if exe == 'nemuplayer.exe': if dir2 == 'nemu': return cls.MuMuPlayer elif dir2 == 'nemu9': return cls.MuMuPlayerX else: return cls.MuMuPlayer - if exe == 'MuMuPlayer.exe': + if exe == 'mumuplayer.exe': return cls.MuMuPlayer12 - if exe == 'MEmu.exe': + if exe == 'memu.exe': return cls.MEmuPlayer return '' @@ -143,7 +147,9 @@ class Emulator(EmulatorBase): elif 'NemuMultiPlayer.exe' in exe: yield exe.replace('NemuMultiPlayer.exe', 'NemuPlayer.exe') elif 'MuMuMultiPlayer.exe' in exe: - yield exe.replace('MuMuMultiPlayer.exe', 'MuMuManager.exe') + yield exe.replace('MuMuMultiPlayer.exe', 'MuMuPlayer.exe') + elif 'MuMuManager.exe' in exe: + yield exe.replace('MuMuManager.exe', 'MuMuPlayer.exe') elif 'MEmuConsole.exe' in exe: yield exe.replace('MEmuConsole.exe', 'MEmu.exe') else: @@ -316,7 +322,7 @@ class EmulatorManager(EmulatorManagerBase): Get recently executed programs in UserAssist https://github.com/forensicmatt/MonitorUserAssist - Returns: + Yields: str: Path to emulator executables, may contains duplicate values """ path = r'Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist' @@ -447,6 +453,31 @@ class EmulatorManager(EmulatorManagerBase): uninstall = res.group(1) if res else uninstall yield uninstall + @staticmethod + def iter_running_emulator(): + """ + Yields: + str: Path to emulator executables, may contains duplicate values + """ + try: + import psutil + except ModuleNotFoundError: + return + # Since this is a one-time-usage, we access psutil._psplatform.Process directly + # to bypass the call of psutil.Process.is_running(). + # This only costs about 0.017s. + for pid in psutil.pids(): + proc = psutil._psplatform.Process(pid) + try: + exe = proc.cmdline() + exe = exe[0].replace(r'\\', '/').replace('\\', '/') + except (psutil.AccessDenied, IndexError): + # psutil.AccessDenied + continue + + if Emulator.is_emulator(exe): + yield exe + @cached_property def all_emulators(self) -> t.List[Emulator]: """ @@ -474,7 +505,7 @@ class EmulatorManager(EmulatorManagerBase): exe.add(ld) # Uninstall registry - for uninstall in self.iter_uninstall_registry(): + for uninstall in EmulatorManager.iter_uninstall_registry(): # Find emulator executable from uninstaller for file in iter_folder(abspath(os.path.dirname(uninstall)), ext='.exe'): if Emulator.is_emulator(file) and os.path.exists(file): @@ -488,9 +519,14 @@ class EmulatorManager(EmulatorManagerBase): if Emulator.is_emulator(file) and os.path.exists(file): exe.add(file) + # Running + for file in EmulatorManager.iter_running_emulator(): + if os.path.exists(file): + exe.add(file) + + # De-redundancy exe = [Emulator(path).path for path in exe if Emulator.is_emulator(path)] - exe = sorted(set(exe)) - exe = [Emulator(path) for path in exe] + exe = [Emulator(path) for path in remove_duplicated_path(exe)] return exe @cached_property diff --git a/module/device/platform/platform_base.py b/module/device/platform/platform_base.py index e48da238c..02aba6698 100644 --- a/module/device/platform/platform_base.py +++ b/module/device/platform/platform_base.py @@ -6,7 +6,8 @@ from pydantic import BaseModel from module.base.decorator import cached_property, del_cached_property from module.base.utils import SelectedGrids from module.device.connection import Connection -from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase +from module.device.method.utils import get_serial_pair +from module.device.platform.emulator_base import EmulatorInstanceBase, EmulatorManagerBase, remove_duplicated_path from module.logger import logger @@ -47,8 +48,20 @@ class PlatformBase(Connection, EmulatorManagerBase): @cached_property def emulator_info(self) -> EmulatorInfo: emulator = self.config.EmulatorInfo_Emulator - name = str(self.config.EmulatorInfo_name).strip().replace('\n', '') - path = str(self.config.EmulatorInfo_path).strip().replace('\n', '') + if emulator == 'auto': + emulator = '' + + def parse_info(value): + if isinstance(value, str): + value = value.strip().replace('\n', '') + if value in ['None', 'False', 'True']: + value = '' + return value + else: + return '' + + name = parse_info(self.config.EmulatorInfo_name) + path = parse_info(self.config.EmulatorInfo_path) return EmulatorInfo( emulator=emulator, @@ -68,8 +81,14 @@ class PlatformBase(Connection, EmulatorManagerBase): path=data.path, name=data.name, ) + # Redirect emulator-5554 to 127.0.0.1:5555 + serial = self.serial + port_serial, _ = get_serial_pair(self.serial) + if port_serial is not None: + serial = port_serial + instance = self.find_emulator_instance( - serial=str(self.config.Emulator_Serial).strip(), + serial=serial, name=data.name, path=data.path, emulator=data.emulator, @@ -117,7 +136,7 @@ class PlatformBase(Connection, EmulatorManagerBase): # Search by serial select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instance with {search_args}') + logger.warning(f'No emulator instance with {search_args}, serial invalid') return None if select.count == 1: instance = select[0] @@ -130,9 +149,9 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['name'] = name select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, name invalid') + search_args.pop('name') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -143,9 +162,9 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['path'] = path select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, path invalid') + search_args.pop('path') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -156,9 +175,28 @@ class PlatformBase(Connection, EmulatorManagerBase): search_args['type'] = emulator select = instances.select(**search_args) if select.count == 0: - logger.warning(f'No emulator instances with {search_args}') - return None - if select.count == 1: + logger.warning(f'No emulator instances with {search_args}, type invalid') + search_args.pop('type') + elif select.count == 1: + instance = select[0] + logger.hr('Emulator instance', level=2) + logger.info(f'Found emulator instance: {instance}') + return instance + + # Still too many instances, search from running emulators + running = remove_duplicated_path(list(self.iter_running_emulator())) + logger.info('Running emulators') + for exe in running: + logger.info(exe) + if len(running) == 1: + logger.info('Only one running emulator') + # Same as searching path + search_args['path'] = running[0] + select = instances.select(**search_args) + if select.count == 0: + logger.warning(f'No emulator instances with {search_args}, path invalid') + search_args.pop('path') + elif select.count == 1: instance = select[0] logger.hr('Emulator instance', level=2) logger.info(f'Found emulator instance: {instance}') @@ -167,9 +205,3 @@ class PlatformBase(Connection, EmulatorManagerBase): # Still too many instances logger.warning(f'Found multiple emulator instances with {search_args}') return None - - -if __name__ == '__main__': - self = PlatformBase('alas') - d = self.emulator_instance - print(d)