kotonebot 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -312
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/context/task_action.py +183 -183
- kotonebot/backend/core.py +86 -129
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +535 -535
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +369 -529
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -43
- kotonebot/client/host/adb_common.py +101 -107
- kotonebot/client/host/custom.py +118 -118
- kotonebot/client/host/leidian_host.py +196 -196
- kotonebot/client/host/mumu12_host.py +353 -353
- kotonebot/client/host/protocol.py +214 -214
- kotonebot/client/host/windows_common.py +73 -58
- kotonebot/client/implements/__init__.py +65 -70
- kotonebot/client/implements/adb.py +89 -89
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -188
- kotonebot/client/implements/uiautomator2.py +85 -85
- kotonebot/client/implements/windows/__init__.py +1 -0
- kotonebot/client/implements/windows/print_window.py +133 -0
- kotonebot/client/implements/windows/send_message.py +324 -0
- kotonebot/client/implements/{windows.py → windows/windows.py} +175 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +103 -96
- kotonebot/config/config.py +61 -0
- kotonebot/config/manager.py +36 -36
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/{tools → devtools}/mirror.py +354 -354
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +76 -76
- kotonebot/interop/win/__init__.py +13 -9
- kotonebot/interop/win/_mouse.py +310 -310
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/interop/win/window.py +89 -0
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +19 -17
- kotonebot/primitives/geometry.py +1067 -862
- kotonebot/primitives/visual.py +143 -63
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -88
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -54
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/METADATA +84 -82
- kotonebot-0.7.0.dist-info/RECORD +109 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/WHEEL +1 -1
- kotonebot-0.7.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot/client/implements/adb_raw.py +0 -163
- kotonebot-0.5.0.dist-info/RECORD +0 -71
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import socket
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import Callable, TypeVar, Protocol, Any, Generic
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
|
|
7
|
-
from kotonebot import logging
|
|
8
|
-
from kotonebot.client import Device, DeviceImpl
|
|
9
|
-
|
|
10
|
-
from kotonebot.util import Countdown, Interval
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
# https://github.com/python/typing/issues/769#issuecomment-903760354
|
|
14
|
-
_T = TypeVar("_T")
|
|
15
|
-
def copy_type(_: _T) -> Callable[[Any], _T]:
|
|
16
|
-
return lambda x: x
|
|
17
|
-
|
|
18
|
-
# --- 定义专用的 HostConfig 数据类 ---
|
|
19
|
-
@dataclass
|
|
20
|
-
class AdbHostConfig:
|
|
21
|
-
"""由外部为基于 ADB 的主机提供的配置。"""
|
|
22
|
-
timeout: float = 180
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class WindowsHostConfig:
|
|
26
|
-
"""由外部为 Windows 实现提供配置。"""
|
|
27
|
-
window_title: str
|
|
28
|
-
ahk_exe_path: str
|
|
29
|
-
|
|
30
|
-
@dataclass
|
|
31
|
-
class RemoteWindowsHostConfig:
|
|
32
|
-
"""由外部为远程 Windows 实现提供配置。"""
|
|
33
|
-
windows_host_config: WindowsHostConfig
|
|
34
|
-
host: str
|
|
35
|
-
port: int
|
|
36
|
-
|
|
37
|
-
# --- 使用泛型改造 Instance 协议 ---
|
|
38
|
-
T_HostConfig = TypeVar("T_HostConfig")
|
|
39
|
-
|
|
40
|
-
def tcp_ping(host: str, port: int, timeout: float = 1.0) -> bool:
|
|
41
|
-
"""
|
|
42
|
-
通过 TCP ping 检查主机和端口是否可达。
|
|
43
|
-
|
|
44
|
-
:param host: 主机名或 IP 地址
|
|
45
|
-
:param port: 端口号
|
|
46
|
-
:param timeout: 超时时间(秒)
|
|
47
|
-
:return: 如果主机和端口可达,则返回 True,否则返回 False
|
|
48
|
-
"""
|
|
49
|
-
logger.debug('TCP ping %s:%d...', host, port)
|
|
50
|
-
try:
|
|
51
|
-
with socket.create_connection((host, port), timeout):
|
|
52
|
-
logger.debug('TCP ping %s:%d success.', host, port)
|
|
53
|
-
return True
|
|
54
|
-
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
55
|
-
logger.debug('TCP ping %s:%d failed.', host, port)
|
|
56
|
-
return False
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class Instance(Generic[T_HostConfig], ABC):
|
|
60
|
-
"""
|
|
61
|
-
代表一个可运行环境的实例(如一个模拟器)。
|
|
62
|
-
使用泛型来约束 create_device 方法的配置参数类型。
|
|
63
|
-
"""
|
|
64
|
-
def __init__(self,
|
|
65
|
-
id: str,
|
|
66
|
-
name: str,
|
|
67
|
-
adb_port: int | None = None,
|
|
68
|
-
adb_ip: str = '127.0.0.1',
|
|
69
|
-
adb_name: str | None = None
|
|
70
|
-
):
|
|
71
|
-
self.id: str = id
|
|
72
|
-
self.name: str = name
|
|
73
|
-
self.adb_port: int | None = adb_port
|
|
74
|
-
self.adb_ip: str = adb_ip
|
|
75
|
-
self.adb_name: str | None = adb_name
|
|
76
|
-
|
|
77
|
-
def require_adb_port(self) -> int:
|
|
78
|
-
if self.adb_port is None:
|
|
79
|
-
raise ValueError("ADB port is not set and is required.")
|
|
80
|
-
return self.adb_port
|
|
81
|
-
|
|
82
|
-
@abstractmethod
|
|
83
|
-
def refresh(self):
|
|
84
|
-
"""
|
|
85
|
-
刷新实例信息,如 ADB 端口号等。
|
|
86
|
-
"""
|
|
87
|
-
raise NotImplementedError()
|
|
88
|
-
|
|
89
|
-
@abstractmethod
|
|
90
|
-
def start(self):
|
|
91
|
-
"""
|
|
92
|
-
启动模拟器实例。
|
|
93
|
-
"""
|
|
94
|
-
raise NotImplementedError()
|
|
95
|
-
|
|
96
|
-
@abstractmethod
|
|
97
|
-
def stop(self):
|
|
98
|
-
"""
|
|
99
|
-
停止模拟器实例。
|
|
100
|
-
"""
|
|
101
|
-
raise NotImplementedError()
|
|
102
|
-
|
|
103
|
-
@abstractmethod
|
|
104
|
-
def running(self) -> bool:
|
|
105
|
-
raise NotImplementedError()
|
|
106
|
-
|
|
107
|
-
@abstractmethod
|
|
108
|
-
def create_device(self, impl: DeviceImpl, host_config: T_HostConfig) -> Device:
|
|
109
|
-
"""
|
|
110
|
-
根据实现名称和类型化的主机配置创建设备。
|
|
111
|
-
|
|
112
|
-
:param impl: 设备实现的名称。
|
|
113
|
-
:param host_config: 一个类型化的数据对象,包含创建所需的所有外部配置。
|
|
114
|
-
:return: 配置好的 Device 实例。
|
|
115
|
-
"""
|
|
116
|
-
raise NotImplementedError()
|
|
117
|
-
|
|
118
|
-
# TODO: [refactor] 这个方法不应该挂在 Instance,而是 AndroidEmulatorInstance 上
|
|
119
|
-
def wait_available(self, timeout: float = 180):
|
|
120
|
-
from adbutils import adb, AdbTimeout, AdbError
|
|
121
|
-
from adbutils._device import AdbDevice
|
|
122
|
-
|
|
123
|
-
logger.info('Starting to wait for emulator %s(127.0.0.1:%d) to be available...', self.name, self.adb_port)
|
|
124
|
-
state = 0
|
|
125
|
-
port = self.require_adb_port()
|
|
126
|
-
emulator_name = self.adb_name
|
|
127
|
-
cd = Countdown(timeout)
|
|
128
|
-
it = Interval(1)
|
|
129
|
-
d: AdbDevice | None = None
|
|
130
|
-
while True:
|
|
131
|
-
if cd.expired():
|
|
132
|
-
raise TimeoutError(f'Emulator "{self.name}" is not available.')
|
|
133
|
-
it.wait()
|
|
134
|
-
try:
|
|
135
|
-
match state:
|
|
136
|
-
case 0:
|
|
137
|
-
logger.debug('Ping emulator %s(127.0.0.1:%d)...', self.name, port)
|
|
138
|
-
if tcp_ping('127.0.0.1', port):
|
|
139
|
-
logger.debug('Ping emulator %s(127.0.0.1:%d) success.', self.name, port)
|
|
140
|
-
state = 1
|
|
141
|
-
case 1:
|
|
142
|
-
logger.debug('Connecting to emulator %s(127.0.0.1:%d)...', self.name, port)
|
|
143
|
-
if adb.connect(f'127.0.0.1:{port}', timeout=0.5):
|
|
144
|
-
logger.debug('Connect to emulator %s(127.0.0.1:%d) success.', self.name, port)
|
|
145
|
-
state = 2
|
|
146
|
-
case 2:
|
|
147
|
-
logger.debug('Getting device list...')
|
|
148
|
-
if devices := adb.device_list():
|
|
149
|
-
logger.debug('Get device list success. devices=%s', devices)
|
|
150
|
-
# emulator_name 用于适配雷电模拟器
|
|
151
|
-
# 雷电模拟器启动后,在上方的列表中并不会出现 127.0.0.1:5555,而是 emulator-5554
|
|
152
|
-
d = next(
|
|
153
|
-
(d for d in devices if d.serial == f'127.0.0.1:{port}' or d.serial == emulator_name),
|
|
154
|
-
None
|
|
155
|
-
)
|
|
156
|
-
if d:
|
|
157
|
-
logger.debug('Get target device success. d=%s', d)
|
|
158
|
-
state = 3
|
|
159
|
-
case 3:
|
|
160
|
-
if not d:
|
|
161
|
-
logger.warning('Device is None.')
|
|
162
|
-
state = 0
|
|
163
|
-
continue
|
|
164
|
-
logger.debug('Waiting for device state...')
|
|
165
|
-
if d.get_state() == 'device':
|
|
166
|
-
logger.debug('Device state ready. state=%s', d.get_state())
|
|
167
|
-
state = 4
|
|
168
|
-
case 4:
|
|
169
|
-
logger.debug('Waiting for device boot completed...')
|
|
170
|
-
if not d:
|
|
171
|
-
logger.warning('Device is None.')
|
|
172
|
-
state = 0
|
|
173
|
-
continue
|
|
174
|
-
ret = d.shell('getprop sys.boot_completed')
|
|
175
|
-
if isinstance(ret, str) and ret.strip() == '1':
|
|
176
|
-
logger.debug('Device boot completed. ret=%s', ret)
|
|
177
|
-
state = 5
|
|
178
|
-
case 5:
|
|
179
|
-
if not d:
|
|
180
|
-
logger.warning('Device is None.')
|
|
181
|
-
state = 0
|
|
182
|
-
continue
|
|
183
|
-
app = d.app_current()
|
|
184
|
-
logger.debug('Waiting for launcher... (current=%s)', app)
|
|
185
|
-
if app and 'launcher' in app.package:
|
|
186
|
-
logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
|
|
187
|
-
state = 6
|
|
188
|
-
case 6:
|
|
189
|
-
break
|
|
190
|
-
except (AdbError, AdbTimeout):
|
|
191
|
-
state = 1
|
|
192
|
-
continue
|
|
193
|
-
time.sleep(1)
|
|
194
|
-
logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
|
|
195
|
-
|
|
196
|
-
def __repr__(self) -> str:
|
|
197
|
-
return f'{self.__class__.__name__}(name="{self.name}", id="{self.id}", adb="{self.adb_ip}:{self.adb_port}"({self.adb_name}))'
|
|
198
|
-
|
|
199
|
-
Recipe = TypeVar('Recipe', bound=str)
|
|
200
|
-
class HostProtocol(Generic[Recipe], Protocol):
|
|
201
|
-
@staticmethod
|
|
202
|
-
def installed() -> bool: ...
|
|
203
|
-
|
|
204
|
-
@staticmethod
|
|
205
|
-
def list() -> list[Instance]: ...
|
|
206
|
-
|
|
207
|
-
@staticmethod
|
|
208
|
-
def query(*, id: str) -> Instance | None: ...
|
|
209
|
-
|
|
210
|
-
@staticmethod
|
|
211
|
-
def recipes() -> 'list[Recipe]': ...
|
|
212
|
-
|
|
213
|
-
if __name__ == '__main__':
|
|
214
|
-
pass
|
|
1
|
+
import time
|
|
2
|
+
import socket
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Callable, TypeVar, Protocol, Any, Generic
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from kotonebot import logging
|
|
8
|
+
from kotonebot.client import Device, DeviceImpl
|
|
9
|
+
|
|
10
|
+
from kotonebot.util import Countdown, Interval
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
# https://github.com/python/typing/issues/769#issuecomment-903760354
|
|
14
|
+
_T = TypeVar("_T")
|
|
15
|
+
def copy_type(_: _T) -> Callable[[Any], _T]:
|
|
16
|
+
return lambda x: x
|
|
17
|
+
|
|
18
|
+
# --- 定义专用的 HostConfig 数据类 ---
|
|
19
|
+
@dataclass
|
|
20
|
+
class AdbHostConfig:
|
|
21
|
+
"""由外部为基于 ADB 的主机提供的配置。"""
|
|
22
|
+
timeout: float = 180
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class WindowsHostConfig:
|
|
26
|
+
"""由外部为 Windows 实现提供配置。"""
|
|
27
|
+
window_title: str
|
|
28
|
+
ahk_exe_path: str
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class RemoteWindowsHostConfig:
|
|
32
|
+
"""由外部为远程 Windows 实现提供配置。"""
|
|
33
|
+
windows_host_config: WindowsHostConfig
|
|
34
|
+
host: str
|
|
35
|
+
port: int
|
|
36
|
+
|
|
37
|
+
# --- 使用泛型改造 Instance 协议 ---
|
|
38
|
+
T_HostConfig = TypeVar("T_HostConfig")
|
|
39
|
+
|
|
40
|
+
def tcp_ping(host: str, port: int, timeout: float = 1.0) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
通过 TCP ping 检查主机和端口是否可达。
|
|
43
|
+
|
|
44
|
+
:param host: 主机名或 IP 地址
|
|
45
|
+
:param port: 端口号
|
|
46
|
+
:param timeout: 超时时间(秒)
|
|
47
|
+
:return: 如果主机和端口可达,则返回 True,否则返回 False
|
|
48
|
+
"""
|
|
49
|
+
logger.debug('TCP ping %s:%d...', host, port)
|
|
50
|
+
try:
|
|
51
|
+
with socket.create_connection((host, port), timeout):
|
|
52
|
+
logger.debug('TCP ping %s:%d success.', host, port)
|
|
53
|
+
return True
|
|
54
|
+
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
55
|
+
logger.debug('TCP ping %s:%d failed.', host, port)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Instance(Generic[T_HostConfig], ABC):
|
|
60
|
+
"""
|
|
61
|
+
代表一个可运行环境的实例(如一个模拟器)。
|
|
62
|
+
使用泛型来约束 create_device 方法的配置参数类型。
|
|
63
|
+
"""
|
|
64
|
+
def __init__(self,
|
|
65
|
+
id: str,
|
|
66
|
+
name: str,
|
|
67
|
+
adb_port: int | None = None,
|
|
68
|
+
adb_ip: str = '127.0.0.1',
|
|
69
|
+
adb_name: str | None = None
|
|
70
|
+
):
|
|
71
|
+
self.id: str = id
|
|
72
|
+
self.name: str = name
|
|
73
|
+
self.adb_port: int | None = adb_port
|
|
74
|
+
self.adb_ip: str = adb_ip
|
|
75
|
+
self.adb_name: str | None = adb_name
|
|
76
|
+
|
|
77
|
+
def require_adb_port(self) -> int:
|
|
78
|
+
if self.adb_port is None:
|
|
79
|
+
raise ValueError("ADB port is not set and is required.")
|
|
80
|
+
return self.adb_port
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def refresh(self):
|
|
84
|
+
"""
|
|
85
|
+
刷新实例信息,如 ADB 端口号等。
|
|
86
|
+
"""
|
|
87
|
+
raise NotImplementedError()
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def start(self):
|
|
91
|
+
"""
|
|
92
|
+
启动模拟器实例。
|
|
93
|
+
"""
|
|
94
|
+
raise NotImplementedError()
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def stop(self):
|
|
98
|
+
"""
|
|
99
|
+
停止模拟器实例。
|
|
100
|
+
"""
|
|
101
|
+
raise NotImplementedError()
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def running(self) -> bool:
|
|
105
|
+
raise NotImplementedError()
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def create_device(self, impl: DeviceImpl, host_config: T_HostConfig) -> Device:
|
|
109
|
+
"""
|
|
110
|
+
根据实现名称和类型化的主机配置创建设备。
|
|
111
|
+
|
|
112
|
+
:param impl: 设备实现的名称。
|
|
113
|
+
:param host_config: 一个类型化的数据对象,包含创建所需的所有外部配置。
|
|
114
|
+
:return: 配置好的 Device 实例。
|
|
115
|
+
"""
|
|
116
|
+
raise NotImplementedError()
|
|
117
|
+
|
|
118
|
+
# TODO: [refactor] 这个方法不应该挂在 Instance,而是 AndroidEmulatorInstance 上
|
|
119
|
+
def wait_available(self, timeout: float = 180):
|
|
120
|
+
from adbutils import adb, AdbTimeout, AdbError
|
|
121
|
+
from adbutils._device import AdbDevice
|
|
122
|
+
|
|
123
|
+
logger.info('Starting to wait for emulator %s(127.0.0.1:%d) to be available...', self.name, self.adb_port)
|
|
124
|
+
state = 0
|
|
125
|
+
port = self.require_adb_port()
|
|
126
|
+
emulator_name = self.adb_name
|
|
127
|
+
cd = Countdown(timeout)
|
|
128
|
+
it = Interval(1)
|
|
129
|
+
d: AdbDevice | None = None
|
|
130
|
+
while True:
|
|
131
|
+
if cd.expired():
|
|
132
|
+
raise TimeoutError(f'Emulator "{self.name}" is not available.')
|
|
133
|
+
it.wait()
|
|
134
|
+
try:
|
|
135
|
+
match state:
|
|
136
|
+
case 0:
|
|
137
|
+
logger.debug('Ping emulator %s(127.0.0.1:%d)...', self.name, port)
|
|
138
|
+
if tcp_ping('127.0.0.1', port):
|
|
139
|
+
logger.debug('Ping emulator %s(127.0.0.1:%d) success.', self.name, port)
|
|
140
|
+
state = 1
|
|
141
|
+
case 1:
|
|
142
|
+
logger.debug('Connecting to emulator %s(127.0.0.1:%d)...', self.name, port)
|
|
143
|
+
if adb.connect(f'127.0.0.1:{port}', timeout=0.5):
|
|
144
|
+
logger.debug('Connect to emulator %s(127.0.0.1:%d) success.', self.name, port)
|
|
145
|
+
state = 2
|
|
146
|
+
case 2:
|
|
147
|
+
logger.debug('Getting device list...')
|
|
148
|
+
if devices := adb.device_list():
|
|
149
|
+
logger.debug('Get device list success. devices=%s', devices)
|
|
150
|
+
# emulator_name 用于适配雷电模拟器
|
|
151
|
+
# 雷电模拟器启动后,在上方的列表中并不会出现 127.0.0.1:5555,而是 emulator-5554
|
|
152
|
+
d = next(
|
|
153
|
+
(d for d in devices if d.serial == f'127.0.0.1:{port}' or d.serial == emulator_name),
|
|
154
|
+
None
|
|
155
|
+
)
|
|
156
|
+
if d:
|
|
157
|
+
logger.debug('Get target device success. d=%s', d)
|
|
158
|
+
state = 3
|
|
159
|
+
case 3:
|
|
160
|
+
if not d:
|
|
161
|
+
logger.warning('Device is None.')
|
|
162
|
+
state = 0
|
|
163
|
+
continue
|
|
164
|
+
logger.debug('Waiting for device state...')
|
|
165
|
+
if d.get_state() == 'device':
|
|
166
|
+
logger.debug('Device state ready. state=%s', d.get_state())
|
|
167
|
+
state = 4
|
|
168
|
+
case 4:
|
|
169
|
+
logger.debug('Waiting for device boot completed...')
|
|
170
|
+
if not d:
|
|
171
|
+
logger.warning('Device is None.')
|
|
172
|
+
state = 0
|
|
173
|
+
continue
|
|
174
|
+
ret = d.shell('getprop sys.boot_completed')
|
|
175
|
+
if isinstance(ret, str) and ret.strip() == '1':
|
|
176
|
+
logger.debug('Device boot completed. ret=%s', ret)
|
|
177
|
+
state = 5
|
|
178
|
+
case 5:
|
|
179
|
+
if not d:
|
|
180
|
+
logger.warning('Device is None.')
|
|
181
|
+
state = 0
|
|
182
|
+
continue
|
|
183
|
+
app = d.app_current()
|
|
184
|
+
logger.debug('Waiting for launcher... (current=%s)', app)
|
|
185
|
+
if app and 'launcher' in app.package:
|
|
186
|
+
logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
|
|
187
|
+
state = 6
|
|
188
|
+
case 6:
|
|
189
|
+
break
|
|
190
|
+
except (AdbError, AdbTimeout):
|
|
191
|
+
state = 1
|
|
192
|
+
continue
|
|
193
|
+
time.sleep(1)
|
|
194
|
+
logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
|
|
195
|
+
|
|
196
|
+
def __repr__(self) -> str:
|
|
197
|
+
return f'{self.__class__.__name__}(name="{self.name}", id="{self.id}", adb="{self.adb_ip}:{self.adb_port}"({self.adb_name}))'
|
|
198
|
+
|
|
199
|
+
Recipe = TypeVar('Recipe', bound=str)
|
|
200
|
+
class HostProtocol(Generic[Recipe], Protocol):
|
|
201
|
+
@staticmethod
|
|
202
|
+
def installed() -> bool: ...
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def list() -> list[Instance]: ...
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def query(*, id: str) -> Instance | None: ...
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def recipes() -> 'list[Recipe]': ...
|
|
212
|
+
|
|
213
|
+
if __name__ == '__main__':
|
|
214
|
+
pass
|
|
@@ -1,58 +1,73 @@
|
|
|
1
|
-
from abc import ABC
|
|
2
|
-
from typing import Literal
|
|
3
|
-
from typing_extensions import assert_never
|
|
4
|
-
|
|
5
|
-
from kotonebot import logging
|
|
6
|
-
from kotonebot.client.device import WindowsDevice
|
|
7
|
-
from kotonebot.util import require_windows
|
|
8
|
-
from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
WindowsRecipes = Literal['windows', 'remote_windows']
|
|
12
|
-
|
|
13
|
-
# Windows 相关的配置类型联合
|
|
14
|
-
WindowsHostConfigs = WindowsHostConfig | RemoteWindowsHostConfig
|
|
15
|
-
|
|
16
|
-
class CommonWindowsCreateDeviceMixin(ABC):
|
|
17
|
-
"""
|
|
18
|
-
通用 Windows 创建设备的 Mixin。
|
|
19
|
-
该 Mixin 定义了创建 Windows 设备的通用接口。
|
|
20
|
-
"""
|
|
21
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
22
|
-
super().__init__(*args, **kwargs)
|
|
23
|
-
require_windows('CommonWindowsCreateDeviceMixin', self.__class__)
|
|
24
|
-
|
|
25
|
-
def create_device(self, recipe: WindowsRecipes, config: WindowsHostConfigs) -> Device:
|
|
26
|
-
"""
|
|
27
|
-
创建 Windows 设备。
|
|
28
|
-
"""
|
|
29
|
-
require_windows('CommonWindowsCreateDeviceMixin.create_device', self.__class__)
|
|
30
|
-
match recipe:
|
|
31
|
-
case 'windows':
|
|
32
|
-
if not isinstance(config, WindowsHostConfig):
|
|
33
|
-
raise ValueError(f"Expected WindowsHostConfig for 'windows' recipe, got {type(config)}")
|
|
34
|
-
from kotonebot.client.implements.windows import WindowsImpl
|
|
35
|
-
d = WindowsDevice()
|
|
36
|
-
impl = WindowsImpl(
|
|
37
|
-
device=d,
|
|
38
|
-
window_title=config.window_title,
|
|
39
|
-
ahk_exe_path=config.ahk_exe_path
|
|
40
|
-
)
|
|
41
|
-
d._screenshot = impl
|
|
42
|
-
d._touch = impl
|
|
43
|
-
return d
|
|
44
|
-
case '
|
|
45
|
-
if not isinstance(config,
|
|
46
|
-
raise ValueError(f"Expected
|
|
47
|
-
from kotonebot.client.implements.
|
|
48
|
-
d = WindowsDevice()
|
|
49
|
-
impl =
|
|
50
|
-
device=d,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from typing_extensions import assert_never
|
|
4
|
+
|
|
5
|
+
from kotonebot import logging
|
|
6
|
+
from kotonebot.client.device import WindowsDevice
|
|
7
|
+
from kotonebot.util import require_windows
|
|
8
|
+
from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
WindowsRecipes = Literal['windows', 'remote_windows', 'windows_background']
|
|
12
|
+
|
|
13
|
+
# Windows 相关的配置类型联合
|
|
14
|
+
WindowsHostConfigs = WindowsHostConfig | RemoteWindowsHostConfig
|
|
15
|
+
|
|
16
|
+
class CommonWindowsCreateDeviceMixin(ABC):
|
|
17
|
+
"""
|
|
18
|
+
通用 Windows 创建设备的 Mixin。
|
|
19
|
+
该 Mixin 定义了创建 Windows 设备的通用接口。
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
require_windows('CommonWindowsCreateDeviceMixin', self.__class__)
|
|
24
|
+
|
|
25
|
+
def create_device(self, recipe: WindowsRecipes, config: WindowsHostConfigs) -> Device:
|
|
26
|
+
"""
|
|
27
|
+
创建 Windows 设备。
|
|
28
|
+
"""
|
|
29
|
+
require_windows('CommonWindowsCreateDeviceMixin.create_device', self.__class__)
|
|
30
|
+
match recipe:
|
|
31
|
+
case 'windows':
|
|
32
|
+
if not isinstance(config, WindowsHostConfig):
|
|
33
|
+
raise ValueError(f"Expected WindowsHostConfig for 'windows' recipe, got {type(config)}")
|
|
34
|
+
from kotonebot.client.implements.windows import WindowsImpl
|
|
35
|
+
d = WindowsDevice()
|
|
36
|
+
impl = WindowsImpl(
|
|
37
|
+
device=d,
|
|
38
|
+
window_title=config.window_title,
|
|
39
|
+
ahk_exe_path=config.ahk_exe_path
|
|
40
|
+
)
|
|
41
|
+
d._screenshot = impl
|
|
42
|
+
d._touch = impl
|
|
43
|
+
return d
|
|
44
|
+
case 'windows_background':
|
|
45
|
+
if not isinstance(config, WindowsHostConfig):
|
|
46
|
+
raise ValueError(f"Expected WindowsHostConfig for 'windows' recipe, got {type(config)}")
|
|
47
|
+
from kotonebot.client.implements.windows import WindowsImpl
|
|
48
|
+
d = WindowsDevice()
|
|
49
|
+
impl = WindowsImpl(
|
|
50
|
+
device=d,
|
|
51
|
+
window_title=config.window_title,
|
|
52
|
+
ahk_exe_path=config.ahk_exe_path
|
|
53
|
+
)
|
|
54
|
+
from kotonebot.client.implements.windows.send_message import SendMessageImpl
|
|
55
|
+
from kotonebot.client.implements.windows.print_window import PrintWindowImpl
|
|
56
|
+
d._screenshot = PrintWindowImpl(d, config.window_title)
|
|
57
|
+
d._touch = SendMessageImpl(d, config.window_title)
|
|
58
|
+
return d
|
|
59
|
+
case 'remote_windows':
|
|
60
|
+
if not isinstance(config, RemoteWindowsHostConfig):
|
|
61
|
+
raise ValueError(f"Expected RemoteWindowsHostConfig for 'remote_windows' recipe, got {type(config)}")
|
|
62
|
+
from kotonebot.client.implements.remote_windows import RemoteWindowsImpl
|
|
63
|
+
d = WindowsDevice()
|
|
64
|
+
impl = RemoteWindowsImpl(
|
|
65
|
+
device=d,
|
|
66
|
+
host=config.host,
|
|
67
|
+
port=config.port
|
|
68
|
+
)
|
|
69
|
+
d._screenshot = impl
|
|
70
|
+
d._touch = impl
|
|
71
|
+
return d
|
|
72
|
+
case _:
|
|
73
|
+
assert_never(f'Unsupported Windows recipe: {recipe}')
|