android-midscene-automation 0.2.4 → 1.1.1
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/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Device Pool Manager for discovering, tracking, and allocating mobile devices."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from artemis.runtime.device_lock import DeviceExecutionLock
|
|
29
|
+
from artemis.toolchain import toolchain
|
|
30
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DeviceStatus:
|
|
37
|
+
"""State and lock allocation metadata for a connected device."""
|
|
38
|
+
|
|
39
|
+
serial: str
|
|
40
|
+
state: str # "device", "offline", "unauthorized", etc.
|
|
41
|
+
model: str | None = None
|
|
42
|
+
product: str | None = None
|
|
43
|
+
is_emulator: bool = False
|
|
44
|
+
is_busy: bool = False
|
|
45
|
+
active_pid: int | None = None
|
|
46
|
+
active_task_desc: str | None = None
|
|
47
|
+
active_session_id: str | None = None
|
|
48
|
+
acquired_at: str | None = None
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"serial": self.serial,
|
|
53
|
+
"state": self.state,
|
|
54
|
+
"model": self.model,
|
|
55
|
+
"product": self.product,
|
|
56
|
+
"is_emulator": self.is_emulator,
|
|
57
|
+
"is_busy": self.is_busy,
|
|
58
|
+
"active_pid": self.active_pid,
|
|
59
|
+
"active_task_desc": self.active_task_desc,
|
|
60
|
+
"active_session_id": self.active_session_id,
|
|
61
|
+
"acquired_at": self.acquired_at,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DevicePool:
|
|
66
|
+
"""Manages discovery and assignment across all connected Android devices."""
|
|
67
|
+
|
|
68
|
+
# A snapshot younger than this is returned without touching adb, so a burst
|
|
69
|
+
# of concurrent enumerations (UI polling, readiness probes, submissions)
|
|
70
|
+
# collapses into a single `adb devices` call.
|
|
71
|
+
CACHE_TTL = 1.0
|
|
72
|
+
# When enumeration fails outright (adb server restarting, subprocess error),
|
|
73
|
+
# the last successful snapshot keeps answering for this long. Real
|
|
74
|
+
# disconnects are not failures -- adb reports them as a successful listing
|
|
75
|
+
# without the device -- so serving stale here only bridges server-level blips.
|
|
76
|
+
STALE_ON_ERROR_TTL = 10.0
|
|
77
|
+
# Until one enumeration has succeeded, `adb devices` may need to spawn the
|
|
78
|
+
# adb server itself, which routinely exceeds the hot-path budget.
|
|
79
|
+
HOT_QUERY_TIMEOUT = 2.0
|
|
80
|
+
COLD_QUERY_TIMEOUT = 8.0
|
|
81
|
+
|
|
82
|
+
def __init__(self, adb_path: str | None = None):
|
|
83
|
+
self._adb_path = adb_path
|
|
84
|
+
self._cache_lock = threading.Lock()
|
|
85
|
+
self._cached_raw: list[tuple[str, str, str | None, str | None]] | None = None
|
|
86
|
+
self._cached_at = 0.0
|
|
87
|
+
self._warmed = False
|
|
88
|
+
self._sync_query_gate = threading.Lock()
|
|
89
|
+
self._async_inflight: tuple[asyncio.AbstractEventLoop, asyncio.Task] | None = None
|
|
90
|
+
|
|
91
|
+
def _resolve_adb(self) -> str | None:
|
|
92
|
+
if self._adb_path:
|
|
93
|
+
return self._adb_path
|
|
94
|
+
try:
|
|
95
|
+
return toolchain.resolve("adb") or shutil.which("adb")
|
|
96
|
+
except Exception:
|
|
97
|
+
return shutil.which("adb")
|
|
98
|
+
|
|
99
|
+
def _current_query_timeout(self) -> float:
|
|
100
|
+
return self.HOT_QUERY_TIMEOUT if self._warmed else self.COLD_QUERY_TIMEOUT
|
|
101
|
+
|
|
102
|
+
def _query_adb_devices_sync(
|
|
103
|
+
self, timeout: float | None = None
|
|
104
|
+
) -> list[tuple[str, str, str | None, str | None]] | None:
|
|
105
|
+
"""Run `adb devices -l` synchronously. Returns None when the query
|
|
106
|
+
itself failed, as opposed to an empty list of attached devices."""
|
|
107
|
+
adb = self._resolve_adb()
|
|
108
|
+
if not adb:
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
res = subprocess.run(
|
|
112
|
+
[adb, "devices", "-l"],
|
|
113
|
+
stdin=subprocess.DEVNULL,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
timeout=timeout if timeout is not None else self._current_query_timeout(),
|
|
117
|
+
check=False,
|
|
118
|
+
)
|
|
119
|
+
if res.returncode != 0:
|
|
120
|
+
return None
|
|
121
|
+
return self._parse_device_lines(res.stdout.splitlines())
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
logger.debug(f"Error querying adb devices: {exc}")
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
async def _query_adb_devices_async(
|
|
127
|
+
self, timeout: float | None = None
|
|
128
|
+
) -> list[tuple[str, str, str | None, str | None]] | None:
|
|
129
|
+
"""Run `adb devices -l` asynchronously. Returns None when the query
|
|
130
|
+
itself failed, as opposed to an empty list of attached devices."""
|
|
131
|
+
adb = self._resolve_adb()
|
|
132
|
+
if not adb:
|
|
133
|
+
return None
|
|
134
|
+
proc = None
|
|
135
|
+
try:
|
|
136
|
+
proc = await asyncio.create_subprocess_exec(
|
|
137
|
+
adb,
|
|
138
|
+
"devices",
|
|
139
|
+
"-l",
|
|
140
|
+
stdout=subprocess.PIPE,
|
|
141
|
+
stderr=subprocess.PIPE,
|
|
142
|
+
)
|
|
143
|
+
stdout, _ = await asyncio.wait_for(
|
|
144
|
+
proc.communicate(),
|
|
145
|
+
timeout=timeout if timeout is not None else self._current_query_timeout(),
|
|
146
|
+
)
|
|
147
|
+
if proc.returncode != 0:
|
|
148
|
+
return None
|
|
149
|
+
return self._parse_device_lines(stdout.decode(errors="replace").splitlines())
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
logger.debug(f"Error querying adb devices asynchronously: {exc}")
|
|
152
|
+
if proc is not None:
|
|
153
|
+
try:
|
|
154
|
+
proc.kill()
|
|
155
|
+
except (ProcessLookupError, OSError):
|
|
156
|
+
# Process already exited; nothing left to clean up.
|
|
157
|
+
pass
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _parse_device_lines(lines: list[str]) -> list[tuple[str, str, str | None, str | None]]:
|
|
162
|
+
results: list[tuple[str, str, str | None, str | None]] = []
|
|
163
|
+
for raw_line in lines:
|
|
164
|
+
line = raw_line.strip()
|
|
165
|
+
if not line or line.startswith("List of devices"):
|
|
166
|
+
continue
|
|
167
|
+
parts = line.split()
|
|
168
|
+
if len(parts) < 2:
|
|
169
|
+
continue
|
|
170
|
+
serial = parts[0]
|
|
171
|
+
state = parts[1]
|
|
172
|
+
model = None
|
|
173
|
+
product = None
|
|
174
|
+
for token in parts[2:]:
|
|
175
|
+
if token.startswith("model:"):
|
|
176
|
+
model = token.split(":", 1)[1].replace("_", " ")
|
|
177
|
+
elif token.startswith("product:"):
|
|
178
|
+
product = token.split(":", 1)[1]
|
|
179
|
+
results.append((serial, state, model, product))
|
|
180
|
+
return results
|
|
181
|
+
|
|
182
|
+
def _cached_snapshot(
|
|
183
|
+
self, *, allow_stale: bool
|
|
184
|
+
) -> list[tuple[str, str, str | None, str | None]] | None:
|
|
185
|
+
with self._cache_lock:
|
|
186
|
+
if self._cached_raw is None:
|
|
187
|
+
return None
|
|
188
|
+
age = time.monotonic() - self._cached_at
|
|
189
|
+
if age <= self.CACHE_TTL or (allow_stale and age <= self.STALE_ON_ERROR_TTL):
|
|
190
|
+
return list(self._cached_raw)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def _store_snapshot(self, raw: list[tuple[str, str, str | None, str | None]]) -> None:
|
|
194
|
+
with self._cache_lock:
|
|
195
|
+
self._cached_raw = list(raw)
|
|
196
|
+
self._cached_at = time.monotonic()
|
|
197
|
+
self._warmed = True
|
|
198
|
+
|
|
199
|
+
def _enumerate_sync(self) -> list[tuple[str, str, str | None, str | None]] | None:
|
|
200
|
+
"""Return the raw enumeration, or None when the query failed and no
|
|
201
|
+
usable snapshot exists -- never an ambiguous empty list on failure."""
|
|
202
|
+
cached = self._cached_snapshot(allow_stale=False)
|
|
203
|
+
if cached is not None:
|
|
204
|
+
return cached
|
|
205
|
+
# The gate serializes concurrent enumerations: followers block until the
|
|
206
|
+
# leader finishes, then hit the snapshot it just stored.
|
|
207
|
+
with self._sync_query_gate:
|
|
208
|
+
cached = self._cached_snapshot(allow_stale=False)
|
|
209
|
+
if cached is not None:
|
|
210
|
+
return cached
|
|
211
|
+
raw = self._query_adb_devices_sync()
|
|
212
|
+
if raw is None:
|
|
213
|
+
return self._cached_snapshot(allow_stale=True)
|
|
214
|
+
self._store_snapshot(raw)
|
|
215
|
+
return raw
|
|
216
|
+
|
|
217
|
+
async def _enumerate_async(self) -> list[tuple[str, str, str | None, str | None]] | None:
|
|
218
|
+
cached = self._cached_snapshot(allow_stale=False)
|
|
219
|
+
if cached is not None:
|
|
220
|
+
return cached
|
|
221
|
+
loop = asyncio.get_running_loop()
|
|
222
|
+
inflight = self._async_inflight
|
|
223
|
+
if inflight is not None and inflight[0] is loop and not inflight[1].done():
|
|
224
|
+
# Shield followers so one cancelled waiter cannot abort the shared query.
|
|
225
|
+
return await asyncio.shield(inflight[1])
|
|
226
|
+
task = loop.create_task(self._enumerate_async_uncached())
|
|
227
|
+
self._async_inflight = (loop, task)
|
|
228
|
+
try:
|
|
229
|
+
return await task
|
|
230
|
+
finally:
|
|
231
|
+
if self._async_inflight is not None and self._async_inflight[1] is task:
|
|
232
|
+
self._async_inflight = None
|
|
233
|
+
|
|
234
|
+
async def _enumerate_async_uncached(
|
|
235
|
+
self,
|
|
236
|
+
) -> list[tuple[str, str, str | None, str | None]] | None:
|
|
237
|
+
raw = await self._query_adb_devices_async()
|
|
238
|
+
if raw is None:
|
|
239
|
+
return self._cached_snapshot(allow_stale=True)
|
|
240
|
+
self._store_snapshot(raw)
|
|
241
|
+
return raw
|
|
242
|
+
|
|
243
|
+
async def _start_adb_server(self, timeout: float) -> None:
|
|
244
|
+
"""Best-effort bounded `adb start-server` so later queries hit a warm daemon."""
|
|
245
|
+
adb = self._resolve_adb()
|
|
246
|
+
if not adb:
|
|
247
|
+
return
|
|
248
|
+
proc = None
|
|
249
|
+
try:
|
|
250
|
+
clean_env = os.environ.copy()
|
|
251
|
+
clean_env.pop("ADB_SERVER_SOCKET", None)
|
|
252
|
+
proc = await asyncio.create_subprocess_exec(
|
|
253
|
+
adb,
|
|
254
|
+
"start-server",
|
|
255
|
+
stdout=subprocess.PIPE,
|
|
256
|
+
stderr=subprocess.PIPE,
|
|
257
|
+
env=clean_env,
|
|
258
|
+
)
|
|
259
|
+
await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
logger.debug(f"adb start-server warm-up did not complete: {exc}")
|
|
262
|
+
if proc is not None:
|
|
263
|
+
try:
|
|
264
|
+
proc.kill()
|
|
265
|
+
except (ProcessLookupError, OSError):
|
|
266
|
+
# Process already exited; nothing left to clean up.
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
async def warm_up_async(
|
|
270
|
+
self,
|
|
271
|
+
*,
|
|
272
|
+
server_timeout: float = 10.0,
|
|
273
|
+
settle_timeout: float = 3.0,
|
|
274
|
+
poll_interval: float = 0.5,
|
|
275
|
+
) -> bool:
|
|
276
|
+
"""Start the adb server and complete one successful enumeration.
|
|
277
|
+
|
|
278
|
+
Meant to run once before an entrypoint starts accepting task
|
|
279
|
+
submissions, so the first requests never race an adb cold start.
|
|
280
|
+
Devices reconnect asynchronously after the server comes up, so the
|
|
281
|
+
settle window keeps polling until at least one device reaches the
|
|
282
|
+
ready state (or the window closes). Returns True once any enumeration
|
|
283
|
+
succeeded -- zero attached devices is still a warm pool.
|
|
284
|
+
"""
|
|
285
|
+
if self._resolve_adb() is None:
|
|
286
|
+
return False
|
|
287
|
+
await self._start_adb_server(timeout=server_timeout)
|
|
288
|
+
deadline = time.monotonic() + max(settle_timeout, 0.0)
|
|
289
|
+
succeeded = False
|
|
290
|
+
while True:
|
|
291
|
+
raw = await self._query_adb_devices_async()
|
|
292
|
+
if raw is not None:
|
|
293
|
+
succeeded = True
|
|
294
|
+
self._store_snapshot(raw)
|
|
295
|
+
if any(state == "device" for _, state, _, _ in raw):
|
|
296
|
+
return True
|
|
297
|
+
if time.monotonic() >= deadline:
|
|
298
|
+
return succeeded
|
|
299
|
+
await asyncio.sleep(poll_interval)
|
|
300
|
+
|
|
301
|
+
def _build_statuses(
|
|
302
|
+
self, raw_devices: list[tuple[str, str, str | None, str | None]]
|
|
303
|
+
) -> list[DeviceStatus]:
|
|
304
|
+
active_owners = DeviceExecutionLock.get_active_owners()
|
|
305
|
+
|
|
306
|
+
devices: list[DeviceStatus] = []
|
|
307
|
+
for serial, state, model, product in raw_devices:
|
|
308
|
+
is_emu = (
|
|
309
|
+
serial.startswith("emulator-") or "127.0.0.1" in serial or "localhost" in serial
|
|
310
|
+
)
|
|
311
|
+
clean_id = DeviceExecutionLock._normalize_lock_id(
|
|
312
|
+
serial,
|
|
313
|
+
os.getenv(DeviceExecutionLock.LOCK_SCOPE_ENV) or None,
|
|
314
|
+
)
|
|
315
|
+
owner = active_owners.get(clean_id) or active_owners.get(
|
|
316
|
+
DeviceExecutionLock._normalize_device_id(serial)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
status = DeviceStatus(
|
|
320
|
+
serial=serial,
|
|
321
|
+
state=state,
|
|
322
|
+
model=model,
|
|
323
|
+
product=product,
|
|
324
|
+
is_emulator=is_emu,
|
|
325
|
+
is_busy=owner is not None,
|
|
326
|
+
active_pid=owner.pid if owner else None,
|
|
327
|
+
active_task_desc=owner.description if owner else None,
|
|
328
|
+
active_session_id=owner.session_id if owner else None,
|
|
329
|
+
acquired_at=owner.acquired_at if owner else None,
|
|
330
|
+
)
|
|
331
|
+
devices.append(status)
|
|
332
|
+
return devices
|
|
333
|
+
|
|
334
|
+
def list_devices(self) -> list[DeviceStatus]:
|
|
335
|
+
"""Synchronously list all connected devices along with their active lock state.
|
|
336
|
+
|
|
337
|
+
The raw adb enumeration may be served from a short-lived snapshot;
|
|
338
|
+
lock ownership is always computed fresh. A failed enumeration collapses
|
|
339
|
+
to an empty list -- use try_list_devices when the caller must tell the
|
|
340
|
+
two apart.
|
|
341
|
+
"""
|
|
342
|
+
return self._build_statuses(self._enumerate_sync() or [])
|
|
343
|
+
|
|
344
|
+
async def list_devices_async(self) -> list[DeviceStatus]:
|
|
345
|
+
"""Asynchronously list all connected devices along with their active lock state.
|
|
346
|
+
|
|
347
|
+
The raw adb enumeration may be served from a short-lived snapshot;
|
|
348
|
+
lock ownership is always computed fresh. A failed enumeration collapses
|
|
349
|
+
to an empty list -- use try_list_devices_async when the caller must tell
|
|
350
|
+
the two apart.
|
|
351
|
+
"""
|
|
352
|
+
return self._build_statuses(await self._enumerate_async() or [])
|
|
353
|
+
|
|
354
|
+
def try_list_devices(self) -> list[DeviceStatus] | None:
|
|
355
|
+
"""Like list_devices, but returns None when enumeration failed and no
|
|
356
|
+
usable snapshot exists, so callers can distinguish "could not ask adb"
|
|
357
|
+
from "adb answered: no devices attached"."""
|
|
358
|
+
raw = self._enumerate_sync()
|
|
359
|
+
return None if raw is None else self._build_statuses(raw)
|
|
360
|
+
|
|
361
|
+
async def try_list_devices_async(self) -> list[DeviceStatus] | None:
|
|
362
|
+
"""Async variant of try_list_devices."""
|
|
363
|
+
raw = await self._enumerate_async()
|
|
364
|
+
return None if raw is None else self._build_statuses(raw)
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def _explicit_serial_error(
|
|
368
|
+
requested_serial: str, devices: list[DeviceStatus] | None
|
|
369
|
+
) -> str | None:
|
|
370
|
+
# Fail open on an indeterminate or empty enumeration (adb missing, server
|
|
371
|
+
# blip, devices still handshaking): the submission proceeds and fails
|
|
372
|
+
# downstream with a clear no-device error instead of a false hard reject.
|
|
373
|
+
if not devices:
|
|
374
|
+
return None
|
|
375
|
+
norm = DeviceExecutionLock._normalize_device_id
|
|
376
|
+
state_by_serial = {norm(d.serial): d.state for d in devices}
|
|
377
|
+
dev_state = state_by_serial.get(norm(requested_serial))
|
|
378
|
+
if dev_state == "device":
|
|
379
|
+
return None
|
|
380
|
+
if dev_state is None:
|
|
381
|
+
return (
|
|
382
|
+
f"Device '{requested_serial}' is not connected. "
|
|
383
|
+
f"Attached devices: {sorted(state_by_serial)}."
|
|
384
|
+
)
|
|
385
|
+
return f"Device '{requested_serial}' is attached but not ready (state: '{dev_state}')."
|
|
386
|
+
|
|
387
|
+
def validate_explicit_serial(self, requested_serial: str) -> str | None:
|
|
388
|
+
"""Return a rejection message for an explicitly requested serial, or None.
|
|
389
|
+
|
|
390
|
+
Only a successful, non-empty enumeration may reject: it is the single
|
|
391
|
+
authority for the strict device-binding check shared by the admission
|
|
392
|
+
paths (admin console router/queue and the MCP task runner).
|
|
393
|
+
"""
|
|
394
|
+
return self._explicit_serial_error(requested_serial, self.try_list_devices())
|
|
395
|
+
|
|
396
|
+
async def validate_explicit_serial_async(self, requested_serial: str) -> str | None:
|
|
397
|
+
"""Async variant of validate_explicit_serial."""
|
|
398
|
+
return self._explicit_serial_error(requested_serial, await self.try_list_devices_async())
|
|
399
|
+
|
|
400
|
+
def get_ready_devices(self) -> list[DeviceStatus]:
|
|
401
|
+
"""Return devices in authorized 'device' state."""
|
|
402
|
+
return [d for d in self.list_devices() if d.state == "device"]
|
|
403
|
+
|
|
404
|
+
def get_idle_devices(self) -> list[DeviceStatus]:
|
|
405
|
+
"""Return ready devices that are not currently holding an execution lock."""
|
|
406
|
+
return [d for d in self.list_devices() if d.state == "device" and not d.is_busy]
|
|
407
|
+
|
|
408
|
+
def get_claimed_serials(self) -> set[str]:
|
|
409
|
+
"""Return device serials Artemis currently claims on the ADB server.
|
|
410
|
+
|
|
411
|
+
A device is claimed while any Artemis process holds its execution lock
|
|
412
|
+
or has a live pending queue reservation targeting it. Placeholder
|
|
413
|
+
reservations not yet bound to a concrete device ("pending"/"any")
|
|
414
|
+
claim nothing. Read-only: computed purely from cross-process lock and
|
|
415
|
+
queue metadata, without any adb traffic.
|
|
416
|
+
"""
|
|
417
|
+
claimed: set[str] = set()
|
|
418
|
+
try:
|
|
419
|
+
for owner in DeviceExecutionLock.get_active_owners().values():
|
|
420
|
+
if owner.device_id:
|
|
421
|
+
claimed.add(str(owner.device_id))
|
|
422
|
+
for item in DeviceExecutionLock.get_queued_tasks():
|
|
423
|
+
serial = item.get("device_serial")
|
|
424
|
+
if serial:
|
|
425
|
+
claimed.add(str(serial))
|
|
426
|
+
except Exception as exc:
|
|
427
|
+
logger.debug(f"Could not compute claimed device serials: {exc}")
|
|
428
|
+
claimed.discard("pending")
|
|
429
|
+
claimed.discard("any")
|
|
430
|
+
return claimed
|
|
431
|
+
|
|
432
|
+
def select_device(self, preferred_serial: str | None = None) -> str | None:
|
|
433
|
+
"""Select a target device serial for task execution.
|
|
434
|
+
|
|
435
|
+
If `preferred_serial` is provided:
|
|
436
|
+
Returns preferred_serial if it is present or online.
|
|
437
|
+
Otherwise:
|
|
438
|
+
Selects the first idle/unlocked ready device. If all are busy,
|
|
439
|
+
selects the first ready device so the task will queue on it.
|
|
440
|
+
Returns None if no devices are attached.
|
|
441
|
+
"""
|
|
442
|
+
return self._pick_device(self.list_devices(), preferred_serial)
|
|
443
|
+
|
|
444
|
+
async def select_device_async(self, preferred_serial: str | None = None) -> str | None:
|
|
445
|
+
"""Select a device without blocking the caller on ADB enumeration."""
|
|
446
|
+
return self._pick_device(await self.list_devices_async(), preferred_serial)
|
|
447
|
+
|
|
448
|
+
def _pick_device(
|
|
449
|
+
self, all_devs: list[DeviceStatus], preferred_serial: str | None
|
|
450
|
+
) -> str | None:
|
|
451
|
+
ready_devs = [d for d in all_devs if d.state == "device"]
|
|
452
|
+
|
|
453
|
+
if preferred_serial:
|
|
454
|
+
# Check if preferred is available or exists
|
|
455
|
+
for dev in all_devs:
|
|
456
|
+
if dev.serial == preferred_serial:
|
|
457
|
+
return dev.serial
|
|
458
|
+
# If not detected by ADB, still return it so ADB can attempt connection
|
|
459
|
+
return preferred_serial
|
|
460
|
+
|
|
461
|
+
# Pick first idle device
|
|
462
|
+
for dev in ready_devs:
|
|
463
|
+
if not dev.is_busy:
|
|
464
|
+
return dev.serial
|
|
465
|
+
|
|
466
|
+
# If all busy, return the first ready device (it will queue)
|
|
467
|
+
if ready_devs:
|
|
468
|
+
return ready_devs[0].serial
|
|
469
|
+
|
|
470
|
+
# Fallback to any attached device if none ready
|
|
471
|
+
if all_devs:
|
|
472
|
+
return all_devs[0].serial
|
|
473
|
+
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# Global device pool instance
|
|
478
|
+
device_pool = DevicePool()
|