@11agents/cli 0.1.23 → 0.1.25
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.
- package/README.md +52 -0
- package/bin/11agents.js +12 -0
- package/mobile-runtime/README.md +19 -0
- package/mobile-runtime/configs/platforms/xiaohongshu_d01.json +73 -0
- package/mobile-runtime/configs/platforms/xiaohongshu_d02.json +70 -0
- package/mobile-runtime/configs/platforms/xiaohongshu_d03.json +73 -0
- package/mobile-runtime/configs/publish_policy.json +40 -0
- package/mobile-runtime/data-templates/README.md +4 -0
- package/mobile-runtime/data-templates/accounts.example.csv +6 -0
- package/mobile-runtime/data-templates/devices.example.csv +2 -0
- package/mobile-runtime/data-templates/publish_records.example.jsonl +2 -0
- package/mobile-runtime/data-templates/tasks.example.jsonl +5 -0
- package/mobile-runtime/data-templates/video_metrics.example.jsonl +1 -0
- package/mobile-runtime/python/pyproject.toml +34 -0
- package/mobile-runtime/python/src/device_control/__init__.py +5 -0
- package/mobile-runtime/python/src/device_control/adapters/__init__.py +31 -0
- package/mobile-runtime/python/src/device_control/adapters/base.py +43 -0
- package/mobile-runtime/python/src/device_control/adapters/facebook.py +30 -0
- package/mobile-runtime/python/src/device_control/adapters/instagram.py +25 -0
- package/mobile-runtime/python/src/device_control/adapters/reddit.py +29 -0
- package/mobile-runtime/python/src/device_control/adapters/tiktok.py +25 -0
- package/mobile-runtime/python/src/device_control/adapters/x.py +29 -0
- package/mobile-runtime/python/src/device_control/adapters/xiaohongshu.py +26 -0
- package/mobile-runtime/python/src/device_control/adb.py +161 -0
- package/mobile-runtime/python/src/device_control/appium_client.py +131 -0
- package/mobile-runtime/python/src/device_control/appium_manager.py +403 -0
- package/mobile-runtime/python/src/device_control/cli.py +1608 -0
- package/mobile-runtime/python/src/device_control/entrypoints.py +60 -0
- package/mobile-runtime/python/src/device_control/locks.py +162 -0
- package/mobile-runtime/python/src/device_control/metrics/__init__.py +33 -0
- package/mobile-runtime/python/src/device_control/metrics/collectors.py +320 -0
- package/mobile-runtime/python/src/device_control/metrics/tiktok_account_adb.py +367 -0
- package/mobile-runtime/python/src/device_control/metrics/tiktok_video_adb.py +714 -0
- package/mobile-runtime/python/src/device_control/models.py +439 -0
- package/mobile-runtime/python/src/device_control/publish_policy.py +173 -0
- package/mobile-runtime/python/src/device_control/publishers/__init__.py +24 -0
- package/mobile-runtime/python/src/device_control/publishers/facebook_adb.py +494 -0
- package/mobile-runtime/python/src/device_control/publishers/instagram_adb.py +663 -0
- package/mobile-runtime/python/src/device_control/publishers/reddit_adb.py +595 -0
- package/mobile-runtime/python/src/device_control/publishers/tiktok_adb.py +477 -0
- package/mobile-runtime/python/src/device_control/publishers/tiktok_appium.py +259 -0
- package/mobile-runtime/python/src/device_control/publishers/ui_helpers.py +372 -0
- package/mobile-runtime/python/src/device_control/publishers/x_adb.py +636 -0
- package/mobile-runtime/python/src/device_control/publishers/xiaohongshu_adb.py +1143 -0
- package/mobile-runtime/python/src/device_control/store.py +137 -0
- package/mobile-runtime/scripts/appium_smoke.py +71 -0
- package/mobile-runtime/skills/android-collect-tiktok-metrics/SKILL.md +60 -0
- package/mobile-runtime/skills/android-collect-tiktok-metrics/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-group-control-cli/SKILL.md +76 -0
- package/mobile-runtime/skills/android-group-control-cli/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-group-control-cli/references/command-reference.md +122 -0
- package/mobile-runtime/skills/android-publish-facebook/SKILL.md +41 -0
- package/mobile-runtime/skills/android-publish-facebook/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-instagram/SKILL.md +45 -0
- package/mobile-runtime/skills/android-publish-instagram/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-reddit/SKILL.md +41 -0
- package/mobile-runtime/skills/android-publish-reddit/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-tiktok/SKILL.md +43 -0
- package/mobile-runtime/skills/android-publish-tiktok/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-x/SKILL.md +40 -0
- package/mobile-runtime/skills/android-publish-x/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/android-publish-xiaohongshu/SKILL.md +50 -0
- package/mobile-runtime/skills/android-publish-xiaohongshu/agents/openai.yaml +4 -0
- package/mobile-runtime/skills/mobile-publish-data-collection/SKILL.md +49 -0
- package/mobile-runtime/skills/mobile-publish-device-health/SKILL.md +47 -0
- package/mobile-runtime/skills/mobile-publish-execution/SKILL.md +57 -0
- package/mobile-runtime/skills/mobile-publish-records/SKILL.md +29 -0
- package/package.json +4 -1
- package/scripts/mobile-postinstall.js +26 -0
- package/src/commands/mobile.js +695 -0
- package/src/commands/runtime.js +63 -28
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import urllib.request
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .adb import resolve_adb_bin
|
|
16
|
+
from .appium_client import AppiumClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_APPIUM_SERVER = "http://127.0.0.1:4723"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AppiumEnsureError(RuntimeError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class AppiumEnsureResult:
|
|
28
|
+
status: str
|
|
29
|
+
server_url: str
|
|
30
|
+
appium_bin: str
|
|
31
|
+
android_sdk_root: str
|
|
32
|
+
pid: int | None
|
|
33
|
+
log_path: str
|
|
34
|
+
smoke_udids: list[str]
|
|
35
|
+
message: str = ""
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
return {
|
|
39
|
+
"status": self.status,
|
|
40
|
+
"server_url": self.server_url,
|
|
41
|
+
"appium_bin": self.appium_bin,
|
|
42
|
+
"android_sdk_root": self.android_sdk_root,
|
|
43
|
+
"pid": self.pid,
|
|
44
|
+
"log_path": self.log_path,
|
|
45
|
+
"smoke_udids": self.smoke_udids,
|
|
46
|
+
"message": self.message,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_appium_server(
|
|
51
|
+
server_url: str = DEFAULT_APPIUM_SERVER,
|
|
52
|
+
*,
|
|
53
|
+
udids: list[str] | None = None,
|
|
54
|
+
start: bool = True,
|
|
55
|
+
restart: bool = False,
|
|
56
|
+
smoke: bool = True,
|
|
57
|
+
project_root: str | Path | None = None,
|
|
58
|
+
startup_timeout_s: float = 45,
|
|
59
|
+
) -> AppiumEnsureResult:
|
|
60
|
+
root = Path(project_root).expanduser() if project_root else find_project_root()
|
|
61
|
+
normalized_url = normalize_server_url(server_url)
|
|
62
|
+
appium_bin = resolve_appium_bin(root)
|
|
63
|
+
sdk_root = resolve_android_sdk_root()
|
|
64
|
+
log_path = _log_path(root, normalized_url)
|
|
65
|
+
pid: int | None = _read_managed_pid(root, normalized_url)
|
|
66
|
+
|
|
67
|
+
if restart:
|
|
68
|
+
stop_managed_appium(normalized_url, project_root=root, allow_unmanaged_appium=True)
|
|
69
|
+
pid = None
|
|
70
|
+
|
|
71
|
+
status = _get_status(normalized_url)
|
|
72
|
+
started = False
|
|
73
|
+
if status is None:
|
|
74
|
+
if not start:
|
|
75
|
+
raise AppiumEnsureError(f"Appium is not reachable at {normalized_url}")
|
|
76
|
+
_require_local_server(normalized_url)
|
|
77
|
+
pid = _start_appium(normalized_url, appium_bin, sdk_root, log_path, root)
|
|
78
|
+
started = True
|
|
79
|
+
status = _wait_for_status(normalized_url, startup_timeout_s)
|
|
80
|
+
if status is None:
|
|
81
|
+
raise AppiumEnsureError(f"Appium did not become ready within {int(startup_timeout_s)}s; log={log_path}")
|
|
82
|
+
|
|
83
|
+
smoke_udids = [item for item in (udids or []) if item]
|
|
84
|
+
if smoke and smoke_udids:
|
|
85
|
+
try:
|
|
86
|
+
for udid in smoke_udids:
|
|
87
|
+
smoke_appium_session(normalized_url, udid)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
if not started and (_is_managed_process(root, normalized_url) or _local_appium_pid(normalized_url)):
|
|
90
|
+
stop_managed_appium(normalized_url, project_root=root, allow_unmanaged_appium=True)
|
|
91
|
+
pid = _start_appium(normalized_url, appium_bin, sdk_root, log_path, root)
|
|
92
|
+
status = _wait_for_status(normalized_url, startup_timeout_s)
|
|
93
|
+
if status is None:
|
|
94
|
+
raise AppiumEnsureError(f"Appium restart did not become ready; log={log_path}") from exc
|
|
95
|
+
for udid in smoke_udids:
|
|
96
|
+
smoke_appium_session(normalized_url, udid)
|
|
97
|
+
else:
|
|
98
|
+
raise AppiumEnsureError(
|
|
99
|
+
f"Appium smoke session failed for {smoke_udids[0]} at {normalized_url}: {exc}. "
|
|
100
|
+
f"Use `11agents mobile ensure-appium --restart --device Dxx` if a stale local server is occupying the port. "
|
|
101
|
+
f"log={log_path}"
|
|
102
|
+
) from exc
|
|
103
|
+
|
|
104
|
+
return AppiumEnsureResult(
|
|
105
|
+
status="started" if started else "running",
|
|
106
|
+
server_url=normalized_url,
|
|
107
|
+
appium_bin=str(appium_bin),
|
|
108
|
+
android_sdk_root=str(sdk_root),
|
|
109
|
+
pid=pid or _read_managed_pid(root, normalized_url) or _local_appium_pid(normalized_url),
|
|
110
|
+
log_path=str(log_path),
|
|
111
|
+
smoke_udids=smoke_udids if smoke else [],
|
|
112
|
+
message="ok",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def stop_managed_appium(
|
|
117
|
+
server_url: str = DEFAULT_APPIUM_SERVER,
|
|
118
|
+
*,
|
|
119
|
+
project_root: str | Path | None = None,
|
|
120
|
+
allow_unmanaged_appium: bool = False,
|
|
121
|
+
) -> bool:
|
|
122
|
+
root = Path(project_root).expanduser() if project_root else find_project_root()
|
|
123
|
+
normalized_url = normalize_server_url(server_url)
|
|
124
|
+
pid_file = _pid_file(root, normalized_url)
|
|
125
|
+
pid = _read_managed_pid(root, normalized_url)
|
|
126
|
+
if not pid and allow_unmanaged_appium:
|
|
127
|
+
pid = _local_appium_pid(normalized_url)
|
|
128
|
+
if not pid:
|
|
129
|
+
if pid_file.exists():
|
|
130
|
+
pid_file.unlink()
|
|
131
|
+
return False
|
|
132
|
+
try:
|
|
133
|
+
os.kill(pid, signal.SIGTERM)
|
|
134
|
+
except ProcessLookupError:
|
|
135
|
+
pid_file.unlink(missing_ok=True)
|
|
136
|
+
return False
|
|
137
|
+
deadline = time.time() + 8
|
|
138
|
+
while time.time() < deadline:
|
|
139
|
+
if not _pid_running(pid):
|
|
140
|
+
pid_file.unlink(missing_ok=True)
|
|
141
|
+
return True
|
|
142
|
+
time.sleep(0.2)
|
|
143
|
+
try:
|
|
144
|
+
os.kill(pid, signal.SIGKILL)
|
|
145
|
+
except ProcessLookupError:
|
|
146
|
+
pass
|
|
147
|
+
pid_file.unlink(missing_ok=True)
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def smoke_appium_session(server_url: str, udid: str) -> None:
|
|
152
|
+
appium = AppiumClient(server_url)
|
|
153
|
+
try:
|
|
154
|
+
appium.start_session(_smoke_capabilities(udid))
|
|
155
|
+
appium.source()
|
|
156
|
+
finally:
|
|
157
|
+
try:
|
|
158
|
+
appium.delete_session()
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def find_project_root() -> Path:
|
|
164
|
+
candidates = [Path.cwd(), *Path.cwd().parents, Path(__file__).resolve()]
|
|
165
|
+
candidates.extend(Path(__file__).resolve().parents)
|
|
166
|
+
for candidate in candidates:
|
|
167
|
+
root = candidate if candidate.is_dir() else candidate.parent
|
|
168
|
+
if (root / "pyproject.toml").exists() and (root / "src" / "device_control").exists():
|
|
169
|
+
return root
|
|
170
|
+
return Path.cwd()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def resolve_appium_bin(project_root: str | Path | None = None) -> Path:
|
|
174
|
+
root = Path(project_root).expanduser() if project_root else find_project_root()
|
|
175
|
+
env_bin = os.environ.get("APPIUM_BIN", "")
|
|
176
|
+
candidates = [
|
|
177
|
+
Path(env_bin).expanduser() if env_bin else None,
|
|
178
|
+
root / "node_modules" / ".bin" / "appium",
|
|
179
|
+
Path(shutil.which("appium")) if shutil.which("appium") else None,
|
|
180
|
+
]
|
|
181
|
+
for candidate in candidates:
|
|
182
|
+
if candidate and candidate.exists():
|
|
183
|
+
return candidate
|
|
184
|
+
raise AppiumEnsureError(
|
|
185
|
+
"Appium binary not found. Install project node dependencies or set APPIUM_BIN. "
|
|
186
|
+
f"Expected project binary: {root / 'node_modules' / '.bin' / 'appium'}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def resolve_android_sdk_root() -> Path:
|
|
191
|
+
env_candidates = [os.environ.get("ANDROID_SDK_ROOT", ""), os.environ.get("ANDROID_HOME", "")]
|
|
192
|
+
candidates: list[Path] = [Path(item).expanduser() for item in env_candidates if item]
|
|
193
|
+
candidates.extend(
|
|
194
|
+
[
|
|
195
|
+
Path.home() / "Library" / "Android" / "sdk",
|
|
196
|
+
Path("/Users/edy/Library/Android/sdk"),
|
|
197
|
+
Path("/opt/homebrew/share/android-commandlinetools"),
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
adb_path = _resolve_adb_path()
|
|
201
|
+
if adb_path and adb_path.parent.name == "platform-tools":
|
|
202
|
+
candidates.append(adb_path.parent.parent)
|
|
203
|
+
for candidate in sorted(Path("/opt/homebrew/Caskroom/android-platform-tools").glob("*"), reverse=True):
|
|
204
|
+
candidates.append(candidate)
|
|
205
|
+
|
|
206
|
+
for candidate in candidates:
|
|
207
|
+
if _has_platform_tools(candidate):
|
|
208
|
+
return candidate
|
|
209
|
+
raise AppiumEnsureError(
|
|
210
|
+
"Android SDK root not found. Set ANDROID_SDK_ROOT or ANDROID_HOME to a directory containing platform-tools/adb."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def normalize_server_url(server_url: str) -> str:
|
|
215
|
+
parsed = urllib.parse.urlparse(server_url or DEFAULT_APPIUM_SERVER)
|
|
216
|
+
scheme = parsed.scheme or "http"
|
|
217
|
+
host = parsed.hostname or "127.0.0.1"
|
|
218
|
+
port = parsed.port or 4723
|
|
219
|
+
return f"{scheme}://{host}:{port}"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _start_appium(server_url: str, appium_bin: Path, sdk_root: Path, log_path: Path, project_root: Path) -> int:
|
|
223
|
+
parsed = urllib.parse.urlparse(server_url)
|
|
224
|
+
host = parsed.hostname or "127.0.0.1"
|
|
225
|
+
port = str(parsed.port or 4723)
|
|
226
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
env = os.environ.copy()
|
|
228
|
+
env["ANDROID_HOME"] = str(sdk_root)
|
|
229
|
+
env["ANDROID_SDK_ROOT"] = str(sdk_root)
|
|
230
|
+
command = [str(appium_bin), "--address", host, "--port", port, "--log-level", "info"]
|
|
231
|
+
log_handle = log_path.open("ab")
|
|
232
|
+
proc = subprocess.Popen(
|
|
233
|
+
command,
|
|
234
|
+
cwd=str(project_root),
|
|
235
|
+
env=env,
|
|
236
|
+
stdout=log_handle,
|
|
237
|
+
stderr=subprocess.STDOUT,
|
|
238
|
+
stdin=subprocess.DEVNULL,
|
|
239
|
+
start_new_session=True,
|
|
240
|
+
close_fds=True,
|
|
241
|
+
)
|
|
242
|
+
_pid_file(project_root, server_url).write_text(
|
|
243
|
+
json.dumps(
|
|
244
|
+
{
|
|
245
|
+
"pid": proc.pid,
|
|
246
|
+
"server_url": server_url,
|
|
247
|
+
"appium_bin": str(appium_bin),
|
|
248
|
+
"android_sdk_root": str(sdk_root),
|
|
249
|
+
"log_path": str(log_path),
|
|
250
|
+
"started_at_epoch": int(time.time()),
|
|
251
|
+
},
|
|
252
|
+
ensure_ascii=False,
|
|
253
|
+
sort_keys=True,
|
|
254
|
+
),
|
|
255
|
+
encoding="utf-8",
|
|
256
|
+
)
|
|
257
|
+
return proc.pid
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _get_status(server_url: str) -> dict | None:
|
|
261
|
+
try:
|
|
262
|
+
req = urllib.request.Request(f"{server_url}/status", method="GET")
|
|
263
|
+
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
264
|
+
raw = resp.read().decode()
|
|
265
|
+
return json.loads(raw) if raw else {}
|
|
266
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _wait_for_status(server_url: str, timeout_s: float) -> dict | None:
|
|
271
|
+
deadline = time.time() + timeout_s
|
|
272
|
+
while time.time() < deadline:
|
|
273
|
+
status = _get_status(server_url)
|
|
274
|
+
if status is not None:
|
|
275
|
+
return status
|
|
276
|
+
time.sleep(0.5)
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _require_local_server(server_url: str) -> None:
|
|
281
|
+
host = urllib.parse.urlparse(server_url).hostname or ""
|
|
282
|
+
if host not in {"127.0.0.1", "localhost", "::1"}:
|
|
283
|
+
raise AppiumEnsureError(f"Refusing to auto-start Appium for non-local server {server_url}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _resolve_adb_path() -> Path | None:
|
|
287
|
+
adb_bin = resolve_adb_bin()
|
|
288
|
+
resolved = shutil.which(adb_bin) or adb_bin
|
|
289
|
+
path = Path(resolved).expanduser()
|
|
290
|
+
try:
|
|
291
|
+
return path.resolve()
|
|
292
|
+
except FileNotFoundError:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _has_platform_tools(path: Path) -> bool:
|
|
297
|
+
return path.exists() and (path / "platform-tools" / "adb").exists()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _state_dir(project_root: Path) -> Path:
|
|
301
|
+
path = project_root / "artifacts" / "appium"
|
|
302
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
303
|
+
return path
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _port_label(server_url: str) -> str:
|
|
307
|
+
parsed = urllib.parse.urlparse(server_url)
|
|
308
|
+
host = (parsed.hostname or "127.0.0.1").replace(":", "_")
|
|
309
|
+
return f"{host}-{parsed.port or 4723}"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _pid_file(project_root: Path, server_url: str) -> Path:
|
|
313
|
+
return _state_dir(project_root) / f"appium-{_port_label(server_url)}.pid.json"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _log_path(project_root: Path, server_url: str) -> Path:
|
|
317
|
+
return _state_dir(project_root) / f"appium-{_port_label(server_url)}.log"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _read_managed_pid(project_root: Path, server_url: str) -> int | None:
|
|
321
|
+
pid_file = _pid_file(project_root, server_url)
|
|
322
|
+
if not pid_file.exists():
|
|
323
|
+
return None
|
|
324
|
+
try:
|
|
325
|
+
payload = json.loads(pid_file.read_text(encoding="utf-8"))
|
|
326
|
+
pid = int(payload.get("pid") or 0)
|
|
327
|
+
except Exception:
|
|
328
|
+
pid_file.unlink(missing_ok=True)
|
|
329
|
+
return None
|
|
330
|
+
if pid and _pid_running(pid):
|
|
331
|
+
return pid
|
|
332
|
+
pid_file.unlink(missing_ok=True)
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _pid_running(pid: int) -> bool:
|
|
337
|
+
try:
|
|
338
|
+
os.kill(pid, 0)
|
|
339
|
+
return True
|
|
340
|
+
except ProcessLookupError:
|
|
341
|
+
return False
|
|
342
|
+
except PermissionError:
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _is_managed_process(project_root: Path, server_url: str) -> bool:
|
|
347
|
+
return _read_managed_pid(project_root, server_url) is not None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _local_appium_pid(server_url: str) -> int | None:
|
|
351
|
+
parsed = urllib.parse.urlparse(server_url)
|
|
352
|
+
host = parsed.hostname or ""
|
|
353
|
+
if host not in {"127.0.0.1", "localhost", "::1"}:
|
|
354
|
+
return None
|
|
355
|
+
port = str(parsed.port or 4723)
|
|
356
|
+
try:
|
|
357
|
+
proc = subprocess.run(
|
|
358
|
+
["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN", "-t"],
|
|
359
|
+
capture_output=True,
|
|
360
|
+
text=True,
|
|
361
|
+
check=False,
|
|
362
|
+
timeout=5,
|
|
363
|
+
)
|
|
364
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
365
|
+
return None
|
|
366
|
+
for line in proc.stdout.splitlines():
|
|
367
|
+
try:
|
|
368
|
+
pid = int(line.strip())
|
|
369
|
+
except ValueError:
|
|
370
|
+
continue
|
|
371
|
+
if _pid_command_contains(pid, "appium"):
|
|
372
|
+
return pid
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _pid_command_contains(pid: int, needle: str) -> bool:
|
|
377
|
+
try:
|
|
378
|
+
proc = subprocess.run(
|
|
379
|
+
["ps", "-p", str(pid), "-o", "command="],
|
|
380
|
+
capture_output=True,
|
|
381
|
+
text=True,
|
|
382
|
+
check=False,
|
|
383
|
+
timeout=5,
|
|
384
|
+
)
|
|
385
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
386
|
+
return False
|
|
387
|
+
return needle.lower() in proc.stdout.lower()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _smoke_capabilities(udid: str) -> dict:
|
|
391
|
+
return {
|
|
392
|
+
"platformName": "Android",
|
|
393
|
+
"appium:automationName": "UiAutomator2",
|
|
394
|
+
"appium:udid": udid,
|
|
395
|
+
"appium:noReset": True,
|
|
396
|
+
"appium:newCommandTimeout": 120,
|
|
397
|
+
"appium:disableWindowAnimation": True,
|
|
398
|
+
"appium:skipDeviceInitialization": True,
|
|
399
|
+
"appium:ignoreHiddenApiPolicyError": True,
|
|
400
|
+
"appium:disableSuppressAccessibilityService": True,
|
|
401
|
+
"appium:settings[enableNotificationListener]": False,
|
|
402
|
+
"appium:autoLaunch": False,
|
|
403
|
+
}
|