Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
50 changes: 31 additions & 19 deletions autowsgr/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,25 +268,37 @@ def __post_init__(self) -> None:
object.__setattr__(self, 'os_type', OSType.auto())

# 模拟器
if self.emulator_name is None:
object.__setattr__(
self,
'emulator_name',
self.emulator_type.default_emulator_name(self.os_type),
)
if self.emulator_start_cmd is None:
object.__setattr__(
self,
'emulator_start_cmd',
self.emulator_type.auto_emulator_path(self.os_type),
)
assert self.emulator_start_cmd is not None
if self.emulator_process_name is None:
object.__setattr__(
self,
'emulator_process_name',
os.path.basename(self.emulator_start_cmd),
)
if self.os_type == OSType.linux:
if self.emulator_name is None:
raise ValueError('WSL 需要显式设置 emulator_name')
if self.emulator_start_cmd is None:
raise ValueError('WSL 需要显式设置 emulator_start_cmd')
if self.emulator_process_name is None:
object.__setattr__(
self,
'emulator_process_name',
os.path.basename(self.emulator_start_cmd),
)
else:
if self.emulator_name is None:
object.__setattr__(
self,
'emulator_name',
self.emulator_type.default_emulator_name(self.os_type),
)
if self.emulator_start_cmd is None:
object.__setattr__(
self,
'emulator_start_cmd',
self.emulator_type.auto_emulator_path(self.os_type),
)
assert self.emulator_start_cmd is not None
if self.emulator_process_name is None:
object.__setattr__(
self,
'emulator_process_name',
os.path.basename(self.emulator_start_cmd),
)

# 游戏
object.__setattr__(self, 'app_name', self.game_app.app_name)
Expand Down
112 changes: 111 additions & 1 deletion autowsgr/timer/controllers/os_controller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import re
import shlex
import subprocess
import time
from typing import Protocol
Expand All @@ -12,7 +13,7 @@

from autowsgr.configs import UserConfig
from autowsgr.constants.custom_exceptions import CriticalErr
from autowsgr.types import EmulatorType
from autowsgr.types import EmulatorType, OSType
from autowsgr.utils.logger import Logger


Expand Down Expand Up @@ -300,3 +301,112 @@ def __get_mumu_info(self) -> dict:
except Exception as e:
self.logger.error(f'{cmd} {e}')
return {}


class LinuxController(OSController):
def __init__(self, config: UserConfig, logger: Logger) -> None:
self.logger = logger
self.is_wsl = OSType._is_wsl()

self.emulator_type = config.emulator_type
if config.emulator_name is None:
raise CriticalErr('WSL 需要显式设置 emulator_name')
if config.emulator_start_cmd is None:
raise CriticalErr('WSL 需要显式设置 emulator_start_cmd')
if config.emulator_process_name is None:
raise CriticalErr('WSL 需要显式设置 emulator_process_name')
self.emulator_name = config.emulator_name
self.emulator_start_cmd = config.emulator_start_cmd
self.emulator_process_name = config.emulator_process_name
self.dev_name = f'Android:///{self.emulator_name}'

def is_android_online(self) -> bool:
"""判断 timer 给定的设备是否在线"""
devices = self._adb_devices()
if self.emulator_name in devices:
return True
if self.is_wsl:
return self._is_windows_process_running()
try:
subprocess.run(
['pgrep', '-f', self.emulator_process_name],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return True
except subprocess.CalledProcessError:
return False

def kill_android(self) -> None:
"""强制终止模拟器进程"""
try:
if self.is_wsl:
result = subprocess.run(
['taskkill.exe', '/f', '/im', self.emulator_process_name],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise CriticalErr(result.stderr.strip() or result.stdout.strip())
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WSL 分支中 taskkill.exe 返回非 0 时直接抛 CriticalErr,会导致“模拟器未运行/已退出”时 kill_android 失败,从而让 connect_android() 的 restart_android() 在首次启动或进程已退出时无法继续。建议将“未找到进程/没有任务匹配”这类返回视为可忽略(例如检查 stderr/stdout 中的典型提示或 returncode),仅在真正的执行错误时才抛错。

Suggested change
raise CriticalErr(result.stderr.strip() or result.stdout.strip())
# 在 WSL 中,taskkill.exe 在未找到进程时也会返回非 0。
# 这类情况视为“已经退出”,不应中断后续重启逻辑。
output_text = f"{result.stderr or ''}\n{result.stdout or ''}".lower()
not_found_markers = [
'no instance', # e.g. "no instance(s) available"
'not found', # e.g. "process \"xxx\" not found"
'no tasks are running', # 英文提示
'没有运行的任务', # 简体中文提示
'没有运行的实例', # 简体中文提示
]
if any(marker in output_text for marker in not_found_markers):
self.logger.info(
f'未找到要终止的模拟器进程(可能已退出): {self.emulator_process_name}'
)
else:
raise CriticalErr(result.stderr.strip() or result.stdout.strip())

Copilot uses AI. Check for mistakes.
else:
subprocess.run(
['pkill', '-9', '-f', self.emulator_process_name],
check=True,
)
Comment on lines +353 to +356
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

非 WSL 分支对 pkill 使用 check=True;当目标进程不存在时 pkill 会以非 0 退出码返回,进而被包装成 CriticalErr,导致 restart_android() 在模拟器未运行时无法启动。建议对“未找到进程”的退出码做容错(例如去掉 check=True / 捕获 CalledProcessError 并忽略 returncode=1),只在真正执行失败时抛错。

Suggested change
subprocess.run(
['pkill', '-9', '-f', self.emulator_process_name],
check=True,
)
try:
subprocess.run(
['pkill', '-9', '-f', self.emulator_process_name],
check=True,
)
except subprocess.CalledProcessError as e:
# pkill 返回 1 表示未找到匹配进程,这种情况不视为错误
if e.returncode == 1:
self.logger.info(
f'未找到需要终止的模拟器进程: {self.emulator_process_name}'
)
else:
raise

Copilot uses AI. Check for mistakes.
self.logger.info(f'已终止模拟器进程: {self.emulator_process_name}')
except Exception as e:
raise CriticalErr(f'终止模拟器进程失败: {e}')

def start_android(self) -> None:
"""启动模拟器"""
try:
subprocess.Popen(self._split_command(self.emulator_start_cmd))
self.logger.info(f'正在启动模拟器: {self.emulator_start_cmd}')
start_time = time.time()
while not self.is_android_online():
time.sleep(1)
if time.time() - start_time > 120:
raise TimeoutError('模拟器启动超时!')
except Exception as e:
raise CriticalErr(f'启动模拟器失败: {e}')

def _adb_devices(self) -> list[str]:
try:
from airtest.core.android.adb import ADB

adb = ADB().get_adb_path()
result = subprocess.run(
[adb, 'devices'],
capture_output=True,
text=True,
check=True,
)
except Exception as e:
self.logger.error(f'adb devices 失败: {e}')
return []

devices = []
for line in result.stdout.splitlines():
line = line.strip()
if not line or line.startswith('List of devices'):
continue
parts = line.split()
if len(parts) >= 2 and parts[1] == 'device':
devices.append(parts[0])
return devices

def _is_windows_process_running(self) -> bool:
result = subprocess.run(
['tasklist.exe', '/fi', f'IMAGENAME eq {self.emulator_process_name}'],
capture_output=True,
text=True,
)
output = (result.stdout or '').lower()
if 'no tasks' in output:
return False
return self.emulator_process_name.lower() in output

@staticmethod
def _split_command(command: str) -> list[str]:
return shlex.split(command)
19 changes: 18 additions & 1 deletion autowsgr/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class OcrBackend(StrEnum):

class OSType(StrEnum):
windows = 'Windows'
linux = 'Linux'
linux = 'linux'
macos = 'macOS'

@classmethod
Expand All @@ -55,8 +55,25 @@ def auto(cls) -> 'OSType':
return OSType.windows
if sys.platform == 'darwin':
return OSType.macos
if sys.platform.startswith('linux'):
if cls._is_wsl():
return OSType.linux
Comment on lines +59 to +60
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSType.auto() 现在在 WSL 上会返回 OSType.linux,但当前 Timer.initialize_controllers() 的 adapter_fun 只包含 windows/macos(见 autowsgr/timer/timer.py:72-76),因此在 WSL 环境下会在 adapter_fun[self.config.os_type] 处触发 KeyError。建议同时把 LinuxController 接入 controller 适配表(并在 controllers/init.py 导出),否则会出现“检测到 WSL 但无法启动”的运行时错误。

Suggested change
if cls._is_wsl():
return OSType.linux
# 在 WSL 环境下,当前仍复用 Windows 的实现,返回 OSType.windows
if cls._is_wsl():
return OSType.windows

Copilot uses AI. Check for mistakes.
raise ValueError('暂不支持非 WSL 的 Linux 系统')
raise ValueError(f'不支持的操作系统 {sys.platform}')

@staticmethod
def _is_wsl() -> bool:
if os.environ.get('WSL_DISTRO_NAME') or os.environ.get('WSL_INTEROP'):
return True
for path in ('/proc/sys/kernel/osrelease', '/proc/version'):
try:
with open(path, encoding='utf-8', errors='ignore') as handle:
if 'microsoft' in handle.read().lower():
return True
except OSError:
continue
return False


class EmulatorType(StrEnum):
leidian = '雷电'
Expand Down