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,197 +1,197 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import subprocess
|
|
3
|
-
from typing import Literal
|
|
4
|
-
from functools import lru_cache
|
|
5
|
-
from typing_extensions import override
|
|
6
|
-
|
|
7
|
-
from kotonebot import logging
|
|
8
|
-
from kotonebot.client import Device
|
|
9
|
-
from kotonebot.util import Countdown, Interval
|
|
10
|
-
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
|
11
|
-
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
|
12
|
-
from ...interop.win.reg import read_reg
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
LeidianRecipes = AdbRecipes
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
|
19
|
-
@copy_type(Instance.__init__)
|
|
20
|
-
def __init__(self, *args, **kwargs):
|
|
21
|
-
super().__init__(*args, **kwargs)
|
|
22
|
-
self._args = args
|
|
23
|
-
self.index: int | None = None
|
|
24
|
-
self.is_running: bool = False
|
|
25
|
-
|
|
26
|
-
@override
|
|
27
|
-
def refresh(self):
|
|
28
|
-
ins = LeidianHost.query(id=self.id)
|
|
29
|
-
assert isinstance(ins, LeidianInstance), f'Expected LeidianInstance, got {type(ins)}'
|
|
30
|
-
if ins is not None:
|
|
31
|
-
self.adb_port = ins.adb_port
|
|
32
|
-
self.adb_ip = ins.adb_ip
|
|
33
|
-
self.adb_name = ins.adb_name
|
|
34
|
-
self.is_running = ins.is_running
|
|
35
|
-
logger.debug('Refreshed Leidian instance: %s', repr(ins))
|
|
36
|
-
|
|
37
|
-
@override
|
|
38
|
-
def start(self):
|
|
39
|
-
if self.running():
|
|
40
|
-
logger.warning('Instance is already running.')
|
|
41
|
-
return
|
|
42
|
-
logger.info('Starting Leidian instance %s', self)
|
|
43
|
-
LeidianHost._invoke_manager(['launch', '--index', str(self.index)])
|
|
44
|
-
self.refresh()
|
|
45
|
-
|
|
46
|
-
@override
|
|
47
|
-
def stop(self):
|
|
48
|
-
if not self.running():
|
|
49
|
-
logger.warning('Instance is not running.')
|
|
50
|
-
return
|
|
51
|
-
logger.info('Stopping Leidian instance id=%s name=%s...', self.id, self.name)
|
|
52
|
-
LeidianHost._invoke_manager(['quit', '--index', str(self.index)])
|
|
53
|
-
self.refresh()
|
|
54
|
-
|
|
55
|
-
@override
|
|
56
|
-
def wait_available(self, timeout: float = 180):
|
|
57
|
-
cd = Countdown(timeout)
|
|
58
|
-
it = Interval(5)
|
|
59
|
-
while not cd.expired() and not self.running():
|
|
60
|
-
it.wait()
|
|
61
|
-
self.refresh()
|
|
62
|
-
if not self.running():
|
|
63
|
-
raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
|
|
64
|
-
|
|
65
|
-
@override
|
|
66
|
-
def running(self) -> bool:
|
|
67
|
-
return self.is_running
|
|
68
|
-
|
|
69
|
-
@override
|
|
70
|
-
def create_device(self, impl: LeidianRecipes, host_config: AdbHostConfig) -> Device:
|
|
71
|
-
"""为雷电模拟器实例创建 Device。"""
|
|
72
|
-
if self.adb_port is None:
|
|
73
|
-
raise ValueError("ADB port is not set and is required.")
|
|
74
|
-
|
|
75
|
-
return super().create_device(impl, host_config, connect=False, disconnect=False)
|
|
76
|
-
|
|
77
|
-
class LeidianHost(HostProtocol[LeidianRecipes]):
|
|
78
|
-
@staticmethod
|
|
79
|
-
@lru_cache(maxsize=1)
|
|
80
|
-
def _read_install_path() -> str | None:
|
|
81
|
-
"""
|
|
82
|
-
从注册表中读取雷电模拟器的安装路径。
|
|
83
|
-
|
|
84
|
-
:return: 安装路径,如果未找到则返回 None。
|
|
85
|
-
"""
|
|
86
|
-
if os.name != 'nt':
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
try:
|
|
90
|
-
icon_path = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'DisplayIcon', default=None)
|
|
91
|
-
if icon_path and isinstance(icon_path, str):
|
|
92
|
-
icon_path = icon_path.replace('"', '')
|
|
93
|
-
path = os.path.dirname(icon_path)
|
|
94
|
-
logger.debug('Leidian installation path (from DisplayIcon): %s', path)
|
|
95
|
-
return path
|
|
96
|
-
install_dir = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'InstallDir', default=None)
|
|
97
|
-
if install_dir and isinstance(install_dir, str):
|
|
98
|
-
install_dir = install_dir.replace('"', '')
|
|
99
|
-
logger.debug('Leidian installation path (from InstallDir): %s', install_dir)
|
|
100
|
-
return install_dir
|
|
101
|
-
except Exception as e:
|
|
102
|
-
logger.error(f'Failed to read Leidian installation path from registry: {e}')
|
|
103
|
-
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
@staticmethod
|
|
107
|
-
def _invoke_manager(args: list[str]) -> str:
|
|
108
|
-
"""
|
|
109
|
-
调用 ldconsole.exe。
|
|
110
|
-
|
|
111
|
-
参考文档:https://www.ldmnq.com/forum/30.html,以及命令行帮助。
|
|
112
|
-
另外还有个 ld.exe,封装了 adb.exe,可以直接执行 adb 命令。(https://www.ldmnq.com/forum/9178.html)
|
|
113
|
-
|
|
114
|
-
:param args: 命令行参数列表。
|
|
115
|
-
:return: 命令执行的输出。
|
|
116
|
-
"""
|
|
117
|
-
install_path = LeidianHost._read_install_path()
|
|
118
|
-
if install_path is None:
|
|
119
|
-
raise RuntimeError('Leidian is not installed.')
|
|
120
|
-
manager_path = os.path.join(install_path, 'ldconsole.exe')
|
|
121
|
-
logger.debug('ldconsole execute: %s', repr(args))
|
|
122
|
-
output = subprocess.run(
|
|
123
|
-
[manager_path] + args,
|
|
124
|
-
capture_output=True,
|
|
125
|
-
text=True,
|
|
126
|
-
# encoding='utf-8', # 居然不是 utf-8 编码
|
|
127
|
-
# https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
|
|
128
|
-
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
129
|
-
)
|
|
130
|
-
if output.returncode != 0:
|
|
131
|
-
raise RuntimeError(f'Failed to invoke ldconsole: {output.stderr}')
|
|
132
|
-
return output.stdout
|
|
133
|
-
|
|
134
|
-
@staticmethod
|
|
135
|
-
def installed() -> bool:
|
|
136
|
-
return LeidianHost._read_install_path() is not None
|
|
137
|
-
|
|
138
|
-
@staticmethod
|
|
139
|
-
def list() -> list[Instance]:
|
|
140
|
-
output = LeidianHost._invoke_manager(['list2'])
|
|
141
|
-
instances = []
|
|
142
|
-
|
|
143
|
-
# 解析 list2 命令的输出
|
|
144
|
-
# 格式: 索引,标题,顶层窗口句柄,绑定窗口句柄,是否进入android,进程PID,VBox进程PID
|
|
145
|
-
for line in output.strip().split('\n'):
|
|
146
|
-
if not line:
|
|
147
|
-
continue
|
|
148
|
-
|
|
149
|
-
parts = line.split(',')
|
|
150
|
-
if len(parts) < 5:
|
|
151
|
-
logger.warning(f'Invalid list2 output line: {line}')
|
|
152
|
-
continue
|
|
153
|
-
|
|
154
|
-
index = parts[0]
|
|
155
|
-
name = parts[1]
|
|
156
|
-
is_android_started = parts[4] == '1'
|
|
157
|
-
# 端口号规则 https://help.ldmnq.com/docs/LD9adbserver#a67730c2e7e2e0400d40bcab37d0e0cf
|
|
158
|
-
adb_port = 5554 + (int(index) * 2)
|
|
159
|
-
|
|
160
|
-
instance = LeidianInstance(
|
|
161
|
-
id=index,
|
|
162
|
-
name=name,
|
|
163
|
-
adb_port=adb_port,
|
|
164
|
-
adb_ip='127.0.0.1',
|
|
165
|
-
adb_name=f'emulator-{adb_port}'
|
|
166
|
-
)
|
|
167
|
-
instance.index = int(index)
|
|
168
|
-
instance.is_running = is_android_started
|
|
169
|
-
logger.debug('Leidian instance: %s', repr(instance))
|
|
170
|
-
instances.append(instance)
|
|
171
|
-
|
|
172
|
-
return instances
|
|
173
|
-
|
|
174
|
-
@staticmethod
|
|
175
|
-
def query(*, id: str) -> Instance | None:
|
|
176
|
-
instances = LeidianHost.list()
|
|
177
|
-
for instance in instances:
|
|
178
|
-
if instance.id == id:
|
|
179
|
-
return instance
|
|
180
|
-
return None
|
|
181
|
-
|
|
182
|
-
@staticmethod
|
|
183
|
-
def recipes() -> 'list[LeidianRecipes]':
|
|
184
|
-
return ['adb', '
|
|
185
|
-
|
|
186
|
-
if __name__ == '__main__':
|
|
187
|
-
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
188
|
-
print(LeidianHost._read_install_path())
|
|
189
|
-
print(LeidianHost.installed())
|
|
190
|
-
print(LeidianHost.list())
|
|
191
|
-
print(ins:=LeidianHost.query(id='0'))
|
|
192
|
-
assert isinstance(ins, LeidianInstance)
|
|
193
|
-
ins.start()
|
|
194
|
-
ins.wait_available()
|
|
195
|
-
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
196
|
-
# ins.stop()
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
7
|
+
from kotonebot import logging
|
|
8
|
+
from kotonebot.client import Device
|
|
9
|
+
from kotonebot.util import Countdown, Interval
|
|
10
|
+
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
|
11
|
+
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
|
12
|
+
from ...interop.win.reg import read_reg
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
LeidianRecipes = AdbRecipes
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
|
19
|
+
@copy_type(Instance.__init__)
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
self._args = args
|
|
23
|
+
self.index: int | None = None
|
|
24
|
+
self.is_running: bool = False
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
def refresh(self):
|
|
28
|
+
ins = LeidianHost.query(id=self.id)
|
|
29
|
+
assert isinstance(ins, LeidianInstance), f'Expected LeidianInstance, got {type(ins)}'
|
|
30
|
+
if ins is not None:
|
|
31
|
+
self.adb_port = ins.adb_port
|
|
32
|
+
self.adb_ip = ins.adb_ip
|
|
33
|
+
self.adb_name = ins.adb_name
|
|
34
|
+
self.is_running = ins.is_running
|
|
35
|
+
logger.debug('Refreshed Leidian instance: %s', repr(ins))
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
def start(self):
|
|
39
|
+
if self.running():
|
|
40
|
+
logger.warning('Instance is already running.')
|
|
41
|
+
return
|
|
42
|
+
logger.info('Starting Leidian instance %s', self)
|
|
43
|
+
LeidianHost._invoke_manager(['launch', '--index', str(self.index)])
|
|
44
|
+
self.refresh()
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def stop(self):
|
|
48
|
+
if not self.running():
|
|
49
|
+
logger.warning('Instance is not running.')
|
|
50
|
+
return
|
|
51
|
+
logger.info('Stopping Leidian instance id=%s name=%s...', self.id, self.name)
|
|
52
|
+
LeidianHost._invoke_manager(['quit', '--index', str(self.index)])
|
|
53
|
+
self.refresh()
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
def wait_available(self, timeout: float = 180):
|
|
57
|
+
cd = Countdown(timeout)
|
|
58
|
+
it = Interval(5)
|
|
59
|
+
while not cd.expired() and not self.running():
|
|
60
|
+
it.wait()
|
|
61
|
+
self.refresh()
|
|
62
|
+
if not self.running():
|
|
63
|
+
raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
def running(self) -> bool:
|
|
67
|
+
return self.is_running
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
def create_device(self, impl: LeidianRecipes, host_config: AdbHostConfig) -> Device:
|
|
71
|
+
"""为雷电模拟器实例创建 Device。"""
|
|
72
|
+
if self.adb_port is None:
|
|
73
|
+
raise ValueError("ADB port is not set and is required.")
|
|
74
|
+
|
|
75
|
+
return super().create_device(impl, host_config, connect=False, disconnect=False)
|
|
76
|
+
|
|
77
|
+
class LeidianHost(HostProtocol[LeidianRecipes]):
|
|
78
|
+
@staticmethod
|
|
79
|
+
@lru_cache(maxsize=1)
|
|
80
|
+
def _read_install_path() -> str | None:
|
|
81
|
+
"""
|
|
82
|
+
从注册表中读取雷电模拟器的安装路径。
|
|
83
|
+
|
|
84
|
+
:return: 安装路径,如果未找到则返回 None。
|
|
85
|
+
"""
|
|
86
|
+
if os.name != 'nt':
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
icon_path = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'DisplayIcon', default=None)
|
|
91
|
+
if icon_path and isinstance(icon_path, str):
|
|
92
|
+
icon_path = icon_path.replace('"', '')
|
|
93
|
+
path = os.path.dirname(icon_path)
|
|
94
|
+
logger.debug('Leidian installation path (from DisplayIcon): %s', path)
|
|
95
|
+
return path
|
|
96
|
+
install_dir = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'InstallDir', default=None)
|
|
97
|
+
if install_dir and isinstance(install_dir, str):
|
|
98
|
+
install_dir = install_dir.replace('"', '')
|
|
99
|
+
logger.debug('Leidian installation path (from InstallDir): %s', install_dir)
|
|
100
|
+
return install_dir
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f'Failed to read Leidian installation path from registry: {e}')
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _invoke_manager(args: list[str]) -> str:
|
|
108
|
+
"""
|
|
109
|
+
调用 ldconsole.exe。
|
|
110
|
+
|
|
111
|
+
参考文档:https://www.ldmnq.com/forum/30.html,以及命令行帮助。
|
|
112
|
+
另外还有个 ld.exe,封装了 adb.exe,可以直接执行 adb 命令。(https://www.ldmnq.com/forum/9178.html)
|
|
113
|
+
|
|
114
|
+
:param args: 命令行参数列表。
|
|
115
|
+
:return: 命令执行的输出。
|
|
116
|
+
"""
|
|
117
|
+
install_path = LeidianHost._read_install_path()
|
|
118
|
+
if install_path is None:
|
|
119
|
+
raise RuntimeError('Leidian is not installed.')
|
|
120
|
+
manager_path = os.path.join(install_path, 'ldconsole.exe')
|
|
121
|
+
logger.debug('ldconsole execute: %s', repr(args))
|
|
122
|
+
output = subprocess.run(
|
|
123
|
+
[manager_path] + args,
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
# encoding='utf-8', # 居然不是 utf-8 编码
|
|
127
|
+
# https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
|
|
128
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
129
|
+
)
|
|
130
|
+
if output.returncode != 0:
|
|
131
|
+
raise RuntimeError(f'Failed to invoke ldconsole: {output.stderr}')
|
|
132
|
+
return output.stdout
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def installed() -> bool:
|
|
136
|
+
return LeidianHost._read_install_path() is not None
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def list() -> list[Instance]:
|
|
140
|
+
output = LeidianHost._invoke_manager(['list2'])
|
|
141
|
+
instances = []
|
|
142
|
+
|
|
143
|
+
# 解析 list2 命令的输出
|
|
144
|
+
# 格式: 索引,标题,顶层窗口句柄,绑定窗口句柄,是否进入android,进程PID,VBox进程PID
|
|
145
|
+
for line in output.strip().split('\n'):
|
|
146
|
+
if not line:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
parts = line.split(',')
|
|
150
|
+
if len(parts) < 5:
|
|
151
|
+
logger.warning(f'Invalid list2 output line: {line}')
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
index = parts[0]
|
|
155
|
+
name = parts[1]
|
|
156
|
+
is_android_started = parts[4] == '1'
|
|
157
|
+
# 端口号规则 https://help.ldmnq.com/docs/LD9adbserver#a67730c2e7e2e0400d40bcab37d0e0cf
|
|
158
|
+
adb_port = 5554 + (int(index) * 2)
|
|
159
|
+
|
|
160
|
+
instance = LeidianInstance(
|
|
161
|
+
id=index,
|
|
162
|
+
name=name,
|
|
163
|
+
adb_port=adb_port,
|
|
164
|
+
adb_ip='127.0.0.1',
|
|
165
|
+
adb_name=f'emulator-{adb_port}'
|
|
166
|
+
)
|
|
167
|
+
instance.index = int(index)
|
|
168
|
+
instance.is_running = is_android_started
|
|
169
|
+
logger.debug('Leidian instance: %s', repr(instance))
|
|
170
|
+
instances.append(instance)
|
|
171
|
+
|
|
172
|
+
return instances
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def query(*, id: str) -> Instance | None:
|
|
176
|
+
instances = LeidianHost.list()
|
|
177
|
+
for instance in instances:
|
|
178
|
+
if instance.id == id:
|
|
179
|
+
return instance
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def recipes() -> 'list[LeidianRecipes]':
|
|
184
|
+
return ['adb', 'uiautomator2']
|
|
185
|
+
|
|
186
|
+
if __name__ == '__main__':
|
|
187
|
+
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
188
|
+
print(LeidianHost._read_install_path())
|
|
189
|
+
print(LeidianHost.installed())
|
|
190
|
+
print(LeidianHost.list())
|
|
191
|
+
print(ins:=LeidianHost.query(id='0'))
|
|
192
|
+
assert isinstance(ins, LeidianInstance)
|
|
193
|
+
ins.start()
|
|
194
|
+
ins.wait_available()
|
|
195
|
+
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
|
196
|
+
# ins.stop()
|
|
197
197
|
# print('status', ins.running(), ins.adb_port, ins.adb_ip)
|