Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opt: Add start/stop LDPlayer,NoxPlayer,BlueStack4,MEmuPlayer support. #3867

Merged
merged 8 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions alas.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ def run(self, command):
logger.warning(e)
self.config.task_call('Restart')
return True
except EmulatorNotRunningError as e:
logger.warning(e)
self.device.emulator_start()
self.run('start')
return True
except (GameStuckError, GameTooManyClickError) as e:
logger.error(e)
self.save_error_log()
Expand Down
16 changes: 16 additions & 0 deletions module/device/platform/emulator_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ def MuMuPlayer12_id(self):

return None

@cached_property
def LDPlayer_id(self):
"""
Convert LDPlayer instance name to instance id.
Example names:
leidian0
leidian1

Returns:
int: Instance ID, or None if this is not a LDPlayer instance
"""
res = re.search(r'leidian(\d+)', self.name)
if res:
return int(res.group(1))

return None

class EmulatorBase:
# Values here must match those in argument.yaml EmulatorInfo.Emulator.option
Expand Down
6 changes: 3 additions & 3 deletions module/device/platform/emulator_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def path_to_type(cls, path: str) -> str:
return cls.NoxPlayer64
else:
return cls.NoxPlayer
if exe == 'bluestacks.exe':
if dir1 in ['bluestacks', 'bluestacks_cn']:
if exe in ['bluestacks.exe', 'bluestacksgp.exe']:
if dir1 in ['bluestacks', 'bluestacks_cn', 'bluestackscn']:
return cls.BlueStacks4
elif dir1 in ['bluestacks_nxt', 'bluestacks_nxt_cn']:
return cls.BlueStacks5
Expand Down Expand Up @@ -224,7 +224,7 @@ def iter_instances(self):
elif self == Emulator.BlueStacks4:
# ../Engine/Android
regex = re.compile(r'^Android')
for folder in self.list_folder('../Engine', is_dir=True):
for folder in self.list_folder('./Engine/ProgramData/Engine', is_dir=True):
folder = os.path.basename(folder)
res = regex.match(folder)
if not res:
Expand Down
178 changes: 105 additions & 73 deletions module/device/platform/platform_windows.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ctypes
import re
import subprocess
import win32process
import win32con
import win32gui

import psutil

Expand All @@ -17,44 +18,34 @@ class EmulatorUnknown(Exception):
pass


def get_focused_window():
return ctypes.windll.user32.GetForegroundWindow()


def set_focus_window(hwnd):
ctypes.windll.user32.SetForegroundWindow(hwnd)


def minimize_window(hwnd):
ctypes.windll.user32.ShowWindow(hwnd, 6)


def get_window_title(hwnd):
"""Returns the window title as a string."""
text_len_in_characters = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
string_buffer = ctypes.create_unicode_buffer(
text_len_in_characters + 1) # +1 for the \0 at the end of the null-terminated string.
ctypes.windll.user32.GetWindowTextW(hwnd, string_buffer, text_len_in_characters + 1)
return string_buffer.value


def flash_window(hwnd, flash=True):
ctypes.windll.user32.FlashWindow(hwnd, flash)
class HwndNotFoundError(Exception):
pass


class PlatformWindows(PlatformBase, EmulatorManager):
@classmethod
def execute(cls, command):
def __init__(self, config):
super().__init__(config)
self.process: tuple = None
self.hwnds: list[int] = None

def execute(self, command: str):
"""
Args:
command (str):

Returns:
subprocess.Popen:
win32process.CreateProcess -> tuple(Incomplete, Incomplete, int, int):
"""
command = command.replace(r"\\", "/").replace("\\", "/").replace('"', '"')
logger.info(f'Execute: {command}')
return subprocess.Popen(command, close_fds=True) # only work on Windows
startupinfo = win32process.STARTUPINFO()
startupinfo.dwFlags = win32con.STARTF_USESHOWWINDOW
self.process = win32process.CreateProcess( #Only work for Windows.
None,command,None,None,False,
win32con.DETACHED_PROCESS,
None,None,startupinfo
)
return True

@classmethod
def kill_process_by_regex(cls, regex: str) -> int:
Expand All @@ -77,12 +68,44 @@ def kill_process_by_regex(cls, regex: str) -> int:
count += 1

return count

def gethwnds(self, pid: int):
def callback(hwnd: int, hwnds: list):
_, fpid = win32process.GetWindowThreadProcessId(hwnd)
if fpid == pid:
hwnds.append(hwnd)
return True
hwnds = []
win32gui.EnumWindows(callback, hwnds)
if not hwnds:
logger.critical(
"Hwnd not found! \n"
"1.Perhaps emulator was killed. \n"
"2.Environment has something wrong. Please check the running environment. "
)
raise HwndNotFoundError("Hwnd not found")
return hwnds

def _switch_window(self, hwnd:int, arg:int):
win32gui.ShowWindow(hwnd,arg)

def switch_window(self, arg: int):
if self.process is None:
return
for hwnd in self.hwnds:
if not win32gui.IsWindow(hwnd):
continue
if win32gui.GetParent(hwnd):
continue
if set(win32gui.GetWindowRect(hwnd)) == {0}:
continue
self._switch_window(hwnd, arg)# May arg will be sent in.

def _emulator_start(self, instance: EmulatorInstance):
"""
Start a emulator without error handling
"""
exe = instance.emulator.path
exe: str = instance.emulator.path
if instance == Emulator.MuMuPlayer:
# NemuPlayer.exe
self.execute(exe)
Expand All @@ -94,24 +117,31 @@ def _emulator_start(self, instance: EmulatorInstance):
if instance.MuMuPlayer12_id is None:
logger.warning(f'Cannot get MuMu instance index from name {instance.name}')
self.execute(f'"{exe}" -v {instance.MuMuPlayer12_id}')
elif instance == Emulator.LDPlayerFamily:
# LDPlayer.exe index=0
self.execute(f'"{exe}" index={instance.LDPlayer_id}')
elif instance == Emulator.NoxPlayerFamily:
# Nox.exe -clone:Nox_1
self.execute(f'"{exe}" -clone:{instance.name}')
elif instance == Emulator.BlueStacks5:
# HD-Player.exe -instance Pie64
self.execute(f'"{exe}" -instance {instance.name}')
elif instance == Emulator.BlueStacks4:
# BlueStacks\Client\Bluestacks.exe -vmname Android_1
# Bluestacks.exe -vmname Android_1
self.execute(f'"{exe}" -vmname {instance.name}')
elif instance == Emulator.MEmuPlayer:
# MEmu.exe MEmu_0
self.execute(f'"{exe}" {instance.name}')
else:
raise EmulatorUnknown(f'Cannot start an unknown emulator instance: {instance}')

def _emulator_stop(self, instance: EmulatorInstance):
"""
Stop a emulator without error handling
"""
import os
logger.hr('Emulator stop', level=2)
exe = instance.emulator.path
exe: str = instance.emulator.path
if instance == Emulator.MuMuPlayer:
# MuMu6 does not have multi instance, kill one means kill all
# Has 4 processes
Expand Down Expand Up @@ -141,26 +171,36 @@ def _emulator_stop(self, instance: EmulatorInstance):
rf')'
)
elif instance == Emulator.MuMuPlayer12:
# MuMu 12 has 2 processes:
# E:\ProgramFiles\Netease\MuMuPlayer-12.0\shell\MuMuPlayer.exe -v 0
# "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMHeadless.exe" --comment MuMuPlayer-12.0-0 --startvm xxx
# E:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe api -v 1 shutdown_player
if instance.MuMuPlayer12_id is None:
logger.warning(f'Cannot get MuMu instance index from name {instance.name}')
self.execute(f'"{os.path.join(os.path.dirname(exe),"MuMuManager.exe")}" api -v {instance.MuMuPlayer12_id} shutdown_player')
elif instance == Emulator.LDPlayerFamily:
# E:\Program Files\leidian\LDPlayer9\dnconsole.exe quit --index 0
self.execute(f'"{os.path.join(os.path.dirname(exe),"dnconsole.exe")}" quit --index {instance.LDPlayer_id}')
elif instance == Emulator.NoxPlayerFamily:
# Nox.exe -clone:Nox_1 -quit
self.execute(f'"{exe}" -clone:{instance.name} -quit')
elif instance == Emulator.BlueStacks5:
# BlueStack has 2 processes
# C:\Program Files\BlueStacks_nxt_cn\HD-Player.exe --instance Pie64
# C:\Program Files\BlueStacks_nxt_cn\BstkSVC.exe -Embedding
self.kill_process_by_regex(
rf'('
rf'MuMuVMMHeadless.exe.*--comment {instance.name}'
rf'|MuMuPlayer.exe.*-v {instance.MuMuPlayer12_id}'
rf'HD-Player.exe.*"--instance" "{instance.name}"'
rf'|BstkSVC.exe.*-Embedding'
rf')'
)
# There is also a shared service, no need to kill it
# "C:\Program Files\MuMuVMMVbox\Hypervisor\MuMuVMMSVC.exe" --Embedding
elif instance == Emulator.NoxPlayerFamily:
# Nox.exe -clone:Nox_1 -quit
self.execute(f'"{exe}" -clone:{instance.name} -quit')
elif instance == Emulator.BlueStacks4:
# E:\Program Files (x86)\BluestacksCN\bsconsole.exe quit --name Android
self.execute(f'"{os.path.join(os.path.dirname(exe),"bsconsole.exe")}" quit --name {instance.name}')
elif instance == Emulator.MEmuPlayer:
# F:\Program Files\Microvirt\MEmu\memuc.exe stop -n MEmu_0
self.execute(f'"{os.path.join(os.path.dirname(exe),"memuc.exe")}" stop -n {instance.name}')
else:
raise EmulatorUnknown(f'Cannot stop an unknown emulator instance: {instance}')

def _emulator_function_wrapper(self, func):
def _emulator_function_wrapper(self, func: callable):
"""
Args:
func (callable): _emulator_start or _emulator_stop
Expand Down Expand Up @@ -190,10 +230,8 @@ def emulator_start_watch(self):
bool: True if startup completed
False if timeout
"""
logger.hr('Emulator start', level=2)
current_window = get_focused_window()
logger.info("Emulator starting...")
serial = self.emulator_instance.serial
logger.info(f'Current window: {current_window}')

def adb_connect():
m = self.adb_client.connect(self.serial)
Expand Down Expand Up @@ -222,24 +260,13 @@ def show_package(m):

interval = Timer(0.5).start()
timeout = Timer(300).start()
new_window = 0
while 1:
interval.wait()
interval.reset()
if timeout.reached():
logger.warning(f'Emulator start timeout')
return False

# Check emulator window showing up
# logger.info([get_focused_window(), get_window_title(get_focused_window())])
if current_window != 0 and new_window == 0:
new_window = get_focused_window()
if current_window != new_window:
logger.info(f'New window showing up: {new_window}, focus back')
set_focus_window(current_window)
else:
new_window = 0

# Check device connection
devices = self.list_device().select(serial=serial)
# logger.info(devices)
Expand Down Expand Up @@ -277,24 +304,17 @@ def show_package(m):
# All check passed
break

if new_window != 0 and new_window != current_window:
logger.info(f'Minimize new window: {new_window}')
minimize_window(new_window)
if current_window:
logger.info(f'De-flash current window: {current_window}')
flash_window(current_window, flash=False)
if new_window:
logger.info(f'Flash new window: {new_window}')
flash_window(new_window, flash=True)
logger.info('Emulator start completed')
# Check emulator process and hwnds
self.hwnds = self.gethwnds(self.process[2])

logger.info(f'Emulator start completed')
logger.info(f'Emulator Process: {self.process}')
logger.info(f'Emulator hwnds: {self.hwnds}')
return True

def emulator_start(self):
logger.hr('Emulator start', level=1)
for _ in range(3):
# Stop
if not self._emulator_function_wrapper(self._emulator_stop):
return False
# Start
if self._emulator_function_wrapper(self._emulator_start):
# Success
Expand All @@ -312,10 +332,22 @@ def emulator_start(self):

def emulator_stop(self):
logger.hr('Emulator stop', level=1)
return self._emulator_function_wrapper(self._emulator_stop)

for _ in range(3):
# Stop
if self._emulator_function_wrapper(self._emulator_stop):
# Success
return True
else:
# Failed to stop, start and stop again
if self._emulator_function_wrapper(self._emulator_start):
continue
else:
return False

logger.error('Failed to stop emulator 3 times, stopped')
return False

if __name__ == '__main__':
self = PlatformWindows('alas')
d = self.emulator_instance
print(d)
print(d)