minitap-mobile-use 0.0.1.dev0__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.
Potentially problematic release.
This version of minitap-mobile-use might be problematic. Click here for more details.
- minitap/mobile_use/__init__.py +0 -0
- minitap/mobile_use/agents/contextor/contextor.py +42 -0
- minitap/mobile_use/agents/cortex/cortex.md +93 -0
- minitap/mobile_use/agents/cortex/cortex.py +107 -0
- minitap/mobile_use/agents/cortex/types.py +11 -0
- minitap/mobile_use/agents/executor/executor.md +73 -0
- minitap/mobile_use/agents/executor/executor.py +84 -0
- minitap/mobile_use/agents/executor/executor_context_cleaner.py +27 -0
- minitap/mobile_use/agents/executor/utils.py +11 -0
- minitap/mobile_use/agents/hopper/hopper.md +13 -0
- minitap/mobile_use/agents/hopper/hopper.py +45 -0
- minitap/mobile_use/agents/orchestrator/human.md +13 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.md +18 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.py +114 -0
- minitap/mobile_use/agents/orchestrator/types.py +14 -0
- minitap/mobile_use/agents/outputter/human.md +25 -0
- minitap/mobile_use/agents/outputter/outputter.py +75 -0
- minitap/mobile_use/agents/outputter/test_outputter.py +107 -0
- minitap/mobile_use/agents/planner/human.md +12 -0
- minitap/mobile_use/agents/planner/planner.md +64 -0
- minitap/mobile_use/agents/planner/planner.py +64 -0
- minitap/mobile_use/agents/planner/types.py +44 -0
- minitap/mobile_use/agents/planner/utils.py +45 -0
- minitap/mobile_use/agents/summarizer/summarizer.py +34 -0
- minitap/mobile_use/clients/device_hardware_client.py +23 -0
- minitap/mobile_use/clients/ios_client.py +44 -0
- minitap/mobile_use/clients/screen_api_client.py +53 -0
- minitap/mobile_use/config.py +285 -0
- minitap/mobile_use/constants.py +2 -0
- minitap/mobile_use/context.py +65 -0
- minitap/mobile_use/controllers/__init__.py +0 -0
- minitap/mobile_use/controllers/mobile_command_controller.py +379 -0
- minitap/mobile_use/controllers/platform_specific_commands_controller.py +74 -0
- minitap/mobile_use/graph/graph.py +149 -0
- minitap/mobile_use/graph/state.py +73 -0
- minitap/mobile_use/main.py +122 -0
- minitap/mobile_use/sdk/__init__.py +12 -0
- minitap/mobile_use/sdk/agent.py +524 -0
- minitap/mobile_use/sdk/builders/__init__.py +10 -0
- minitap/mobile_use/sdk/builders/agent_config_builder.py +213 -0
- minitap/mobile_use/sdk/builders/index.py +15 -0
- minitap/mobile_use/sdk/builders/task_request_builder.py +218 -0
- minitap/mobile_use/sdk/constants.py +14 -0
- minitap/mobile_use/sdk/examples/README.md +45 -0
- minitap/mobile_use/sdk/examples/__init__.py +1 -0
- minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
- minitap/mobile_use/sdk/examples/smart_notification_assistant.py +177 -0
- minitap/mobile_use/sdk/types/__init__.py +49 -0
- minitap/mobile_use/sdk/types/agent.py +73 -0
- minitap/mobile_use/sdk/types/exceptions.py +74 -0
- minitap/mobile_use/sdk/types/task.py +191 -0
- minitap/mobile_use/sdk/utils.py +28 -0
- minitap/mobile_use/servers/config.py +19 -0
- minitap/mobile_use/servers/device_hardware_bridge.py +212 -0
- minitap/mobile_use/servers/device_screen_api.py +143 -0
- minitap/mobile_use/servers/start_servers.py +151 -0
- minitap/mobile_use/servers/stop_servers.py +215 -0
- minitap/mobile_use/servers/utils.py +11 -0
- minitap/mobile_use/services/accessibility.py +100 -0
- minitap/mobile_use/services/llm.py +143 -0
- minitap/mobile_use/tools/index.py +54 -0
- minitap/mobile_use/tools/mobile/back.py +52 -0
- minitap/mobile_use/tools/mobile/copy_text_from.py +77 -0
- minitap/mobile_use/tools/mobile/erase_text.py +124 -0
- minitap/mobile_use/tools/mobile/input_text.py +74 -0
- minitap/mobile_use/tools/mobile/launch_app.py +59 -0
- minitap/mobile_use/tools/mobile/list_packages.py +78 -0
- minitap/mobile_use/tools/mobile/long_press_on.py +62 -0
- minitap/mobile_use/tools/mobile/open_link.py +59 -0
- minitap/mobile_use/tools/mobile/paste_text.py +66 -0
- minitap/mobile_use/tools/mobile/press_key.py +58 -0
- minitap/mobile_use/tools/mobile/run_flow.py +57 -0
- minitap/mobile_use/tools/mobile/stop_app.py +58 -0
- minitap/mobile_use/tools/mobile/swipe.py +56 -0
- minitap/mobile_use/tools/mobile/take_screenshot.py +70 -0
- minitap/mobile_use/tools/mobile/tap.py +66 -0
- minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +68 -0
- minitap/mobile_use/tools/tool_wrapper.py +33 -0
- minitap/mobile_use/utils/cli_helpers.py +40 -0
- minitap/mobile_use/utils/cli_selection.py +144 -0
- minitap/mobile_use/utils/conversations.py +31 -0
- minitap/mobile_use/utils/decorators.py +123 -0
- minitap/mobile_use/utils/errors.py +6 -0
- minitap/mobile_use/utils/file.py +13 -0
- minitap/mobile_use/utils/logger.py +184 -0
- minitap/mobile_use/utils/media.py +73 -0
- minitap/mobile_use/utils/recorder.py +55 -0
- minitap/mobile_use/utils/requests_utils.py +37 -0
- minitap/mobile_use/utils/shell_utils.py +20 -0
- minitap/mobile_use/utils/time.py +6 -0
- minitap/mobile_use/utils/ui_hierarchy.py +30 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/METADATA +274 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/RECORD +95 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/WHEEL +4 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from minitap.mobile_use.context import DevicePlatform
|
|
11
|
+
from minitap.mobile_use.servers.utils import is_port_in_use
|
|
12
|
+
|
|
13
|
+
MAESTRO_STUDIO_PORT = 9999
|
|
14
|
+
DEVICE_HARDWARE_BRIDGE_PORT = MAESTRO_STUDIO_PORT
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BridgeStatus(Enum):
|
|
18
|
+
STOPPED = "stopped"
|
|
19
|
+
STARTING = "starting"
|
|
20
|
+
RUNNING = "running"
|
|
21
|
+
NO_DEVICE = "no_device"
|
|
22
|
+
PORT_IN_USE = "port_in_use"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DeviceHardwareBridge:
|
|
27
|
+
def __init__(self, device_id: str, platform: DevicePlatform, adb_host: Optional[str] = None):
|
|
28
|
+
self.process = None
|
|
29
|
+
self.status = BridgeStatus.STOPPED
|
|
30
|
+
self.thread = None
|
|
31
|
+
self.output = []
|
|
32
|
+
self.lock = threading.Lock()
|
|
33
|
+
self.device_id: str = device_id
|
|
34
|
+
self.platform: DevicePlatform = platform
|
|
35
|
+
self.adb_host: Optional[str] = adb_host
|
|
36
|
+
|
|
37
|
+
def _run_maestro_studio(self):
|
|
38
|
+
try:
|
|
39
|
+
creation_flags = 0
|
|
40
|
+
if hasattr(subprocess, "CREATE_NO_WINDOW"):
|
|
41
|
+
creation_flags = subprocess.CREATE_NO_WINDOW
|
|
42
|
+
|
|
43
|
+
maestro_platform = "android" if self.platform == DevicePlatform.ANDROID else "ios"
|
|
44
|
+
cmd = ["maestro", "--device", self.device_id, "--platform", maestro_platform]
|
|
45
|
+
if self.adb_host is not None:
|
|
46
|
+
cmd.append(f"--host={self.adb_host}")
|
|
47
|
+
cmd.extend(["studio", "--no-window"])
|
|
48
|
+
|
|
49
|
+
self.process = subprocess.Popen(
|
|
50
|
+
args=cmd,
|
|
51
|
+
stdout=subprocess.PIPE,
|
|
52
|
+
stderr=subprocess.PIPE,
|
|
53
|
+
text=True,
|
|
54
|
+
encoding="utf-8",
|
|
55
|
+
errors="replace",
|
|
56
|
+
creationflags=creation_flags,
|
|
57
|
+
shell=platform.system() == "Windows",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
with self.lock:
|
|
61
|
+
self.status = BridgeStatus.STARTING
|
|
62
|
+
|
|
63
|
+
stdout_thread = threading.Thread(target=self._read_stdout, daemon=True)
|
|
64
|
+
stdout_thread.start()
|
|
65
|
+
|
|
66
|
+
stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
|
|
67
|
+
stderr_thread.start()
|
|
68
|
+
|
|
69
|
+
stdout_thread.join()
|
|
70
|
+
stderr_thread.join()
|
|
71
|
+
|
|
72
|
+
if self.process:
|
|
73
|
+
self.process.wait()
|
|
74
|
+
|
|
75
|
+
except FileNotFoundError:
|
|
76
|
+
print("Error: 'maestro' command not found. Is Maestro installed and in your PATH?")
|
|
77
|
+
with self.lock:
|
|
78
|
+
self.status = BridgeStatus.FAILED
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"An unexpected error occurred: {e}")
|
|
81
|
+
with self.lock:
|
|
82
|
+
self.status = BridgeStatus.FAILED
|
|
83
|
+
finally:
|
|
84
|
+
with self.lock:
|
|
85
|
+
if self.status not in [
|
|
86
|
+
BridgeStatus.RUNNING,
|
|
87
|
+
BridgeStatus.NO_DEVICE,
|
|
88
|
+
BridgeStatus.PORT_IN_USE,
|
|
89
|
+
]:
|
|
90
|
+
self.status = BridgeStatus.STOPPED
|
|
91
|
+
print("Maestro Studio process has terminated.")
|
|
92
|
+
|
|
93
|
+
def _read_stdout(self):
|
|
94
|
+
if not self.process or not self.process.stdout:
|
|
95
|
+
return
|
|
96
|
+
for line in iter(self.process.stdout.readline, ""):
|
|
97
|
+
if not line:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
line = line.strip()
|
|
101
|
+
print(f"[Maestro Studio]: {line}")
|
|
102
|
+
self.output.append(line)
|
|
103
|
+
|
|
104
|
+
if "No running devices found" in line:
|
|
105
|
+
with self.lock:
|
|
106
|
+
self.status = BridgeStatus.NO_DEVICE
|
|
107
|
+
if self.process:
|
|
108
|
+
self.process.kill()
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
connected_match = re.search(r"Running on (\S+)", line)
|
|
112
|
+
if connected_match:
|
|
113
|
+
with self.lock:
|
|
114
|
+
self.device_id = connected_match.group(1)
|
|
115
|
+
|
|
116
|
+
if "Maestro Studio is running at" in line:
|
|
117
|
+
if self._wait_for_health_check():
|
|
118
|
+
with self.lock:
|
|
119
|
+
self.status = BridgeStatus.RUNNING
|
|
120
|
+
else:
|
|
121
|
+
with self.lock:
|
|
122
|
+
self.status = BridgeStatus.FAILED
|
|
123
|
+
if self.process:
|
|
124
|
+
self.process.kill()
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
def _read_stderr(self):
|
|
128
|
+
if not self.process or not self.process.stderr:
|
|
129
|
+
return
|
|
130
|
+
for line in iter(self.process.stderr.readline, ""):
|
|
131
|
+
if not line:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
line = line.strip()
|
|
135
|
+
print(f"[Maestro Studio ERROR]: {line}")
|
|
136
|
+
self.output.append(line)
|
|
137
|
+
|
|
138
|
+
if "device offline" in line.lower():
|
|
139
|
+
with self.lock:
|
|
140
|
+
self.status = BridgeStatus.FAILED
|
|
141
|
+
if self.process:
|
|
142
|
+
self.process.kill()
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if "address already in use" in line.lower():
|
|
146
|
+
with self.lock:
|
|
147
|
+
self.status = BridgeStatus.PORT_IN_USE
|
|
148
|
+
if self.process:
|
|
149
|
+
self.process.kill()
|
|
150
|
+
break
|
|
151
|
+
else:
|
|
152
|
+
with self.lock:
|
|
153
|
+
if self.status == BridgeStatus.STARTING:
|
|
154
|
+
self.status = BridgeStatus.FAILED
|
|
155
|
+
|
|
156
|
+
def _wait_for_health_check(self, retries=5, delay=2):
|
|
157
|
+
health_url = f"http://localhost:{DEVICE_HARDWARE_BRIDGE_PORT}/api/banner-message"
|
|
158
|
+
for _ in range(retries):
|
|
159
|
+
try:
|
|
160
|
+
response = requests.get(health_url, timeout=3)
|
|
161
|
+
if response.status_code == 200 and "level" in response.json():
|
|
162
|
+
print("Health check successful.")
|
|
163
|
+
return True
|
|
164
|
+
except requests.exceptions.RequestException:
|
|
165
|
+
pass
|
|
166
|
+
time.sleep(delay)
|
|
167
|
+
print("Health check failed after multiple retries.")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
def _should_start_maestro(self):
|
|
171
|
+
return self.status in [
|
|
172
|
+
BridgeStatus.STOPPED,
|
|
173
|
+
BridgeStatus.FAILED,
|
|
174
|
+
BridgeStatus.NO_DEVICE,
|
|
175
|
+
BridgeStatus.PORT_IN_USE,
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
def start(self):
|
|
179
|
+
if is_port_in_use(DEVICE_HARDWARE_BRIDGE_PORT):
|
|
180
|
+
print("Maestro port already in use - assuming Maestro is running.")
|
|
181
|
+
self.status = BridgeStatus.RUNNING
|
|
182
|
+
return True
|
|
183
|
+
if self._should_start_maestro():
|
|
184
|
+
self.status = BridgeStatus.STARTING
|
|
185
|
+
self.output.clear()
|
|
186
|
+
self.thread = threading.Thread(target=self._run_maestro_studio, daemon=True)
|
|
187
|
+
self.thread.start()
|
|
188
|
+
return True
|
|
189
|
+
print(f"Cannot start, current status is {self.status.value}")
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def wait(self):
|
|
193
|
+
if self.thread:
|
|
194
|
+
self.thread.join()
|
|
195
|
+
|
|
196
|
+
def stop(self):
|
|
197
|
+
if self.process:
|
|
198
|
+
self.process.kill()
|
|
199
|
+
self.process = None
|
|
200
|
+
if self.thread and self.thread.is_alive():
|
|
201
|
+
self.thread.join(timeout=2)
|
|
202
|
+
with self.lock:
|
|
203
|
+
self.status = BridgeStatus.STOPPED
|
|
204
|
+
print("Maestro Studio stopped.")
|
|
205
|
+
|
|
206
|
+
def get_status(self):
|
|
207
|
+
with self.lock:
|
|
208
|
+
return {"status": self.status.value, "output": self.output[-10:]}
|
|
209
|
+
|
|
210
|
+
def get_device_id(self) -> Optional[str]:
|
|
211
|
+
with self.lock:
|
|
212
|
+
return self.device_id
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import uvicorn
|
|
9
|
+
from fastapi import FastAPI, HTTPException
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
from minitap.mobile_use.servers.config import server_settings
|
|
12
|
+
from minitap.mobile_use.servers.utils import is_port_in_use
|
|
13
|
+
from sseclient import SSEClient
|
|
14
|
+
|
|
15
|
+
DEVICE_HARDWARE_BRIDGE_BASE_URL = server_settings.DEVICE_HARDWARE_BRIDGE_BASE_URL
|
|
16
|
+
DEVICE_HARDWARE_BRIDGE_API_URL = f"{DEVICE_HARDWARE_BRIDGE_BASE_URL}/api"
|
|
17
|
+
|
|
18
|
+
_latest_screen_data = None
|
|
19
|
+
_data_lock = threading.Lock()
|
|
20
|
+
_stream_thread = None
|
|
21
|
+
_stop_event = threading.Event()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _stream_worker():
|
|
25
|
+
global _latest_screen_data
|
|
26
|
+
sse_url = f"{DEVICE_HARDWARE_BRIDGE_API_URL}/device-screen/sse"
|
|
27
|
+
headers = {"Accept": "text/event-stream"}
|
|
28
|
+
|
|
29
|
+
while not _stop_event.is_set():
|
|
30
|
+
try:
|
|
31
|
+
with requests.get(sse_url, stream=True, headers=headers) as response:
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
print("--- Stream connected, listening for events... ---")
|
|
34
|
+
event_source = (chunk for chunk in response.iter_content())
|
|
35
|
+
client = SSEClient(event_source)
|
|
36
|
+
for event in client.events():
|
|
37
|
+
if _stop_event.is_set():
|
|
38
|
+
break
|
|
39
|
+
if event.event == "message" and event.data:
|
|
40
|
+
data = json.loads(event.data)
|
|
41
|
+
screenshot_path = data.get("screenshot")
|
|
42
|
+
elements = data.get("elements", [])
|
|
43
|
+
width = data.get("width")
|
|
44
|
+
height = data.get("height")
|
|
45
|
+
platform = data.get("platform")
|
|
46
|
+
|
|
47
|
+
image_url = f"{DEVICE_HARDWARE_BRIDGE_BASE_URL}{screenshot_path}"
|
|
48
|
+
image_response = requests.get(image_url)
|
|
49
|
+
image_response.raise_for_status()
|
|
50
|
+
base64_image = base64.b64encode(image_response.content).decode("utf-8")
|
|
51
|
+
base64_data_url = f"data:image/png;base64,{base64_image}"
|
|
52
|
+
|
|
53
|
+
with _data_lock:
|
|
54
|
+
_latest_screen_data = {
|
|
55
|
+
"base64": base64_data_url,
|
|
56
|
+
"elements": elements,
|
|
57
|
+
"width": width,
|
|
58
|
+
"height": height,
|
|
59
|
+
"platform": platform,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
except requests.exceptions.RequestException as e:
|
|
63
|
+
print(f"Connection error in stream worker: {e}. Retrying in 2 seconds...")
|
|
64
|
+
with _data_lock:
|
|
65
|
+
_latest_screen_data = None
|
|
66
|
+
time.sleep(2)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def start_stream():
|
|
70
|
+
global _stream_thread
|
|
71
|
+
if _stream_thread is None or not _stream_thread.is_alive():
|
|
72
|
+
_stop_event.clear()
|
|
73
|
+
_stream_thread = threading.Thread(target=_stream_worker, daemon=True)
|
|
74
|
+
_stream_thread.start()
|
|
75
|
+
print("--- Background screen streaming started ---")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def stop_stream():
|
|
79
|
+
global _stream_thread
|
|
80
|
+
if _stream_thread and _stream_thread.is_alive():
|
|
81
|
+
_stop_event.set()
|
|
82
|
+
_stream_thread.join(timeout=2)
|
|
83
|
+
print("--- Background screen streaming stopped ---")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@asynccontextmanager
|
|
87
|
+
async def lifespan(_: FastAPI):
|
|
88
|
+
start_stream()
|
|
89
|
+
yield
|
|
90
|
+
stop_stream()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
app = FastAPI(lifespan=lifespan)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_latest_data():
|
|
97
|
+
"""Helper to get the latest data safely, with retries."""
|
|
98
|
+
max_wait_time = 30 # seconds
|
|
99
|
+
retry_delay = 2 # seconds
|
|
100
|
+
start_time = time.time()
|
|
101
|
+
|
|
102
|
+
while time.time() - start_time < max_wait_time:
|
|
103
|
+
with _data_lock:
|
|
104
|
+
if _latest_screen_data is not None:
|
|
105
|
+
return _latest_screen_data
|
|
106
|
+
time.sleep(retry_delay)
|
|
107
|
+
|
|
108
|
+
raise HTTPException(
|
|
109
|
+
status_code=503,
|
|
110
|
+
detail="Screen data is not yet available after multiple retries.",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.get("/screen-info")
|
|
115
|
+
async def get_screen_info():
|
|
116
|
+
data = get_latest_data()
|
|
117
|
+
return JSONResponse(content=data)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.get("/health")
|
|
121
|
+
async def health_check():
|
|
122
|
+
"""Check if the Maestro Studio server is healthy."""
|
|
123
|
+
health_url = f"{DEVICE_HARDWARE_BRIDGE_API_URL}/banner-message"
|
|
124
|
+
try:
|
|
125
|
+
response = requests.get(health_url, timeout=5)
|
|
126
|
+
response.raise_for_status()
|
|
127
|
+
with _data_lock:
|
|
128
|
+
if _latest_screen_data is None:
|
|
129
|
+
raise HTTPException(
|
|
130
|
+
status_code=503,
|
|
131
|
+
detail="Screen data is not yet available after multiple retries.",
|
|
132
|
+
)
|
|
133
|
+
return JSONResponse(content=response.json())
|
|
134
|
+
except requests.exceptions.RequestException as e:
|
|
135
|
+
raise HTTPException(status_code=503, detail=f"Maestro Studio not available: {e}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def start():
|
|
139
|
+
if not is_port_in_use(server_settings.DEVICE_SCREEN_API_PORT):
|
|
140
|
+
uvicorn.run(app, host="0.0.0.0", port=server_settings.DEVICE_SCREEN_API_PORT)
|
|
141
|
+
return True
|
|
142
|
+
print(f"Device screen API is already running on port {server_settings.DEVICE_SCREEN_API_PORT}")
|
|
143
|
+
return False
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import multiprocessing
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import typer
|
|
11
|
+
from minitap.mobile_use.context import DevicePlatform
|
|
12
|
+
from minitap.mobile_use.servers.config import server_settings
|
|
13
|
+
from minitap.mobile_use.servers.device_hardware_bridge import DeviceHardwareBridge
|
|
14
|
+
from minitap.mobile_use.servers.device_screen_api import start as _start_device_screen_api
|
|
15
|
+
from minitap.mobile_use.servers.stop_servers import stop_servers
|
|
16
|
+
from minitap.mobile_use.utils.logger import get_server_logger
|
|
17
|
+
|
|
18
|
+
logger = get_server_logger()
|
|
19
|
+
|
|
20
|
+
running_processes = []
|
|
21
|
+
bridge_instance = None
|
|
22
|
+
shutdown_requested = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_device_screen_api_health(base_url: Optional[str] = None, max_retries=30, delay=1):
|
|
26
|
+
base_url = base_url or f"http://localhost:{server_settings.DEVICE_SCREEN_API_PORT}"
|
|
27
|
+
health_url = f"{base_url}/health"
|
|
28
|
+
|
|
29
|
+
max_retries = int(os.getenv("MOBILE_USE_HEALTH_RETRIES", max_retries))
|
|
30
|
+
delay = int(os.getenv("MOBILE_USE_HEALTH_DELAY", delay))
|
|
31
|
+
|
|
32
|
+
for attempt in range(max_retries):
|
|
33
|
+
try:
|
|
34
|
+
response = requests.get(health_url, timeout=3)
|
|
35
|
+
if response.status_code == 200:
|
|
36
|
+
logger.success(
|
|
37
|
+
f"Device Screen API is healthy on port {server_settings.DEVICE_SCREEN_API_PORT}"
|
|
38
|
+
)
|
|
39
|
+
return True
|
|
40
|
+
except requests.exceptions.RequestException:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
if attempt == 0:
|
|
44
|
+
logger.info(f"Waiting for Device Screen API to become healthy on {base_url}...")
|
|
45
|
+
|
|
46
|
+
time.sleep(delay)
|
|
47
|
+
|
|
48
|
+
logger.error(f"Device Screen API failed to become healthy after {max_retries} attempts")
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _start_device_screen_api_process() -> Optional[multiprocessing.Process]:
|
|
53
|
+
try:
|
|
54
|
+
process = multiprocessing.Process(target=start_device_screen_api, daemon=True)
|
|
55
|
+
process.start()
|
|
56
|
+
return process
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.error(f"Failed to start Device Screen API process: {e}")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def start_device_hardware_bridge(
|
|
63
|
+
device_id: str, platform: DevicePlatform
|
|
64
|
+
) -> Optional[DeviceHardwareBridge]:
|
|
65
|
+
logger.info("Starting Device Hardware Bridge...")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
bridge = DeviceHardwareBridge(
|
|
69
|
+
device_id=device_id,
|
|
70
|
+
platform=platform,
|
|
71
|
+
adb_host=server_settings.ADB_HOST,
|
|
72
|
+
)
|
|
73
|
+
success = bridge.start()
|
|
74
|
+
|
|
75
|
+
if success:
|
|
76
|
+
logger.info("Device Hardware Bridge started successfully")
|
|
77
|
+
return bridge
|
|
78
|
+
else:
|
|
79
|
+
logger.error("Failed to start Device Hardware Bridge. Exiting.")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Error starting Device Hardware Bridge: {e}")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def start_device_screen_api(use_process: bool = False):
|
|
88
|
+
logger.info("Starting Device Screen API...")
|
|
89
|
+
if use_process:
|
|
90
|
+
api_process = _start_device_screen_api_process()
|
|
91
|
+
if not api_process:
|
|
92
|
+
logger.error("Failed to start Device Screen API. Exiting.")
|
|
93
|
+
return False
|
|
94
|
+
logger.info("Device Screen API started successfully")
|
|
95
|
+
return api_process
|
|
96
|
+
else:
|
|
97
|
+
return _start_device_screen_api()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
cli = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SupportedServers(str, Enum):
|
|
104
|
+
DEVICE_HARDWARE_BRIDGE = "hardware_bridge"
|
|
105
|
+
DEVICE_SCREEN_API = "screen_api"
|
|
106
|
+
ALL = "all"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@cli.command()
|
|
110
|
+
def start(
|
|
111
|
+
device_id: Annotated[str, typer.Option("--device", help="Device ID")],
|
|
112
|
+
platform: Annotated[DevicePlatform, typer.Option("--platform", help="Device platform")],
|
|
113
|
+
only: Annotated[
|
|
114
|
+
SupportedServers, typer.Option("--only", help="Start only one server")
|
|
115
|
+
] = SupportedServers.ALL,
|
|
116
|
+
):
|
|
117
|
+
servers_to_stop = {"device_screen_api": False, "device_hardware_bridge": False}
|
|
118
|
+
|
|
119
|
+
def signal_handler(signum, frame):
|
|
120
|
+
logger.info("Signal received, stopping servers...")
|
|
121
|
+
if any(servers_to_stop.values()):
|
|
122
|
+
stop_servers(**servers_to_stop)
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
126
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if only == SupportedServers.ALL:
|
|
130
|
+
hardware_bridge = start_device_hardware_bridge(device_id=device_id, platform=platform)
|
|
131
|
+
if hardware_bridge:
|
|
132
|
+
servers_to_stop["device_hardware_bridge"] = True
|
|
133
|
+
start_device_screen_api(use_process=False)
|
|
134
|
+
|
|
135
|
+
elif only == SupportedServers.DEVICE_HARDWARE_BRIDGE:
|
|
136
|
+
hardware_bridge = start_device_hardware_bridge(device_id=device_id, platform=platform)
|
|
137
|
+
if hardware_bridge:
|
|
138
|
+
servers_to_stop["device_hardware_bridge"] = True
|
|
139
|
+
hardware_bridge.wait()
|
|
140
|
+
|
|
141
|
+
elif only == SupportedServers.DEVICE_SCREEN_API:
|
|
142
|
+
start_device_screen_api(use_process=False)
|
|
143
|
+
|
|
144
|
+
except KeyboardInterrupt:
|
|
145
|
+
logger.info("Keyboard interrupt received. Shutting down...")
|
|
146
|
+
finally:
|
|
147
|
+
signal_handler(signal.SIGINT, None)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
cli()
|