kotonebot 0.4.0__py3-none-any.whl → 0.5.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/task_action.py +183 -183
- kotonebot/backend/core.py +129 -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/ocr.py +535 -529
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +528 -503
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -12
- kotonebot/client/host/adb_common.py +107 -103
- kotonebot/client/host/custom.py +118 -114
- kotonebot/client/host/leidian_host.py +196 -201
- kotonebot/client/host/mumu12_host.py +353 -358
- kotonebot/client/host/protocol.py +214 -213
- kotonebot/client/host/windows_common.py +58 -58
- kotonebot/client/implements/__init__.py +71 -15
- kotonebot/client/implements/adb.py +89 -85
- kotonebot/client/implements/adb_raw.py +162 -158
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
- 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 -81
- kotonebot/client/implements/windows.py +176 -172
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/config/base_config.py +96 -96
- kotonebot/config/manager.py +36 -36
- kotonebot/errors.py +76 -71
- kotonebot/interop/win/__init__.py +10 -3
- kotonebot/interop/win/_mouse.py +311 -0
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +17 -17
- kotonebot/primitives/geometry.py +862 -290
- kotonebot/primitives/visual.py +63 -63
- kotonebot/tools/mirror.py +354 -354
- 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 -87
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -53
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -81
- kotonebot-0.5.0.dist-info/RECORD +71 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot-0.4.0.dist-info/RECORD +0 -70
- {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,358 +1,353 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
import os
|
|
3
|
-
import json
|
|
4
|
-
import subprocess
|
|
5
|
-
from functools import lru_cache
|
|
6
|
-
from typing import Any, Literal, overload, TYPE_CHECKING
|
|
7
|
-
from typing_extensions import override
|
|
8
|
-
|
|
9
|
-
from kotonebot import logging
|
|
10
|
-
from kotonebot.client import Device
|
|
11
|
-
from kotonebot.client.device import AndroidDevice
|
|
12
|
-
from kotonebot.client.implements.adb import AdbImpl
|
|
13
|
-
from kotonebot.client.implements.nemu_ipc import NemuIpcImpl, NemuIpcImplConfig
|
|
14
|
-
from kotonebot.util import Countdown, Interval
|
|
15
|
-
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
|
16
|
-
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin, connect_adb, is_adb_recipe
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
self.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
logger.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@
|
|
208
|
-
def create_device(self, recipe:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
device
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"""
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
print(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
ins.start()
|
|
355
|
-
ins.wait_available()
|
|
356
|
-
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
357
|
-
ins.stop()
|
|
358
|
-
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Any, Literal, overload, TYPE_CHECKING
|
|
7
|
+
from typing_extensions import override
|
|
8
|
+
|
|
9
|
+
from kotonebot import logging
|
|
10
|
+
from kotonebot.client import Device
|
|
11
|
+
from kotonebot.client.device import AndroidDevice
|
|
12
|
+
from kotonebot.client.implements.adb import AdbImpl
|
|
13
|
+
from kotonebot.client.implements.nemu_ipc import NemuIpcImpl, NemuIpcImplConfig
|
|
14
|
+
from kotonebot.util import Countdown, Interval
|
|
15
|
+
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
|
16
|
+
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin, connect_adb, is_adb_recipe
|
|
17
|
+
from ...interop.win.reg import read_reg
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Forward declarations for type hints
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from typing import Type
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
MuMu12Recipes = AdbRecipes | Literal['nemu_ipc']
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MuMu12HostConfig(AdbHostConfig):
|
|
29
|
+
"""nemu_ipc 能力的配置模型。"""
|
|
30
|
+
display_id: int | None = 0
|
|
31
|
+
"""目标显示器 ID,默认为 0(主显示器)。若为 None 且设置了 target_package_name,则自动获取对应的 display_id。"""
|
|
32
|
+
target_package_name: str | None = None
|
|
33
|
+
"""目标应用包名,用于自动获取 display_id。"""
|
|
34
|
+
app_index: int = 0
|
|
35
|
+
"""多开应用索引,传给 get_display_id 方法。"""
|
|
36
|
+
|
|
37
|
+
class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
|
38
|
+
InstanceClass: 'Type[Mumu12Instance]'
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
@lru_cache(maxsize=1)
|
|
42
|
+
def _read_install_path() -> str | None:
|
|
43
|
+
r"""
|
|
44
|
+
从注册表中读取 MuMu Player 12 的安装路径。
|
|
45
|
+
|
|
46
|
+
返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
|
|
47
|
+
|
|
48
|
+
:return: 若找到,则返回安装路径;否则返回 None。
|
|
49
|
+
"""
|
|
50
|
+
if os.name != 'nt':
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
uninstall_subkeys = [
|
|
54
|
+
r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer-12.0',
|
|
55
|
+
# TODO: 支持国际版 MuMu
|
|
56
|
+
# r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayerGlobal-12.0'
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
for subkey in uninstall_subkeys:
|
|
60
|
+
icon_path = read_reg('HKLM', subkey, 'DisplayIcon', default=None)
|
|
61
|
+
if icon_path and isinstance(icon_path, str):
|
|
62
|
+
icon_path = icon_path.replace('"', '')
|
|
63
|
+
path = os.path.dirname(icon_path)
|
|
64
|
+
logger.debug('MuMu Player 12 installation path: %s', path)
|
|
65
|
+
# 返回根目录(去掉 shell 子目录)
|
|
66
|
+
if os.path.basename(path).lower() == 'shell':
|
|
67
|
+
path = os.path.dirname(path)
|
|
68
|
+
return path
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def _invoke_manager(cls,args: list[str]) -> str:
|
|
73
|
+
"""
|
|
74
|
+
调用 MuMuManager.exe。
|
|
75
|
+
|
|
76
|
+
:param args: 命令行参数列表。
|
|
77
|
+
:return: 命令执行的输出。
|
|
78
|
+
"""
|
|
79
|
+
install_path = cls._read_install_path()
|
|
80
|
+
if install_path is None:
|
|
81
|
+
raise RuntimeError('MuMu Player 12 is not installed.')
|
|
82
|
+
manager_path = os.path.join(install_path, 'shell', 'MuMuManager.exe')
|
|
83
|
+
logger.debug('MuMuManager execute: %s', repr(args))
|
|
84
|
+
output = subprocess.run(
|
|
85
|
+
[manager_path] + args,
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
encoding='utf-8',
|
|
89
|
+
# https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
|
|
90
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
91
|
+
)
|
|
92
|
+
if output.returncode != 0:
|
|
93
|
+
# raise RuntimeError(f'Failed to invoke MuMuManager: {output.stderr}')
|
|
94
|
+
logger.warning('Failed to invoke MuMuManager: %s', output.stderr)
|
|
95
|
+
return output.stdout
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def installed() -> bool:
|
|
99
|
+
return Mumu12Host._read_install_path() is not None
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def list(cls) -> list[Instance]:
|
|
103
|
+
nemu_path = cls._read_install_path()
|
|
104
|
+
if nemu_path is None:
|
|
105
|
+
raise RuntimeError("Nemu path not found.")
|
|
106
|
+
output = cls._invoke_manager(['info', '-v', 'all'])
|
|
107
|
+
logger.debug('MuMuManager.exe output: %s', output)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
data: dict[str, dict[str, Any]] = json.loads(output)
|
|
111
|
+
if 'name' in data.keys():
|
|
112
|
+
# 这里有个坑:
|
|
113
|
+
# 如果只有一个实例,返回的 JSON 结构是单个对象而不是数组
|
|
114
|
+
data = { '0': data }
|
|
115
|
+
instances = []
|
|
116
|
+
for index, instance_data in data.items():
|
|
117
|
+
instance = cls.InstanceClass(
|
|
118
|
+
id=index,
|
|
119
|
+
name=instance_data['name'],
|
|
120
|
+
adb_port=instance_data.get('adb_port'),
|
|
121
|
+
adb_ip=instance_data.get('adb_host_ip', '127.0.0.1'),
|
|
122
|
+
adb_name=None,
|
|
123
|
+
)
|
|
124
|
+
instance.nemu_path = nemu_path
|
|
125
|
+
instance.index = int(index)
|
|
126
|
+
instance.is_android_started = instance_data.get('is_android_started', False)
|
|
127
|
+
logger.debug('Mumu12 instance: %s', repr(instance))
|
|
128
|
+
instances.append(instance)
|
|
129
|
+
return instances
|
|
130
|
+
except json.JSONDecodeError as e:
|
|
131
|
+
raise RuntimeError(f'Failed to parse output: {e}')
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def query(cls, *, id: str) -> Instance | None:
|
|
135
|
+
instances = cls.list()
|
|
136
|
+
for instance in instances:
|
|
137
|
+
if instance.id == id:
|
|
138
|
+
return instance
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def recipes() -> 'list[MuMu12Recipes]':
|
|
143
|
+
return ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc']
|
|
144
|
+
|
|
145
|
+
class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
|
|
146
|
+
HostClass: 'type[Mumu12Host]' = Mumu12Host
|
|
147
|
+
|
|
148
|
+
@copy_type(Instance.__init__)
|
|
149
|
+
def __init__(self, *args, **kwargs):
|
|
150
|
+
if not hasattr(self.HostClass, 'InstanceClass'):
|
|
151
|
+
raise RuntimeError(f"{self.HostClass.__name__}.InstanceClass not initialized")
|
|
152
|
+
|
|
153
|
+
super().__init__(*args, **kwargs)
|
|
154
|
+
self._args = args
|
|
155
|
+
self.index: int | None = None
|
|
156
|
+
self.is_android_started: bool = False
|
|
157
|
+
self.nemu_path: str | None = None
|
|
158
|
+
|
|
159
|
+
@override
|
|
160
|
+
def refresh(self):
|
|
161
|
+
ins = self.HostClass.query(id=self.id)
|
|
162
|
+
if ins is not None and isinstance(ins, self.__class__):
|
|
163
|
+
self.adb_port = ins.adb_port
|
|
164
|
+
self.adb_ip = ins.adb_ip
|
|
165
|
+
self.adb_name = ins.adb_name
|
|
166
|
+
self.is_android_started = ins.is_android_started
|
|
167
|
+
self.nemu_path = ins.nemu_path
|
|
168
|
+
logger.debug('Refreshed MuMu12 instance: %s', repr(ins))
|
|
169
|
+
|
|
170
|
+
@override
|
|
171
|
+
def start(self):
|
|
172
|
+
if self.running():
|
|
173
|
+
logger.warning('Instance is already running.')
|
|
174
|
+
return
|
|
175
|
+
logger.info('Starting MuMu12 instance %s', self)
|
|
176
|
+
self.HostClass._invoke_manager(['control', '-v', self.id, 'launch'])
|
|
177
|
+
self.refresh()
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
def stop(self):
|
|
181
|
+
if not self.running():
|
|
182
|
+
logger.warning('Instance is not running.')
|
|
183
|
+
return
|
|
184
|
+
logger.info('Stopping MuMu12 instance id=%s name=%s...', self.id, self.name)
|
|
185
|
+
self.HostClass._invoke_manager(['control', '-v', self.id, 'shutdown'])
|
|
186
|
+
self.refresh()
|
|
187
|
+
|
|
188
|
+
@override
|
|
189
|
+
def wait_available(self, timeout: float = 180):
|
|
190
|
+
cd = Countdown(timeout)
|
|
191
|
+
it = Interval(5)
|
|
192
|
+
while not cd.expired() and not self.running():
|
|
193
|
+
it.wait()
|
|
194
|
+
self.refresh()
|
|
195
|
+
if not self.running():
|
|
196
|
+
raise TimeoutError(f'MuMu12 instance "{self.name}" is not available.')
|
|
197
|
+
|
|
198
|
+
@override
|
|
199
|
+
def running(self) -> bool:
|
|
200
|
+
return self.is_android_started
|
|
201
|
+
|
|
202
|
+
@overload
|
|
203
|
+
def create_device(self, recipe: Literal['nemu_ipc'], host_config: MuMu12HostConfig) -> Device: ...
|
|
204
|
+
@overload
|
|
205
|
+
def create_device(self, recipe: AdbRecipes, host_config: AdbHostConfig) -> Device: ...
|
|
206
|
+
|
|
207
|
+
@override
|
|
208
|
+
def create_device(self, recipe: MuMu12Recipes, host_config: MuMu12HostConfig | AdbHostConfig) -> Device:
|
|
209
|
+
"""为MuMu12模拟器实例创建 Device。"""
|
|
210
|
+
if self.adb_port is None:
|
|
211
|
+
raise ValueError("ADB port is not set and is required.")
|
|
212
|
+
|
|
213
|
+
if recipe == 'nemu_ipc' and isinstance(host_config, MuMu12HostConfig):
|
|
214
|
+
# NemuImpl
|
|
215
|
+
if self.nemu_path is None:
|
|
216
|
+
raise RuntimeError("Nemu path is not set.")
|
|
217
|
+
nemu_config = NemuIpcImplConfig(
|
|
218
|
+
nemu_folder=self.nemu_path,
|
|
219
|
+
instance_id=int(self.id),
|
|
220
|
+
display_id=host_config.display_id,
|
|
221
|
+
target_package_name=host_config.target_package_name,
|
|
222
|
+
app_index=host_config.app_index
|
|
223
|
+
)
|
|
224
|
+
nemu_impl = NemuIpcImpl(nemu_config)
|
|
225
|
+
# AdbImpl
|
|
226
|
+
adb_impl = AdbImpl(connect_adb(
|
|
227
|
+
self.adb_ip,
|
|
228
|
+
self.adb_port,
|
|
229
|
+
timeout=host_config.timeout,
|
|
230
|
+
device_serial=self.adb_name
|
|
231
|
+
))
|
|
232
|
+
device = AndroidDevice()
|
|
233
|
+
device._screenshot = nemu_impl
|
|
234
|
+
device._touch = nemu_impl
|
|
235
|
+
device.commands = adb_impl
|
|
236
|
+
|
|
237
|
+
return device
|
|
238
|
+
elif isinstance(host_config, AdbHostConfig) and is_adb_recipe(recipe):
|
|
239
|
+
return super().create_device(recipe, host_config)
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError(f'Unknown recipe: {recipe}')
|
|
242
|
+
|
|
243
|
+
class Mumu12V5Host(Mumu12Host):
|
|
244
|
+
InstanceClass: 'Type[Mumu12V5Instance]'
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
@lru_cache(maxsize=1)
|
|
248
|
+
def _read_install_path(cls) -> str | None:
|
|
249
|
+
r"""
|
|
250
|
+
从注册表中读取 MuMu Player 12 v5.x 的安装路径。
|
|
251
|
+
|
|
252
|
+
返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
|
|
253
|
+
|
|
254
|
+
:return: 若找到,则返回安装路径;否则返回 None。
|
|
255
|
+
"""
|
|
256
|
+
if os.name != 'nt':
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
uninstall_subkeys = [
|
|
260
|
+
r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer',
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
for subkey in uninstall_subkeys:
|
|
264
|
+
icon_path = read_reg('HKLM', subkey, 'DisplayIcon', default=None)
|
|
265
|
+
if icon_path and isinstance(icon_path, str):
|
|
266
|
+
icon_path = icon_path.replace('"', '')
|
|
267
|
+
path = os.path.dirname(icon_path)
|
|
268
|
+
logger.debug('MuMu Player 12 installation path: %s', path)
|
|
269
|
+
# 返回根目录(去掉 shell 子目录)
|
|
270
|
+
if os.path.basename(path).lower() == 'nx_main':
|
|
271
|
+
path = os.path.dirname(path)
|
|
272
|
+
return path
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def _invoke_manager(cls, args: list[str]) -> str:
|
|
277
|
+
"""
|
|
278
|
+
调用 MuMuManager.exe。
|
|
279
|
+
|
|
280
|
+
:param args: 命令行参数列表。
|
|
281
|
+
:return: 命令执行的输出。
|
|
282
|
+
"""
|
|
283
|
+
install_path = cls._read_install_path()
|
|
284
|
+
if install_path is None:
|
|
285
|
+
raise RuntimeError('MuMu Player 12 v5.x is not installed.')
|
|
286
|
+
manager_path = os.path.join(install_path, 'nx_main', 'MuMuManager.exe')
|
|
287
|
+
logger.debug('MuMuManager execute: %s', repr(args))
|
|
288
|
+
output = subprocess.run(
|
|
289
|
+
[manager_path] + args,
|
|
290
|
+
capture_output=True,
|
|
291
|
+
text=True,
|
|
292
|
+
encoding='utf-8',
|
|
293
|
+
# https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
|
|
294
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
295
|
+
)
|
|
296
|
+
if output.returncode != 0:
|
|
297
|
+
# raise RuntimeError(f'Failed to invoke MuMuManager: {output.stderr}')
|
|
298
|
+
logger.warning('Failed to invoke MuMuManager: %s', output.stderr)
|
|
299
|
+
return output.stdout
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def installed(cls) -> bool:
|
|
303
|
+
return cls._read_install_path() is not None
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def list(cls) -> list[Instance]:
|
|
307
|
+
output = cls._invoke_manager(['info', '-v', 'all'])
|
|
308
|
+
logger.debug('MuMuManager.exe output: %s', output)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
data: dict[str, dict[str, Any]] = json.loads(output)
|
|
312
|
+
if 'name' in data.keys():
|
|
313
|
+
# 这里有个坑:
|
|
314
|
+
# 如果只有一个实例,返回的 JSON 结构是单个对象而不是数组
|
|
315
|
+
data = { '0': data }
|
|
316
|
+
instances = []
|
|
317
|
+
for index, instance_data in data.items():
|
|
318
|
+
instance = cls.InstanceClass(
|
|
319
|
+
id=index,
|
|
320
|
+
name=instance_data['name'],
|
|
321
|
+
adb_port=instance_data.get('adb_port'),
|
|
322
|
+
adb_ip=instance_data.get('adb_host_ip', '127.0.0.1'),
|
|
323
|
+
adb_name=None
|
|
324
|
+
)
|
|
325
|
+
instance.nemu_path = cls._read_install_path()
|
|
326
|
+
instance.index = int(index)
|
|
327
|
+
instance.is_android_started = instance_data.get('is_android_started', False)
|
|
328
|
+
logger.debug('Mumu12 v5.x instance: %s', repr(instance))
|
|
329
|
+
instances.append(instance)
|
|
330
|
+
return instances
|
|
331
|
+
except json.JSONDecodeError as e:
|
|
332
|
+
raise RuntimeError(f'Failed to parse output: {e}')
|
|
333
|
+
|
|
334
|
+
class Mumu12V5Instance(Mumu12Instance):
|
|
335
|
+
HostClass: 'type[Mumu12V5Host]' = Mumu12V5Host
|
|
336
|
+
|
|
337
|
+
# 延迟初始化 InstanceClass 变量
|
|
338
|
+
Mumu12Host.InstanceClass = Mumu12Instance
|
|
339
|
+
Mumu12V5Host.InstanceClass = Mumu12V5Instance
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == '__main__':
|
|
343
|
+
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
344
|
+
print(Mumu12Host._read_install_path())
|
|
345
|
+
print(Mumu12Host.installed())
|
|
346
|
+
print(Mumu12Host.list())
|
|
347
|
+
print(ins:=Mumu12Host.query(id='2'))
|
|
348
|
+
assert isinstance(ins, Mumu12Host.InstanceClass)
|
|
349
|
+
ins.start()
|
|
350
|
+
ins.wait_available()
|
|
351
|
+
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
352
|
+
ins.stop()
|
|
353
|
+
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|