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.

Files changed (95) hide show
  1. minitap/mobile_use/__init__.py +0 -0
  2. minitap/mobile_use/agents/contextor/contextor.py +42 -0
  3. minitap/mobile_use/agents/cortex/cortex.md +93 -0
  4. minitap/mobile_use/agents/cortex/cortex.py +107 -0
  5. minitap/mobile_use/agents/cortex/types.py +11 -0
  6. minitap/mobile_use/agents/executor/executor.md +73 -0
  7. minitap/mobile_use/agents/executor/executor.py +84 -0
  8. minitap/mobile_use/agents/executor/executor_context_cleaner.py +27 -0
  9. minitap/mobile_use/agents/executor/utils.py +11 -0
  10. minitap/mobile_use/agents/hopper/hopper.md +13 -0
  11. minitap/mobile_use/agents/hopper/hopper.py +45 -0
  12. minitap/mobile_use/agents/orchestrator/human.md +13 -0
  13. minitap/mobile_use/agents/orchestrator/orchestrator.md +18 -0
  14. minitap/mobile_use/agents/orchestrator/orchestrator.py +114 -0
  15. minitap/mobile_use/agents/orchestrator/types.py +14 -0
  16. minitap/mobile_use/agents/outputter/human.md +25 -0
  17. minitap/mobile_use/agents/outputter/outputter.py +75 -0
  18. minitap/mobile_use/agents/outputter/test_outputter.py +107 -0
  19. minitap/mobile_use/agents/planner/human.md +12 -0
  20. minitap/mobile_use/agents/planner/planner.md +64 -0
  21. minitap/mobile_use/agents/planner/planner.py +64 -0
  22. minitap/mobile_use/agents/planner/types.py +44 -0
  23. minitap/mobile_use/agents/planner/utils.py +45 -0
  24. minitap/mobile_use/agents/summarizer/summarizer.py +34 -0
  25. minitap/mobile_use/clients/device_hardware_client.py +23 -0
  26. minitap/mobile_use/clients/ios_client.py +44 -0
  27. minitap/mobile_use/clients/screen_api_client.py +53 -0
  28. minitap/mobile_use/config.py +285 -0
  29. minitap/mobile_use/constants.py +2 -0
  30. minitap/mobile_use/context.py +65 -0
  31. minitap/mobile_use/controllers/__init__.py +0 -0
  32. minitap/mobile_use/controllers/mobile_command_controller.py +379 -0
  33. minitap/mobile_use/controllers/platform_specific_commands_controller.py +74 -0
  34. minitap/mobile_use/graph/graph.py +149 -0
  35. minitap/mobile_use/graph/state.py +73 -0
  36. minitap/mobile_use/main.py +122 -0
  37. minitap/mobile_use/sdk/__init__.py +12 -0
  38. minitap/mobile_use/sdk/agent.py +524 -0
  39. minitap/mobile_use/sdk/builders/__init__.py +10 -0
  40. minitap/mobile_use/sdk/builders/agent_config_builder.py +213 -0
  41. minitap/mobile_use/sdk/builders/index.py +15 -0
  42. minitap/mobile_use/sdk/builders/task_request_builder.py +218 -0
  43. minitap/mobile_use/sdk/constants.py +14 -0
  44. minitap/mobile_use/sdk/examples/README.md +45 -0
  45. minitap/mobile_use/sdk/examples/__init__.py +1 -0
  46. minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
  47. minitap/mobile_use/sdk/examples/smart_notification_assistant.py +177 -0
  48. minitap/mobile_use/sdk/types/__init__.py +49 -0
  49. minitap/mobile_use/sdk/types/agent.py +73 -0
  50. minitap/mobile_use/sdk/types/exceptions.py +74 -0
  51. minitap/mobile_use/sdk/types/task.py +191 -0
  52. minitap/mobile_use/sdk/utils.py +28 -0
  53. minitap/mobile_use/servers/config.py +19 -0
  54. minitap/mobile_use/servers/device_hardware_bridge.py +212 -0
  55. minitap/mobile_use/servers/device_screen_api.py +143 -0
  56. minitap/mobile_use/servers/start_servers.py +151 -0
  57. minitap/mobile_use/servers/stop_servers.py +215 -0
  58. minitap/mobile_use/servers/utils.py +11 -0
  59. minitap/mobile_use/services/accessibility.py +100 -0
  60. minitap/mobile_use/services/llm.py +143 -0
  61. minitap/mobile_use/tools/index.py +54 -0
  62. minitap/mobile_use/tools/mobile/back.py +52 -0
  63. minitap/mobile_use/tools/mobile/copy_text_from.py +77 -0
  64. minitap/mobile_use/tools/mobile/erase_text.py +124 -0
  65. minitap/mobile_use/tools/mobile/input_text.py +74 -0
  66. minitap/mobile_use/tools/mobile/launch_app.py +59 -0
  67. minitap/mobile_use/tools/mobile/list_packages.py +78 -0
  68. minitap/mobile_use/tools/mobile/long_press_on.py +62 -0
  69. minitap/mobile_use/tools/mobile/open_link.py +59 -0
  70. minitap/mobile_use/tools/mobile/paste_text.py +66 -0
  71. minitap/mobile_use/tools/mobile/press_key.py +58 -0
  72. minitap/mobile_use/tools/mobile/run_flow.py +57 -0
  73. minitap/mobile_use/tools/mobile/stop_app.py +58 -0
  74. minitap/mobile_use/tools/mobile/swipe.py +56 -0
  75. minitap/mobile_use/tools/mobile/take_screenshot.py +70 -0
  76. minitap/mobile_use/tools/mobile/tap.py +66 -0
  77. minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +68 -0
  78. minitap/mobile_use/tools/tool_wrapper.py +33 -0
  79. minitap/mobile_use/utils/cli_helpers.py +40 -0
  80. minitap/mobile_use/utils/cli_selection.py +144 -0
  81. minitap/mobile_use/utils/conversations.py +31 -0
  82. minitap/mobile_use/utils/decorators.py +123 -0
  83. minitap/mobile_use/utils/errors.py +6 -0
  84. minitap/mobile_use/utils/file.py +13 -0
  85. minitap/mobile_use/utils/logger.py +184 -0
  86. minitap/mobile_use/utils/media.py +73 -0
  87. minitap/mobile_use/utils/recorder.py +55 -0
  88. minitap/mobile_use/utils/requests_utils.py +37 -0
  89. minitap/mobile_use/utils/shell_utils.py +20 -0
  90. minitap/mobile_use/utils/time.py +6 -0
  91. minitap/mobile_use/utils/ui_hierarchy.py +30 -0
  92. minitap_mobile_use-0.0.1.dev0.dist-info/METADATA +274 -0
  93. minitap_mobile_use-0.0.1.dev0.dist-info/RECORD +95 -0
  94. minitap_mobile_use-0.0.1.dev0.dist-info/WHEEL +4 -0
  95. 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()