android-emu-agent 0.1.3__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.
Files changed (50) hide show
  1. android_emu_agent/__init__.py +3 -0
  2. android_emu_agent/actions/__init__.py +1 -0
  3. android_emu_agent/actions/executor.py +288 -0
  4. android_emu_agent/actions/selector.py +122 -0
  5. android_emu_agent/actions/wait.py +193 -0
  6. android_emu_agent/artifacts/__init__.py +1 -0
  7. android_emu_agent/artifacts/manager.py +125 -0
  8. android_emu_agent/artifacts/py.typed +0 -0
  9. android_emu_agent/cli/__init__.py +1 -0
  10. android_emu_agent/cli/commands/__init__.py +1 -0
  11. android_emu_agent/cli/commands/action.py +158 -0
  12. android_emu_agent/cli/commands/app_cmd.py +95 -0
  13. android_emu_agent/cli/commands/artifact.py +81 -0
  14. android_emu_agent/cli/commands/daemon.py +62 -0
  15. android_emu_agent/cli/commands/device.py +122 -0
  16. android_emu_agent/cli/commands/emulator.py +46 -0
  17. android_emu_agent/cli/commands/file.py +139 -0
  18. android_emu_agent/cli/commands/reliability.py +310 -0
  19. android_emu_agent/cli/commands/session.py +65 -0
  20. android_emu_agent/cli/commands/ui.py +112 -0
  21. android_emu_agent/cli/commands/wait.py +132 -0
  22. android_emu_agent/cli/daemon_client.py +185 -0
  23. android_emu_agent/cli/main.py +52 -0
  24. android_emu_agent/cli/utils.py +171 -0
  25. android_emu_agent/daemon/__init__.py +1 -0
  26. android_emu_agent/daemon/core.py +62 -0
  27. android_emu_agent/daemon/health.py +177 -0
  28. android_emu_agent/daemon/models.py +244 -0
  29. android_emu_agent/daemon/server.py +1644 -0
  30. android_emu_agent/db/__init__.py +1 -0
  31. android_emu_agent/db/models.py +229 -0
  32. android_emu_agent/device/__init__.py +1 -0
  33. android_emu_agent/device/manager.py +522 -0
  34. android_emu_agent/device/session.py +129 -0
  35. android_emu_agent/errors.py +232 -0
  36. android_emu_agent/files/__init__.py +1 -0
  37. android_emu_agent/files/manager.py +311 -0
  38. android_emu_agent/py.typed +0 -0
  39. android_emu_agent/reliability/__init__.py +1 -0
  40. android_emu_agent/reliability/manager.py +244 -0
  41. android_emu_agent/ui/__init__.py +1 -0
  42. android_emu_agent/ui/context.py +169 -0
  43. android_emu_agent/ui/ref_resolver.py +149 -0
  44. android_emu_agent/ui/snapshotter.py +236 -0
  45. android_emu_agent/validation.py +59 -0
  46. android_emu_agent-0.1.3.dist-info/METADATA +375 -0
  47. android_emu_agent-0.1.3.dist-info/RECORD +50 -0
  48. android_emu_agent-0.1.3.dist-info/WHEEL +4 -0
  49. android_emu_agent-0.1.3.dist-info/entry_points.txt +2 -0
  50. android_emu_agent-0.1.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,177 @@
1
+ """Health monitoring - device connectivity checks and stale connection eviction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ import structlog
12
+
13
+ if TYPE_CHECKING:
14
+ from android_emu_agent.device.manager import DeviceManager
15
+ from android_emu_agent.device.session import SessionManager
16
+
17
+ logger = structlog.get_logger()
18
+
19
+
20
+ @dataclass
21
+ class DeviceHealth:
22
+ """Health status for a single device."""
23
+
24
+ serial: str
25
+ adb_ok: bool
26
+ u2_ok: bool
27
+ last_check: datetime
28
+ error: str | None = None
29
+
30
+
31
+ class HealthMonitor:
32
+ """Monitors device health and evicts stale connections."""
33
+
34
+ def __init__(
35
+ self,
36
+ device_manager: DeviceManager,
37
+ session_manager: SessionManager,
38
+ ) -> None:
39
+ self._device_manager = device_manager
40
+ self._session_manager = session_manager
41
+ self._device_health: dict[str, DeviceHealth] = {}
42
+ self._task: asyncio.Task[None] | None = None
43
+ self._running = False
44
+
45
+ async def check_device(self, serial: str, timeout: float = 3.0) -> DeviceHealth:
46
+ """Check health of a single device.
47
+
48
+ Args:
49
+ serial: Device serial to check
50
+ timeout: Timeout in seconds for each check
51
+
52
+ Returns:
53
+ DeviceHealth with check results
54
+ """
55
+ now = datetime.now()
56
+
57
+ # Step 1: ADB check
58
+ adb_ok = await self._check_adb(serial, timeout)
59
+ if not adb_ok:
60
+ health = DeviceHealth(
61
+ serial=serial,
62
+ adb_ok=False,
63
+ u2_ok=False,
64
+ last_check=now,
65
+ error="ADB connection failed",
66
+ )
67
+ self._device_health[serial] = health
68
+ return health
69
+
70
+ # Step 2: u2 check (only if ADB passed)
71
+ u2_ok, u2_error = await self._check_u2(serial, timeout)
72
+ health = DeviceHealth(
73
+ serial=serial,
74
+ adb_ok=True,
75
+ u2_ok=u2_ok,
76
+ last_check=now,
77
+ error=u2_error,
78
+ )
79
+ self._device_health[serial] = health
80
+ return health
81
+
82
+ async def _check_adb(self, serial: str, timeout: float) -> bool:
83
+ """Check ADB connectivity."""
84
+ device = self._device_manager._adb_devices.get(serial)
85
+ if not device:
86
+ return False
87
+ try:
88
+ result = await asyncio.wait_for(
89
+ asyncio.to_thread(device.shell, "echo ok"),
90
+ timeout=timeout,
91
+ )
92
+ return "ok" in str(result)
93
+ except Exception:
94
+ return False
95
+
96
+ async def _check_u2(self, serial: str, timeout: float) -> tuple[bool, str | None]:
97
+ """Check u2/ATX server connectivity."""
98
+ device = self._device_manager._u2_devices.get(serial)
99
+ if not device:
100
+ # No cached u2 connection = nothing to verify, will connect fresh
101
+ return True, None
102
+ try:
103
+ await asyncio.wait_for(
104
+ asyncio.to_thread(lambda: device.info),
105
+ timeout=timeout,
106
+ )
107
+ return True, None
108
+ except Exception as e:
109
+ return False, f"ATX server unresponsive: {e}"
110
+
111
+ async def start(self) -> None:
112
+ """Start the health monitor heartbeat loop."""
113
+ logger.info("health_monitor_starting")
114
+ self._running = True
115
+ self._task = asyncio.create_task(self._heartbeat_loop())
116
+ logger.info("health_monitor_started")
117
+
118
+ async def stop(self) -> None:
119
+ """Stop the health monitor."""
120
+ logger.info("health_monitor_stopping")
121
+ self._running = False
122
+ if self._task:
123
+ self._task.cancel()
124
+ with contextlib.suppress(asyncio.CancelledError):
125
+ await self._task
126
+ self._task = None
127
+ logger.info("health_monitor_stopped")
128
+
129
+ def get_status(self) -> dict[str, Any]:
130
+ """Get current health status for all monitored devices.
131
+
132
+ Returns:
133
+ Dict with device health information
134
+ """
135
+ devices: dict[str, dict[str, Any]] = {}
136
+ for serial, health in self._device_health.items():
137
+ devices[serial] = {
138
+ "adb_ok": health.adb_ok,
139
+ "u2_ok": health.u2_ok,
140
+ "last_check": health.last_check.isoformat(),
141
+ "error": health.error,
142
+ }
143
+ return {"devices": devices}
144
+
145
+ async def _heartbeat_loop(self) -> None:
146
+ """Periodic heartbeat to check device health."""
147
+ while self._running:
148
+ await asyncio.sleep(15)
149
+ try:
150
+ await self._run_health_checks()
151
+ except Exception:
152
+ logger.exception("health_check_error")
153
+
154
+ async def _run_health_checks(self) -> None:
155
+ """Run health checks on all relevant devices."""
156
+ # Get devices with active sessions (prioritize)
157
+ sessions = await self._session_manager.list_sessions()
158
+ session_devices = {s.device_serial for s in sessions}
159
+
160
+ # Check session devices first, then other known devices
161
+ all_devices = list(session_devices) + [
162
+ s for s in self._device_health if s not in session_devices
163
+ ]
164
+
165
+ for serial in all_devices:
166
+ health = await self.check_device(serial, timeout=3.0)
167
+
168
+ if not health.adb_ok or not health.u2_ok:
169
+ logger.warning(
170
+ "device_unhealthy",
171
+ serial=serial,
172
+ adb_ok=health.adb_ok,
173
+ u2_ok=health.u2_ok,
174
+ error=health.error,
175
+ )
176
+ # Evict stale connections
177
+ await self._device_manager.evict_device(serial)
@@ -0,0 +1,244 @@
1
+ """Pydantic request models for daemon endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, model_validator
8
+
9
+
10
+ class SessionStartRequest(BaseModel):
11
+ device_serial: str
12
+
13
+
14
+ class SessionStopRequest(BaseModel):
15
+ session_id: str
16
+
17
+
18
+ class SnapshotRequest(BaseModel):
19
+ session_id: str
20
+ mode: Literal["compact", "full", "raw"] = "compact"
21
+ full: bool = False # Deprecated: kept for backward compatibility
22
+
23
+ @model_validator(mode="after")
24
+ def migrate_full_to_mode(self) -> SnapshotRequest:
25
+ """Migrate legacy 'full' field to 'mode' field.
26
+
27
+ If 'full=True' is set and mode is still the default 'compact',
28
+ upgrade mode to 'full' for backward compatibility.
29
+ """
30
+ if self.full and self.mode == "compact":
31
+ object.__setattr__(self, "mode", "full")
32
+ return self
33
+
34
+
35
+ class SessionRequest(BaseModel):
36
+ session_id: str
37
+
38
+
39
+ class ActionRequest(BaseModel):
40
+ session_id: str
41
+ ref: str # Can be @ref, text:"...", id:..., desc:"...", or coords:x,y
42
+
43
+
44
+ class SetTextRequest(BaseModel):
45
+ session_id: str
46
+ ref: str
47
+ text: str
48
+
49
+
50
+ class WaitIdleRequest(BaseModel):
51
+ session_id: str
52
+ timeout_ms: int | None = None
53
+
54
+
55
+ class WaitActivityRequest(BaseModel):
56
+ session_id: str
57
+ activity: str
58
+ timeout_ms: int | None = None
59
+
60
+
61
+ class WaitTextRequest(BaseModel):
62
+ session_id: str
63
+ text: str
64
+ timeout_ms: int | None = None
65
+
66
+
67
+ class WaitSelectorRequest(BaseModel):
68
+ session_id: str
69
+ ref: str | None = None
70
+ selector: dict[str, str] | None = None
71
+ timeout_ms: int | None = None
72
+
73
+
74
+ class DeviceSettingRequest(BaseModel):
75
+ serial: str
76
+ state: str
77
+
78
+
79
+ class AppResetRequest(BaseModel):
80
+ session_id: str
81
+ package: str
82
+
83
+
84
+ class RotationRequest(BaseModel):
85
+ serial: str
86
+ orientation: str # portrait, landscape, reverse-portrait, reverse-landscape, auto
87
+
88
+
89
+ class WifiRequest(BaseModel):
90
+ serial: str
91
+ enabled: bool
92
+
93
+
94
+ class MobileRequest(BaseModel):
95
+ serial: str
96
+ enabled: bool
97
+
98
+
99
+ class DozeRequest(BaseModel):
100
+ serial: str
101
+ enabled: bool
102
+
103
+
104
+ class AppLaunchRequest(BaseModel):
105
+ session_id: str
106
+ package: str
107
+ activity: str | None = None
108
+
109
+
110
+ class AppForceStopRequest(BaseModel):
111
+ session_id: str
112
+ package: str
113
+
114
+
115
+ class AppDeeplinkRequest(BaseModel):
116
+ session_id: str
117
+ uri: str
118
+
119
+
120
+ class EmulatorSnapshotRequest(BaseModel):
121
+ serial: str
122
+ name: str
123
+
124
+
125
+ class ArtifactLogsRequest(BaseModel):
126
+ session_id: str
127
+ since: str | None = None
128
+
129
+
130
+ class SwipeRequest(BaseModel):
131
+ """Request for swipe action."""
132
+
133
+ session_id: str
134
+ direction: str # up, down, left, right
135
+ container: str | None = None # Optional @ref or selector
136
+ distance: float = 0.8
137
+ duration_ms: int = 300
138
+
139
+
140
+ class DeviceTargetRequest(BaseModel):
141
+ session_id: str | None = None
142
+ serial: str | None = None
143
+
144
+ @model_validator(mode="after")
145
+ def validate_target(self) -> DeviceTargetRequest:
146
+ if bool(self.session_id) == bool(self.serial):
147
+ raise ValueError("Provide exactly one of session_id or serial")
148
+ return self
149
+
150
+
151
+ class AppListRequest(DeviceTargetRequest):
152
+ scope: str = "all"
153
+
154
+
155
+ class ReliabilityPackageRequest(DeviceTargetRequest):
156
+ package: str
157
+
158
+
159
+ class ReliabilityExitInfoRequest(ReliabilityPackageRequest):
160
+ list_only: bool = False
161
+
162
+
163
+ class ReliabilityEventsRequest(DeviceTargetRequest):
164
+ pattern: str | None = None
165
+ since: str | None = None
166
+ package: str | None = None
167
+
168
+
169
+ class ReliabilityDropboxListRequest(DeviceTargetRequest):
170
+ package: str | None = None
171
+
172
+
173
+ class ReliabilityDropboxPrintRequest(DeviceTargetRequest):
174
+ tag: str
175
+
176
+
177
+ class ReliabilityBugreportRequest(DeviceTargetRequest):
178
+ filename: str | None = None
179
+
180
+
181
+ class ReliabilityBackgroundRequest(ReliabilityPackageRequest):
182
+ pass
183
+
184
+
185
+ class ReliabilityTrimMemoryRequest(ReliabilityPackageRequest):
186
+ level: str = "RUNNING_CRITICAL"
187
+
188
+
189
+ class ReliabilityOomAdjRequest(ReliabilityPackageRequest):
190
+ score: int = 1000
191
+
192
+
193
+ class ReliabilityCompileRequest(ReliabilityPackageRequest):
194
+ mode: Literal["reset", "speed"]
195
+
196
+
197
+ class ReliabilityToggleRequest(DeviceTargetRequest):
198
+ state: str # on|off
199
+
200
+
201
+ class ReliabilityRunAsRequest(ReliabilityPackageRequest):
202
+ path: str = "files/"
203
+
204
+
205
+ class ReliabilityDumpheapRequest(ReliabilityPackageRequest):
206
+ keep_remote: bool = False
207
+
208
+
209
+ class ReliabilitySigquitRequest(ReliabilityPackageRequest):
210
+ pass
211
+
212
+
213
+ class FilePushRequest(DeviceTargetRequest):
214
+ local_path: str
215
+ remote_path: str | None = None
216
+
217
+
218
+ class FilePullRequest(DeviceTargetRequest):
219
+ remote_path: str
220
+ local_path: str | None = None
221
+
222
+
223
+ class FileAppPushRequest(DeviceTargetRequest):
224
+ package: str
225
+ local_path: str
226
+ remote_path: str | None = None
227
+
228
+
229
+ class FileAppPullRequest(DeviceTargetRequest):
230
+ package: str
231
+ remote_path: str
232
+ local_path: str | None = None
233
+
234
+
235
+ class FileFindRequest(DeviceTargetRequest):
236
+ path: str
237
+ name: str
238
+ kind: Literal["file", "dir", "any"] = "any"
239
+ max_depth: int = 4
240
+
241
+
242
+ class FileListRequest(DeviceTargetRequest):
243
+ path: str
244
+ kind: Literal["file", "dir", "any"] = "any"