feat: discover and add connected iOS devices
Tests / Test apps.device-host-agent.tests.test_mcp_token.test_load_or_create_concurrent_calls_do_not_corrupt failed
Tests / Test apps.device-host-agent.tests.test_mcp_token.test_load_or_create_concurrent_calls_do_not_corrupt failed
This commit is contained in:
@@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Resp
|
||||
|
||||
from core.errors import DeviceNotFoundError, DeviceOfflineError, DeviceRuntimeError
|
||||
from device.manager import DeviceManager
|
||||
from driver.registry import build_driver_factory
|
||||
from host_agent.assignment import AssignmentExecutor
|
||||
from host_agent.client import (
|
||||
HostAgentClient,
|
||||
@@ -26,6 +27,7 @@ from host_agent.conversation_log import ConversationLogStore
|
||||
from host_agent.devices import register_local_device, unregister_local_device
|
||||
from host_agent.history import ConsoleHistoryStore
|
||||
from host_agent.identity import HostIdentityStore
|
||||
from host_agent.ios_discovery import IOSDiscoveryError, discover_connected_ios_devices
|
||||
from host_agent.local_account import LocalAccountStore
|
||||
from host_agent.mcp_lock import McpBusyTracker
|
||||
from host_agent.mcp_token import McpTokenStore
|
||||
@@ -526,6 +528,87 @@ def create_console_app(
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/api/devices/discover-ios")
|
||||
async def api_discover_ios_devices(
|
||||
session: SessionState = Depends(require_session),
|
||||
) -> JSONResponse:
|
||||
try:
|
||||
discovered = await asyncio.to_thread(discover_connected_ios_devices)
|
||||
except IOSDiscoveryError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
configured = await asyncio.to_thread(config_store.list)
|
||||
configured_udids = {
|
||||
str(record["connection_info"].get("udid"))
|
||||
for record in configured
|
||||
if record["driver_type"] == "wda"
|
||||
}
|
||||
used_wda_ports = {
|
||||
record["connection_info"].get("wda_local_port") for record in configured
|
||||
}
|
||||
used_mjpeg_ports = {
|
||||
record["connection_info"].get("mjpegServerPort") for record in configured
|
||||
}
|
||||
next_wda_port = 8100
|
||||
next_mjpeg_port = 9100
|
||||
result = []
|
||||
for device in discovered:
|
||||
while next_wda_port in used_wda_ports:
|
||||
next_wda_port += 1
|
||||
while next_mjpeg_port in used_mjpeg_ports:
|
||||
next_mjpeg_port += 1
|
||||
result.append(
|
||||
{
|
||||
**device,
|
||||
"configured": device["udid"] in configured_udids,
|
||||
"suggested_wda_port": next_wda_port,
|
||||
"suggested_mjpeg_port": next_mjpeg_port,
|
||||
}
|
||||
)
|
||||
used_wda_ports.add(next_wda_port)
|
||||
used_mjpeg_ports.add(next_mjpeg_port)
|
||||
next_wda_port += 1
|
||||
next_mjpeg_port += 1
|
||||
return JSONResponse({"devices": result})
|
||||
|
||||
@app.post("/api/devices/test-connection")
|
||||
async def api_device_test_connection(
|
||||
request: Request,
|
||||
session: SessionState = Depends(require_csrf),
|
||||
) -> JSONResponse:
|
||||
payload = await request.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="Request must be an object.")
|
||||
driver_type = str(payload.get("driver_type", "")).strip()
|
||||
connection_info = payload.get("connection_info")
|
||||
if not driver_type or not isinstance(connection_info, dict):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Driver type and connection info are required.",
|
||||
)
|
||||
|
||||
driver = None
|
||||
connected = False
|
||||
try:
|
||||
driver = build_driver_factory(driver_type, connection_info)()
|
||||
await asyncio.to_thread(driver.connect)
|
||||
connected = True
|
||||
await asyncio.to_thread(driver.health_check)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=str(exc) or "device connection test failed",
|
||||
) from exc
|
||||
finally:
|
||||
if connected and driver is not None:
|
||||
try:
|
||||
await asyncio.to_thread(driver.disconnect)
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({"ok": True, "driver_type": driver_type})
|
||||
|
||||
@app.post("/devices/save")
|
||||
async def devices_save(
|
||||
request: Request,
|
||||
@@ -552,6 +635,26 @@ def create_console_app(
|
||||
else:
|
||||
connection_info = parsed
|
||||
|
||||
if error is None:
|
||||
# The console exposes the common Appium settings as regular form
|
||||
# fields. Advanced JSON remains available for uncommon capabilities.
|
||||
field_map = {
|
||||
"server_url": "server_url",
|
||||
"udid": "udid",
|
||||
"device_name": "device_name",
|
||||
}
|
||||
for form_key, config_key in field_map.items():
|
||||
value = str(form.get(form_key, "")).strip()
|
||||
if value:
|
||||
connection_info[config_key] = value
|
||||
port_key = "wda_local_port" if driver_type == "wda" else "system_port"
|
||||
port_value = str(form.get(port_key, "")).strip()
|
||||
if port_value:
|
||||
try:
|
||||
connection_info[port_key] = int(port_value)
|
||||
except ValueError:
|
||||
error = f"{port_key} must be an integer."
|
||||
|
||||
if error is None:
|
||||
try:
|
||||
await asyncio.to_thread(
|
||||
|
||||
Reference in New Issue
Block a user