kotonebot 0.5.0__py3-none-any.whl → 0.6.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 +58 -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.py +176 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +96 -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 +11 -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/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.6.0.dist-info}/METADATA +84 -82
- kotonebot-0.6.0.dist-info/RECORD +105 -0
- kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.6.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.6.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING
|
|
2
|
-
|
|
3
|
-
from .protocol import HostProtocol, Instance, AdbHostConfig, WindowsHostConfig, RemoteWindowsHostConfig
|
|
4
|
-
if TYPE_CHECKING:
|
|
5
|
-
from .custom import CustomInstance, create as create_custom
|
|
6
|
-
from .mumu12_host import Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
|
|
7
|
-
from .leidian_host import LeidianHost, LeidianInstance
|
|
8
|
-
|
|
9
|
-
def _require_custom():
|
|
10
|
-
global CustomInstance, create_custom
|
|
11
|
-
from .custom import CustomInstance, create as create_custom
|
|
12
|
-
|
|
13
|
-
def _require_mumu12():
|
|
14
|
-
global Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
|
|
15
|
-
from .mumu12_host import Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
|
|
16
|
-
|
|
17
|
-
def _require_leidian():
|
|
18
|
-
global LeidianHost, LeidianInstance
|
|
19
|
-
from .leidian_host import LeidianHost, LeidianInstance
|
|
20
|
-
|
|
21
|
-
_IMPORT_NAMES = [
|
|
22
|
-
(_require_custom, ['CustomInstance', 'create_custom']),
|
|
23
|
-
(_require_mumu12, ['Mumu12Host', 'Mumu12Instance', 'Mumu12V5Host', 'Mumu12V5Instance']),
|
|
24
|
-
(_require_leidian, ['LeidianHost', 'LeidianInstance']),
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
def __getattr__(name: str):
|
|
28
|
-
for item in _IMPORT_NAMES:
|
|
29
|
-
if name in item[1]:
|
|
30
|
-
item[0]()
|
|
31
|
-
break
|
|
32
|
-
try:
|
|
33
|
-
return globals()[name]
|
|
34
|
-
except KeyError:
|
|
35
|
-
raise AttributeError(name=name)
|
|
36
|
-
|
|
37
|
-
__all__ = [
|
|
38
|
-
'HostProtocol', 'Instance',
|
|
39
|
-
'AdbHostConfig', 'WindowsHostConfig', 'RemoteWindowsHostConfig',
|
|
40
|
-
'CustomInstance', 'create_custom',
|
|
41
|
-
'Mumu12Host', 'Mumu12Instance', 'Mumu12V5Host', 'Mumu12V5Instance',
|
|
42
|
-
'LeidianHost', 'LeidianInstance'
|
|
43
|
-
]
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from .protocol import HostProtocol, Instance, AdbHostConfig, WindowsHostConfig, RemoteWindowsHostConfig
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .custom import CustomInstance, create as create_custom
|
|
6
|
+
from .mumu12_host import Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
|
|
7
|
+
from .leidian_host import LeidianHost, LeidianInstance
|
|
8
|
+
|
|
9
|
+
def _require_custom():
|
|
10
|
+
global CustomInstance, create_custom
|
|
11
|
+
from .custom import CustomInstance, create as create_custom
|
|
12
|
+
|
|
13
|
+
def _require_mumu12():
|
|
14
|
+
global Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
|
|
15
|
+
from .mumu12_host import Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
|
|
16
|
+
|
|
17
|
+
def _require_leidian():
|
|
18
|
+
global LeidianHost, LeidianInstance
|
|
19
|
+
from .leidian_host import LeidianHost, LeidianInstance
|
|
20
|
+
|
|
21
|
+
_IMPORT_NAMES = [
|
|
22
|
+
(_require_custom, ['CustomInstance', 'create_custom']),
|
|
23
|
+
(_require_mumu12, ['Mumu12Host', 'Mumu12Instance', 'Mumu12V5Host', 'Mumu12V5Instance']),
|
|
24
|
+
(_require_leidian, ['LeidianHost', 'LeidianInstance']),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def __getattr__(name: str):
|
|
28
|
+
for item in _IMPORT_NAMES:
|
|
29
|
+
if name in item[1]:
|
|
30
|
+
item[0]()
|
|
31
|
+
break
|
|
32
|
+
try:
|
|
33
|
+
return globals()[name]
|
|
34
|
+
except KeyError:
|
|
35
|
+
raise AttributeError(name=name)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
'HostProtocol', 'Instance',
|
|
39
|
+
'AdbHostConfig', 'WindowsHostConfig', 'RemoteWindowsHostConfig',
|
|
40
|
+
'CustomInstance', 'create_custom',
|
|
41
|
+
'Mumu12Host', 'Mumu12Instance', 'Mumu12V5Host', 'Mumu12V5Instance',
|
|
42
|
+
'LeidianHost', 'LeidianInstance'
|
|
43
|
+
]
|
|
@@ -1,107 +1,101 @@
|
|
|
1
|
-
from abc import ABC
|
|
2
|
-
from typing import Any, Literal, TypeGuard, TypeVar, get_args
|
|
3
|
-
from typing_extensions import assert_never
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
from adbutils import adb
|
|
7
|
-
from adbutils._device import AdbDevice
|
|
8
|
-
except ImportError as _e:
|
|
9
|
-
from kotonebot.errors import MissingDependencyError
|
|
10
|
-
raise MissingDependencyError(_e, 'android')
|
|
11
|
-
from kotonebot import logging
|
|
12
|
-
from kotonebot.client.device import AndroidDevice
|
|
13
|
-
from .protocol import Instance, AdbHostConfig, Device
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
AdbRecipes = Literal['adb', '
|
|
17
|
-
|
|
18
|
-
def is_adb_recipe(recipe: Any) -> TypeGuard[AdbRecipes]:
|
|
19
|
-
return recipe in get_args(AdbRecipes)
|
|
20
|
-
|
|
21
|
-
def connect_adb(
|
|
22
|
-
ip: str,
|
|
23
|
-
port: int,
|
|
24
|
-
connect: bool = True,
|
|
25
|
-
disconnect: bool = True,
|
|
26
|
-
timeout: float = 180,
|
|
27
|
-
device_serial: str | None = None
|
|
28
|
-
) -> AdbDevice:
|
|
29
|
-
"""
|
|
30
|
-
创建 ADB 连接。
|
|
31
|
-
"""
|
|
32
|
-
if disconnect:
|
|
33
|
-
logger.debug('adb disconnect %s:%d', ip, port)
|
|
34
|
-
adb.disconnect(f'{ip}:{port}')
|
|
35
|
-
else:
|
|
36
|
-
logger.debug('Skip adb disconnect.')
|
|
37
|
-
if connect:
|
|
38
|
-
logger.debug('adb connect %s:%d', ip, port)
|
|
39
|
-
result = adb.connect(f'{ip}:{port}')
|
|
40
|
-
if 'cannot connect to' in result:
|
|
41
|
-
raise ValueError(result)
|
|
42
|
-
else:
|
|
43
|
-
logger.debug('Skip adb connect.')
|
|
44
|
-
serial = device_serial or f'{ip}:{port}'
|
|
45
|
-
logger.debug('adb wait for %s', serial)
|
|
46
|
-
adb.wait_for(serial, timeout=timeout)
|
|
47
|
-
devices = adb.device_list()
|
|
48
|
-
logger.debug('adb device_list: %s', devices)
|
|
49
|
-
d = [d for d in devices if d.serial == serial]
|
|
50
|
-
if len(d) == 0:
|
|
51
|
-
raise ValueError(f"Device {serial} not found")
|
|
52
|
-
d = d[0]
|
|
53
|
-
return d
|
|
54
|
-
|
|
55
|
-
class CommonAdbCreateDeviceMixin(ABC):
|
|
56
|
-
"""
|
|
57
|
-
通用 ADB 创建设备的 Mixin。
|
|
58
|
-
该 Mixin 定义了创建 ADB 设备的通用接口。
|
|
59
|
-
"""
|
|
60
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
61
|
-
super().__init__(*args, **kwargs)
|
|
62
|
-
# 下面的属性只是为了让类型检查通过,无实际实现
|
|
63
|
-
self.adb_ip: str
|
|
64
|
-
self.adb_port: int
|
|
65
|
-
self.adb_name: str
|
|
66
|
-
|
|
67
|
-
def create_device(self, recipe: AdbRecipes, config: AdbHostConfig, *, connect: bool = True, disconnect: bool = True) -> Device:
|
|
68
|
-
"""
|
|
69
|
-
创建 ADB 设备。
|
|
70
|
-
|
|
71
|
-
:param recipe: 连接方式配方名称。
|
|
72
|
-
:param config: ADB 配置。
|
|
73
|
-
:param connect: 创建设备实例前,是否连接 ADB(执行 adb connect)。
|
|
74
|
-
:param disconnect: 创建设备实例前,是否先断开已有 ADB 连接(执行 adb disconnect)。
|
|
75
|
-
"""
|
|
76
|
-
connection = connect_adb(
|
|
77
|
-
self.adb_ip,
|
|
78
|
-
self.adb_port,
|
|
79
|
-
connect=connect,
|
|
80
|
-
disconnect=disconnect,
|
|
81
|
-
timeout=config.timeout,
|
|
82
|
-
device_serial=self.adb_name
|
|
83
|
-
)
|
|
84
|
-
d = AndroidDevice(connection)
|
|
85
|
-
match recipe:
|
|
86
|
-
case 'adb':
|
|
87
|
-
from kotonebot.client.implements.adb import AdbImpl
|
|
88
|
-
impl = AdbImpl(connection)
|
|
89
|
-
d._screenshot = impl
|
|
90
|
-
d._touch = impl
|
|
91
|
-
d.commands = impl
|
|
92
|
-
case '
|
|
93
|
-
from kotonebot.client.implements.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
d.
|
|
97
|
-
d.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
d._screenshot = impl
|
|
103
|
-
d._touch = impl
|
|
104
|
-
d.commands = AdbImpl(connection)
|
|
105
|
-
case _:
|
|
106
|
-
assert_never(f'Unsupported ADB recipe: {recipe}')
|
|
107
|
-
return d
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any, Literal, TypeGuard, TypeVar, get_args
|
|
3
|
+
from typing_extensions import assert_never
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from adbutils import adb
|
|
7
|
+
from adbutils._device import AdbDevice
|
|
8
|
+
except ImportError as _e:
|
|
9
|
+
from kotonebot.errors import MissingDependencyError
|
|
10
|
+
raise MissingDependencyError(_e, 'android')
|
|
11
|
+
from kotonebot import logging
|
|
12
|
+
from kotonebot.client.device import AndroidDevice
|
|
13
|
+
from .protocol import Instance, AdbHostConfig, Device
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
AdbRecipes = Literal['adb', 'uiautomator2']
|
|
17
|
+
|
|
18
|
+
def is_adb_recipe(recipe: Any) -> TypeGuard[AdbRecipes]:
|
|
19
|
+
return recipe in get_args(AdbRecipes)
|
|
20
|
+
|
|
21
|
+
def connect_adb(
|
|
22
|
+
ip: str,
|
|
23
|
+
port: int,
|
|
24
|
+
connect: bool = True,
|
|
25
|
+
disconnect: bool = True,
|
|
26
|
+
timeout: float = 180,
|
|
27
|
+
device_serial: str | None = None
|
|
28
|
+
) -> AdbDevice:
|
|
29
|
+
"""
|
|
30
|
+
创建 ADB 连接。
|
|
31
|
+
"""
|
|
32
|
+
if disconnect:
|
|
33
|
+
logger.debug('adb disconnect %s:%d', ip, port)
|
|
34
|
+
adb.disconnect(f'{ip}:{port}')
|
|
35
|
+
else:
|
|
36
|
+
logger.debug('Skip adb disconnect.')
|
|
37
|
+
if connect:
|
|
38
|
+
logger.debug('adb connect %s:%d', ip, port)
|
|
39
|
+
result = adb.connect(f'{ip}:{port}')
|
|
40
|
+
if 'cannot connect to' in result:
|
|
41
|
+
raise ValueError(result)
|
|
42
|
+
else:
|
|
43
|
+
logger.debug('Skip adb connect.')
|
|
44
|
+
serial = device_serial or f'{ip}:{port}'
|
|
45
|
+
logger.debug('adb wait for %s', serial)
|
|
46
|
+
adb.wait_for(serial, timeout=timeout)
|
|
47
|
+
devices = adb.device_list()
|
|
48
|
+
logger.debug('adb device_list: %s', devices)
|
|
49
|
+
d = [d for d in devices if d.serial == serial]
|
|
50
|
+
if len(d) == 0:
|
|
51
|
+
raise ValueError(f"Device {serial} not found")
|
|
52
|
+
d = d[0]
|
|
53
|
+
return d
|
|
54
|
+
|
|
55
|
+
class CommonAdbCreateDeviceMixin(ABC):
|
|
56
|
+
"""
|
|
57
|
+
通用 ADB 创建设备的 Mixin。
|
|
58
|
+
该 Mixin 定义了创建 ADB 设备的通用接口。
|
|
59
|
+
"""
|
|
60
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
61
|
+
super().__init__(*args, **kwargs)
|
|
62
|
+
# 下面的属性只是为了让类型检查通过,无实际实现
|
|
63
|
+
self.adb_ip: str
|
|
64
|
+
self.adb_port: int
|
|
65
|
+
self.adb_name: str
|
|
66
|
+
|
|
67
|
+
def create_device(self, recipe: AdbRecipes, config: AdbHostConfig, *, connect: bool = True, disconnect: bool = True) -> Device:
|
|
68
|
+
"""
|
|
69
|
+
创建 ADB 设备。
|
|
70
|
+
|
|
71
|
+
:param recipe: 连接方式配方名称。
|
|
72
|
+
:param config: ADB 配置。
|
|
73
|
+
:param connect: 创建设备实例前,是否连接 ADB(执行 adb connect)。
|
|
74
|
+
:param disconnect: 创建设备实例前,是否先断开已有 ADB 连接(执行 adb disconnect)。
|
|
75
|
+
"""
|
|
76
|
+
connection = connect_adb(
|
|
77
|
+
self.adb_ip,
|
|
78
|
+
self.adb_port,
|
|
79
|
+
connect=connect,
|
|
80
|
+
disconnect=disconnect,
|
|
81
|
+
timeout=config.timeout,
|
|
82
|
+
device_serial=self.adb_name
|
|
83
|
+
)
|
|
84
|
+
d = AndroidDevice(connection)
|
|
85
|
+
match recipe:
|
|
86
|
+
case 'adb':
|
|
87
|
+
from kotonebot.client.implements.adb import AdbImpl
|
|
88
|
+
impl = AdbImpl(connection)
|
|
89
|
+
d._screenshot = impl
|
|
90
|
+
d._touch = impl
|
|
91
|
+
d.commands = impl
|
|
92
|
+
case 'uiautomator2':
|
|
93
|
+
from kotonebot.client.implements.uiautomator2 import UiAutomator2Impl
|
|
94
|
+
from kotonebot.client.implements.adb import AdbImpl
|
|
95
|
+
impl = UiAutomator2Impl(connection)
|
|
96
|
+
d._screenshot = impl
|
|
97
|
+
d._touch = impl
|
|
98
|
+
d.commands = AdbImpl(connection)
|
|
99
|
+
case _:
|
|
100
|
+
assert_never(f'Unsupported ADB recipe: {recipe}')
|
|
101
|
+
return d
|
kotonebot/client/host/custom.py
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import subprocess
|
|
3
|
-
try:
|
|
4
|
-
from psutil import process_iter
|
|
5
|
-
except ImportError as _e:
|
|
6
|
-
from kotonebot.errors import MissingDependencyError
|
|
7
|
-
raise MissingDependencyError(_e, 'windows')
|
|
8
|
-
from .protocol import Instance, AdbHostConfig, HostProtocol
|
|
9
|
-
from typing import ParamSpec, TypeVar
|
|
10
|
-
from typing_extensions import override
|
|
11
|
-
|
|
12
|
-
from kotonebot import logging
|
|
13
|
-
from kotonebot.client import Device
|
|
14
|
-
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
CustomRecipes = AdbRecipes
|
|
18
|
-
|
|
19
|
-
P = ParamSpec('P')
|
|
20
|
-
T = TypeVar('T')
|
|
21
|
-
|
|
22
|
-
class CustomInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
|
23
|
-
def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
|
|
24
|
-
super().__init__(*args, **kwargs)
|
|
25
|
-
self.exe_path: str | None = exe_path
|
|
26
|
-
self.exe_args: str = emulator_args
|
|
27
|
-
self.process: subprocess.Popen | None = None
|
|
28
|
-
|
|
29
|
-
@override
|
|
30
|
-
def start(self):
|
|
31
|
-
if self.process:
|
|
32
|
-
logger.warning('Process is already running.')
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
if not self.exe_path:
|
|
36
|
-
raise ValueError('Executable path is not set.')
|
|
37
|
-
if self.exe_args:
|
|
38
|
-
logger.info('Starting process "%s" with args "%s"...', self.exe_path, self.exe_args)
|
|
39
|
-
cmd = f'"{self.exe_path}" {self.exe_args}'
|
|
40
|
-
self.process = subprocess.Popen(cmd, shell=True)
|
|
41
|
-
else:
|
|
42
|
-
logger.info('Starting process "%s"...', self.exe_path)
|
|
43
|
-
self.process = subprocess.Popen(self.exe_path)
|
|
44
|
-
|
|
45
|
-
@override
|
|
46
|
-
def stop(self):
|
|
47
|
-
if not self.process:
|
|
48
|
-
logger.warning('Process is not running.')
|
|
49
|
-
return
|
|
50
|
-
logger.info('Stopping process "%s"...', self.process.pid)
|
|
51
|
-
self.process.terminate()
|
|
52
|
-
self.process.wait()
|
|
53
|
-
self.process = None
|
|
54
|
-
|
|
55
|
-
@override
|
|
56
|
-
def running(self) -> bool:
|
|
57
|
-
if self.process is not None:
|
|
58
|
-
return True
|
|
59
|
-
else:
|
|
60
|
-
if not self.exe_path:
|
|
61
|
-
logger.warning('Executable path is not set, cannot check if process is running.')
|
|
62
|
-
return False
|
|
63
|
-
process_name = os.path.basename(self.exe_path)
|
|
64
|
-
p = next((proc for proc in process_iter() if proc.name() == process_name), None)
|
|
65
|
-
if p:
|
|
66
|
-
return True
|
|
67
|
-
else:
|
|
68
|
-
return False
|
|
69
|
-
|
|
70
|
-
@override
|
|
71
|
-
def refresh(self):
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
|
-
@override
|
|
75
|
-
def create_device(self, impl: CustomRecipes, host_config: AdbHostConfig) -> Device:
|
|
76
|
-
"""为自定义实例创建 Device。"""
|
|
77
|
-
if self.adb_port is None:
|
|
78
|
-
raise ValueError("ADB port is not set and is required.")
|
|
79
|
-
|
|
80
|
-
return super().create_device(impl, host_config)
|
|
81
|
-
|
|
82
|
-
def __repr__(self) -> str:
|
|
83
|
-
return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
|
|
84
|
-
|
|
85
|
-
def _type_check(ins: Instance) -> CustomInstance:
|
|
86
|
-
if not isinstance(ins, CustomInstance):
|
|
87
|
-
raise ValueError(f'Instance {ins} is not a CustomInstance')
|
|
88
|
-
return ins
|
|
89
|
-
|
|
90
|
-
def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
|
|
91
|
-
return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
|
|
92
|
-
|
|
93
|
-
class CustomHost(HostProtocol[CustomRecipes]):
|
|
94
|
-
@staticmethod
|
|
95
|
-
def installed() -> bool:
|
|
96
|
-
# Custom instances don't have a specific installation requirement
|
|
97
|
-
return True
|
|
98
|
-
|
|
99
|
-
@staticmethod
|
|
100
|
-
def list() -> list[Instance]:
|
|
101
|
-
# Custom instances are created manually, not discovered
|
|
102
|
-
return []
|
|
103
|
-
|
|
104
|
-
@staticmethod
|
|
105
|
-
def query(*, id: str) -> Instance | None:
|
|
106
|
-
# Custom instances are created manually, not discovered
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
@staticmethod
|
|
110
|
-
def recipes() -> 'list[CustomRecipes]':
|
|
111
|
-
return ['adb', '
|
|
112
|
-
|
|
113
|
-
if __name__ == '__main__':
|
|
114
|
-
ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
|
|
115
|
-
ins.start()
|
|
116
|
-
ins.wait_available()
|
|
117
|
-
input('Press Enter to stop...')
|
|
118
|
-
ins.stop()
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
try:
|
|
4
|
+
from psutil import process_iter
|
|
5
|
+
except ImportError as _e:
|
|
6
|
+
from kotonebot.errors import MissingDependencyError
|
|
7
|
+
raise MissingDependencyError(_e, 'windows')
|
|
8
|
+
from .protocol import Instance, AdbHostConfig, HostProtocol
|
|
9
|
+
from typing import ParamSpec, TypeVar
|
|
10
|
+
from typing_extensions import override
|
|
11
|
+
|
|
12
|
+
from kotonebot import logging
|
|
13
|
+
from kotonebot.client import Device
|
|
14
|
+
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
CustomRecipes = AdbRecipes
|
|
18
|
+
|
|
19
|
+
P = ParamSpec('P')
|
|
20
|
+
T = TypeVar('T')
|
|
21
|
+
|
|
22
|
+
class CustomInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
|
23
|
+
def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self.exe_path: str | None = exe_path
|
|
26
|
+
self.exe_args: str = emulator_args
|
|
27
|
+
self.process: subprocess.Popen | None = None
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
def start(self):
|
|
31
|
+
if self.process:
|
|
32
|
+
logger.warning('Process is already running.')
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if not self.exe_path:
|
|
36
|
+
raise ValueError('Executable path is not set.')
|
|
37
|
+
if self.exe_args:
|
|
38
|
+
logger.info('Starting process "%s" with args "%s"...', self.exe_path, self.exe_args)
|
|
39
|
+
cmd = f'"{self.exe_path}" {self.exe_args}'
|
|
40
|
+
self.process = subprocess.Popen(cmd, shell=True)
|
|
41
|
+
else:
|
|
42
|
+
logger.info('Starting process "%s"...', self.exe_path)
|
|
43
|
+
self.process = subprocess.Popen(self.exe_path)
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
def stop(self):
|
|
47
|
+
if not self.process:
|
|
48
|
+
logger.warning('Process is not running.')
|
|
49
|
+
return
|
|
50
|
+
logger.info('Stopping process "%s"...', self.process.pid)
|
|
51
|
+
self.process.terminate()
|
|
52
|
+
self.process.wait()
|
|
53
|
+
self.process = None
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
def running(self) -> bool:
|
|
57
|
+
if self.process is not None:
|
|
58
|
+
return True
|
|
59
|
+
else:
|
|
60
|
+
if not self.exe_path:
|
|
61
|
+
logger.warning('Executable path is not set, cannot check if process is running.')
|
|
62
|
+
return False
|
|
63
|
+
process_name = os.path.basename(self.exe_path)
|
|
64
|
+
p = next((proc for proc in process_iter() if proc.name() == process_name), None)
|
|
65
|
+
if p:
|
|
66
|
+
return True
|
|
67
|
+
else:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
def refresh(self):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
def create_device(self, impl: CustomRecipes, host_config: AdbHostConfig) -> Device:
|
|
76
|
+
"""为自定义实例创建 Device。"""
|
|
77
|
+
if self.adb_port is None:
|
|
78
|
+
raise ValueError("ADB port is not set and is required.")
|
|
79
|
+
|
|
80
|
+
return super().create_device(impl, host_config)
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
|
|
84
|
+
|
|
85
|
+
def _type_check(ins: Instance) -> CustomInstance:
|
|
86
|
+
if not isinstance(ins, CustomInstance):
|
|
87
|
+
raise ValueError(f'Instance {ins} is not a CustomInstance')
|
|
88
|
+
return ins
|
|
89
|
+
|
|
90
|
+
def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
|
|
91
|
+
return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
|
|
92
|
+
|
|
93
|
+
class CustomHost(HostProtocol[CustomRecipes]):
|
|
94
|
+
@staticmethod
|
|
95
|
+
def installed() -> bool:
|
|
96
|
+
# Custom instances don't have a specific installation requirement
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def list() -> list[Instance]:
|
|
101
|
+
# Custom instances are created manually, not discovered
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def query(*, id: str) -> Instance | None:
|
|
106
|
+
# Custom instances are created manually, not discovered
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def recipes() -> 'list[CustomRecipes]':
|
|
111
|
+
return ['adb', 'uiautomator2']
|
|
112
|
+
|
|
113
|
+
if __name__ == '__main__':
|
|
114
|
+
ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
|
|
115
|
+
ins.start()
|
|
116
|
+
ins.wait_available()
|
|
117
|
+
input('Press Enter to stop...')
|
|
118
|
+
ins.stop()
|