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,98 @@
|
|
|
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
|
+
"""Single source of truth for PID liveness probing across the Artemis codebase.
|
|
16
|
+
|
|
17
|
+
Semantics (decided by the maintainers):
|
|
18
|
+
|
|
19
|
+
- psutil only. No bare ``os.kill(pid, 0)`` fallback: on Windows, CPython
|
|
20
|
+
implements ``os.kill(pid, 0)`` for non-CTRL signals via
|
|
21
|
+
``OpenProcess``/``TerminateProcess`` handle probing, which is both unreliable
|
|
22
|
+
for dead pids and semantically surprising.
|
|
23
|
+
- ``psutil.NoSuchProcess`` -> dead. Zombie processes -> dead.
|
|
24
|
+
- ``psutil.AccessDenied`` / ``PermissionError`` -> **alive**. When liveness
|
|
25
|
+
cannot be determined the probe defaults to "alive": it is always safer to
|
|
26
|
+
skip reaping a resource than to reap one that is still owned.
|
|
27
|
+
- ``created_at`` (a ``psutil``-style process create time) enables PID-reuse
|
|
28
|
+
protection: when provided and the probed process's create time differs by
|
|
29
|
+
one second or more, the original process is dead and its PID was recycled.
|
|
30
|
+
- Any other unexpected error -> alive (default-alive), with a warning log.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def pid_is_alive(pid: int, created_at: float | None = None) -> bool:
|
|
41
|
+
"""Return whether the process identified by ``pid`` is still alive.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
pid: The process id to probe. Non-positive or unparsable pids are dead.
|
|
45
|
+
created_at: Optional expected process create time (as reported by
|
|
46
|
+
``psutil.Process.create_time()``). When given and > 0, a create-time
|
|
47
|
+
mismatch of >= 1 second means the PID was recycled -> dead.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True when the process is alive or liveness cannot be determined
|
|
51
|
+
(default-alive); False when the process is definitively gone, a
|
|
52
|
+
zombie, or its PID has been recycled by another process.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
pid = int(pid)
|
|
56
|
+
except (TypeError, ValueError):
|
|
57
|
+
return False
|
|
58
|
+
if pid <= 0:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
import psutil
|
|
63
|
+
except Exception as exc: # pragma: no cover - psutil is a hard dependency
|
|
64
|
+
logger.warning(f"psutil unavailable while probing pid {pid}: {exc}; assuming alive")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
process = psutil.Process(pid)
|
|
69
|
+
if not process.is_running():
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
if process.status() == psutil.STATUS_ZOMBIE:
|
|
74
|
+
return False
|
|
75
|
+
except psutil.NoSuchProcess:
|
|
76
|
+
return False
|
|
77
|
+
except (psutil.AccessDenied, PermissionError):
|
|
78
|
+
# Cannot inspect the status; keep the default-alive verdict.
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
if created_at is not None and created_at > 0:
|
|
82
|
+
try:
|
|
83
|
+
if abs(process.create_time() - float(created_at)) >= 1.0:
|
|
84
|
+
# Same PID, different birth time: the PID was recycled.
|
|
85
|
+
return False
|
|
86
|
+
except psutil.NoSuchProcess:
|
|
87
|
+
return False
|
|
88
|
+
except (psutil.AccessDenied, PermissionError):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
return True
|
|
92
|
+
except psutil.NoSuchProcess:
|
|
93
|
+
return False
|
|
94
|
+
except (psutil.AccessDenied, PermissionError):
|
|
95
|
+
return True
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.warning(f"Could not determine liveness of pid {pid}: {exc}; assuming alive")
|
|
98
|
+
return True
|
|
@@ -0,0 +1,435 @@
|
|
|
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
|
+
"""Artemis Server Lifecycle and Process Management.
|
|
16
|
+
|
|
17
|
+
Provides cross-platform facilities to discover, inspect, terminate,
|
|
18
|
+
and restart running Artemis UI / backend server instances regardless
|
|
19
|
+
of which terminal or process spawned them.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
import shutil
|
|
28
|
+
import socket
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
from typing import Any, Callable
|
|
33
|
+
import urllib.error
|
|
34
|
+
import urllib.request
|
|
35
|
+
|
|
36
|
+
from artemis.config.paths import ROOT_DIR, get_server_info_file
|
|
37
|
+
from artemis.runtime.device_lock import DeviceExecutionLock
|
|
38
|
+
from artemis.runtime.process_probe import pid_is_alive
|
|
39
|
+
from artemis.runtime.supervisor import ProcessSupervisor
|
|
40
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_port_in_use(port: int, host: str = "127.0.0.1", timeout: float = 0.5) -> bool:
|
|
46
|
+
"""Check whether a TCP port is currently listening / occupied."""
|
|
47
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
48
|
+
s.settimeout(timeout)
|
|
49
|
+
try:
|
|
50
|
+
return s.connect_ex((host, port)) == 0
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def write_server_info(
|
|
56
|
+
port: int,
|
|
57
|
+
host: str = "127.0.0.1",
|
|
58
|
+
pid: int | None = None,
|
|
59
|
+
lifecycle_token: str | None = None,
|
|
60
|
+
) -> Path:
|
|
61
|
+
"""Persist current Artemis server runtime metadata to well-known path."""
|
|
62
|
+
info_file = get_server_info_file()
|
|
63
|
+
try:
|
|
64
|
+
info_file.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
data = {
|
|
66
|
+
"pid": pid or os.getpid(),
|
|
67
|
+
"port": port,
|
|
68
|
+
"host": host,
|
|
69
|
+
"started_at": time.time(),
|
|
70
|
+
"cwd": str(ROOT_DIR),
|
|
71
|
+
"sys_executable": sys.executable,
|
|
72
|
+
"cmdline": sys.argv,
|
|
73
|
+
}
|
|
74
|
+
if lifecycle_token:
|
|
75
|
+
data["lifecycle_token"] = lifecycle_token
|
|
76
|
+
info_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
77
|
+
logger.debug(f"Saved Artemis server info (PID: {data['pid']}, port: {port}) to {info_file}")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.warning(f"Could not persist server metadata to {info_file}: {e}")
|
|
80
|
+
return info_file
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def read_server_info() -> dict[str, Any] | None:
|
|
84
|
+
"""Read and validate the persisted server runtime metadata if present."""
|
|
85
|
+
info_file = get_server_info_file()
|
|
86
|
+
if not info_file.exists():
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
content = info_file.read_text(encoding="utf-8").strip()
|
|
90
|
+
if not content:
|
|
91
|
+
return None
|
|
92
|
+
data = json.loads(content)
|
|
93
|
+
if isinstance(data, dict) and "pid" in data:
|
|
94
|
+
return data
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.debug(f"Failed to read server info from {info_file}: {e}")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def clear_server_info(
|
|
101
|
+
*,
|
|
102
|
+
port: int | None = None,
|
|
103
|
+
lifecycle_token: str | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Remove persisted metadata, optionally only for the matching server.
|
|
106
|
+
|
|
107
|
+
Scoped removal keeps ``artemis stop --port <unused>`` and a late shutdown
|
|
108
|
+
finalizer from deleting metadata written by a different server instance.
|
|
109
|
+
"""
|
|
110
|
+
info_file = get_server_info_file()
|
|
111
|
+
try:
|
|
112
|
+
if info_file.exists():
|
|
113
|
+
if port is not None or lifecycle_token is not None:
|
|
114
|
+
info = read_server_info()
|
|
115
|
+
if not info:
|
|
116
|
+
return
|
|
117
|
+
if port is not None and info.get("port") != port:
|
|
118
|
+
return
|
|
119
|
+
if lifecycle_token is not None and info.get("lifecycle_token") != lifecycle_token:
|
|
120
|
+
return
|
|
121
|
+
info_file.unlink()
|
|
122
|
+
logger.debug(f"Cleared server info file at {info_file}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.debug(f"Error clearing server info file {info_file}: {e}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def request_graceful_shutdown(port: int, timeout: float = 1.5) -> bool:
|
|
128
|
+
"""Ask a locally managed Artemis server to run its shutdown lifecycle.
|
|
129
|
+
|
|
130
|
+
The per-process token in the server metadata prevents this control request
|
|
131
|
+
from being accepted remotely or accidentally sent to an unrelated service
|
|
132
|
+
using the same port. A missing/stale token simply makes the caller fall back
|
|
133
|
+
to process termination.
|
|
134
|
+
"""
|
|
135
|
+
info = read_server_info()
|
|
136
|
+
if not info or info.get("port") != port:
|
|
137
|
+
return False
|
|
138
|
+
token = info.get("lifecycle_token")
|
|
139
|
+
if not isinstance(token, str) or not token:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
request = urllib.request.Request(
|
|
143
|
+
f"http://127.0.0.1:{port}/api/system/shutdown",
|
|
144
|
+
method="POST",
|
|
145
|
+
headers={
|
|
146
|
+
"User-Agent": "Artemis-Lifecycle-Client/1.0",
|
|
147
|
+
"X-Artemis-Lifecycle-Token": token,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
152
|
+
return response.status in (200, 202)
|
|
153
|
+
except (OSError, urllib.error.URLError, urllib.error.HTTPError):
|
|
154
|
+
return False
|
|
155
|
+
except Exception:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Inverted dependency: the session database lives in the application layer
|
|
160
|
+
# (apps.admin_console), which this base runtime package must not import.
|
|
161
|
+
# The application layer registers its orphan-session reconciler here
|
|
162
|
+
# (apps/admin_console/database/repositories/session_repository.py does so on
|
|
163
|
+
# import); entry points that stop servers without loading the admin console
|
|
164
|
+
# perform that import as part of their assembly. Without a registration,
|
|
165
|
+
# reconciliation is skipped -- the admin console re-runs it on next startup.
|
|
166
|
+
_session_reconciler: Callable[[], int] | None = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def register_session_reconciler(reconciler: Callable[[], int]) -> None:
|
|
170
|
+
"""Register the app-layer callback that marks orphaned sessions as failed."""
|
|
171
|
+
global _session_reconciler
|
|
172
|
+
_session_reconciler = reconciler
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _reconcile_orphaned_sessions() -> int:
|
|
176
|
+
"""Immediately mark sessions whose worker PIDs died during forced stop."""
|
|
177
|
+
if _session_reconciler is None:
|
|
178
|
+
return 0
|
|
179
|
+
try:
|
|
180
|
+
return _session_reconciler()
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
logger.debug(f"Could not reconcile orphaned sessions after server stop: {exc}")
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _any_pid_alive(pids: list[int]) -> bool:
|
|
187
|
+
"""Return whether any target process is still running (zombies excluded)."""
|
|
188
|
+
return any(pid_is_alive(pid) for pid in pids)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def find_server_pids(port: int = 8000) -> list[int]:
|
|
192
|
+
"""Find all process IDs associated with the Artemis server listening on `port`.
|
|
193
|
+
|
|
194
|
+
Uses a multi-tiered discovery strategy:
|
|
195
|
+
1. Check persisted server metadata file.
|
|
196
|
+
2. Check port listeners via lsof (macOS/Linux), fuser (Linux), or netstat (Windows).
|
|
197
|
+
3. Fallback to psutil net_connections and process table scan.
|
|
198
|
+
"""
|
|
199
|
+
discovered: set[int] = set()
|
|
200
|
+
|
|
201
|
+
# 1. Check metadata file
|
|
202
|
+
info = read_server_info()
|
|
203
|
+
if info and info.get("port") == port:
|
|
204
|
+
saved_pid = info.get("pid")
|
|
205
|
+
if saved_pid and isinstance(saved_pid, int):
|
|
206
|
+
if pid_is_alive(saved_pid):
|
|
207
|
+
discovered.add(saved_pid)
|
|
208
|
+
|
|
209
|
+
# 2. Check port listeners with platform-native tools
|
|
210
|
+
# 2a. lsof (macOS & Linux)
|
|
211
|
+
if shutil.which("lsof"):
|
|
212
|
+
try:
|
|
213
|
+
res = subprocess.run(
|
|
214
|
+
["lsof", "-ti", f":{port}", "-sTCP:LISTEN"],
|
|
215
|
+
stdin=subprocess.DEVNULL,
|
|
216
|
+
capture_output=True,
|
|
217
|
+
text=True,
|
|
218
|
+
timeout=2.0,
|
|
219
|
+
)
|
|
220
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
221
|
+
for token in res.stdout.strip().splitlines():
|
|
222
|
+
token = token.strip()
|
|
223
|
+
if token.isdigit():
|
|
224
|
+
discovered.add(int(token))
|
|
225
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
# 2b. fuser (Linux fallback)
|
|
229
|
+
if not discovered and shutil.which("fuser"):
|
|
230
|
+
try:
|
|
231
|
+
res = subprocess.run(
|
|
232
|
+
["fuser", f"{port}/tcp"],
|
|
233
|
+
stdin=subprocess.DEVNULL,
|
|
234
|
+
capture_output=True,
|
|
235
|
+
text=True,
|
|
236
|
+
timeout=2.0,
|
|
237
|
+
)
|
|
238
|
+
for token in res.stdout.strip().split():
|
|
239
|
+
token = token.strip()
|
|
240
|
+
if token.isdigit():
|
|
241
|
+
discovered.add(int(token))
|
|
242
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
# 2c. netstat (Windows fallback)
|
|
246
|
+
if not discovered and sys.platform == "win32":
|
|
247
|
+
try:
|
|
248
|
+
res = subprocess.run(
|
|
249
|
+
["netstat", "-ano"],
|
|
250
|
+
stdin=subprocess.DEVNULL,
|
|
251
|
+
capture_output=True,
|
|
252
|
+
text=True,
|
|
253
|
+
timeout=3.0,
|
|
254
|
+
)
|
|
255
|
+
if res.returncode == 0:
|
|
256
|
+
for line in res.stdout.splitlines():
|
|
257
|
+
if f":{port}" in line and "LISTENING" in line.upper():
|
|
258
|
+
parts = line.strip().split()
|
|
259
|
+
if parts and parts[-1].isdigit():
|
|
260
|
+
discovered.add(int(parts[-1]))
|
|
261
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# 2d. psutil net_connections fallback
|
|
265
|
+
if not discovered:
|
|
266
|
+
try:
|
|
267
|
+
import psutil
|
|
268
|
+
|
|
269
|
+
for conn in psutil.net_connections(kind="inet"):
|
|
270
|
+
if conn.laddr and conn.laddr.port == port:
|
|
271
|
+
if conn.pid:
|
|
272
|
+
discovered.add(conn.pid)
|
|
273
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
274
|
+
# psutil is optional and net_connections() fails in platform-specific ways.
|
|
275
|
+
logger.debug(f"psutil net_connections port scan skipped: {exc}", exc_info=True)
|
|
276
|
+
|
|
277
|
+
# 3. Process table scan if still nothing found on port but port is in use
|
|
278
|
+
if not discovered and is_port_in_use(port):
|
|
279
|
+
try:
|
|
280
|
+
import psutil
|
|
281
|
+
|
|
282
|
+
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
283
|
+
try:
|
|
284
|
+
cmdline = " ".join(proc.info.get("cmdline") or [])
|
|
285
|
+
if "apps.admin_console.server" in cmdline or (
|
|
286
|
+
"artemis" in cmdline and "ui" in cmdline
|
|
287
|
+
):
|
|
288
|
+
if proc.info.get("pid"):
|
|
289
|
+
discovered.add(proc.info["pid"])
|
|
290
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
291
|
+
continue
|
|
292
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
293
|
+
# psutil is optional and process_iter() fails in platform-specific ways.
|
|
294
|
+
logger.debug(f"psutil process table scan skipped: {exc}", exc_info=True)
|
|
295
|
+
|
|
296
|
+
return sorted(discovered)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_server_status(port: int = 8000) -> dict[str, Any]:
|
|
300
|
+
"""Retrieve detailed status of the Artemis server on given port."""
|
|
301
|
+
in_use = is_port_in_use(port)
|
|
302
|
+
pids = find_server_pids(port)
|
|
303
|
+
info = read_server_info()
|
|
304
|
+
|
|
305
|
+
uptime_seconds: float | None = None
|
|
306
|
+
if info and "started_at" in info and isinstance(info["started_at"], (int, float)):
|
|
307
|
+
uptime_seconds = max(0.0, time.time() - info["started_at"])
|
|
308
|
+
|
|
309
|
+
is_running = in_use or bool(pids)
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"running": is_running,
|
|
313
|
+
"port": port,
|
|
314
|
+
"pids": pids,
|
|
315
|
+
"active_pid": pids[0] if pids else (info.get("pid") if info else None),
|
|
316
|
+
"uptime_seconds": uptime_seconds,
|
|
317
|
+
"url": f"http://localhost:{port}" if is_running else None,
|
|
318
|
+
"admin_url": f"http://localhost:{port}/admin" if is_running else None,
|
|
319
|
+
"metadata": info,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def stop_server(
|
|
324
|
+
port: int = 8000,
|
|
325
|
+
timeout: float = 12.0,
|
|
326
|
+
force: bool = False,
|
|
327
|
+
) -> tuple[bool, str, list[int]]:
|
|
328
|
+
"""Gracefully terminate any running Artemis server on `port`.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
port: TCP port to inspect and terminate.
|
|
332
|
+
timeout: Maximum seconds to wait for the server shutdown lifecycle.
|
|
333
|
+
force: If True, skip the shutdown API and terminate the process tree.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
tuple (success, message, affected_pids)
|
|
337
|
+
"""
|
|
338
|
+
pids = find_server_pids(port)
|
|
339
|
+
|
|
340
|
+
if not pids and not is_port_in_use(port):
|
|
341
|
+
clear_server_info(port=port)
|
|
342
|
+
DeviceExecutionLock.cleanup_stale_locks()
|
|
343
|
+
return True, f"No active server detected on port {port}.", []
|
|
344
|
+
|
|
345
|
+
# If no PID was discovered but port is still in use, try one more aggressive discovery
|
|
346
|
+
if not pids and is_port_in_use(port):
|
|
347
|
+
time.sleep(0.2)
|
|
348
|
+
pids = find_server_pids(port)
|
|
349
|
+
|
|
350
|
+
stopped_pids: list[int] = []
|
|
351
|
+
current_pid = os.getpid()
|
|
352
|
+
|
|
353
|
+
# A normal stop is an application lifecycle request, not an OS signal.
|
|
354
|
+
# This matters on Windows where taskkill /F and os.kill(..., SIGTERM) both
|
|
355
|
+
# bypass FastAPI's shutdown hook. Give Uvicorn enough time to cancel task
|
|
356
|
+
# workers, persist terminal session state, and release IPC/device resources.
|
|
357
|
+
graceful_requested = False
|
|
358
|
+
if not force:
|
|
359
|
+
graceful_requested = request_graceful_shutdown(
|
|
360
|
+
port,
|
|
361
|
+
timeout=min(2.0, max(0.25, timeout)),
|
|
362
|
+
)
|
|
363
|
+
if graceful_requested:
|
|
364
|
+
deadline = time.time() + max(0.25, timeout)
|
|
365
|
+
while time.time() < deadline:
|
|
366
|
+
if not is_port_in_use(port) and not _any_pid_alive(pids):
|
|
367
|
+
break
|
|
368
|
+
time.sleep(0.1)
|
|
369
|
+
|
|
370
|
+
if not is_port_in_use(port) and not _any_pid_alive(pids):
|
|
371
|
+
cleaned_locks = DeviceExecutionLock.cleanup_stale_locks()
|
|
372
|
+
clear_server_info(port=port)
|
|
373
|
+
pid_str = f" (PID: {', '.join(map(str, pids))})" if pids else ""
|
|
374
|
+
msg = f"Artemis server stopped gracefully{pid_str}. Port {port} is released."
|
|
375
|
+
if cleaned_locks:
|
|
376
|
+
msg += f" Cleaned up {cleaned_locks} device lock(s)."
|
|
377
|
+
return True, msg, pids
|
|
378
|
+
|
|
379
|
+
logger.warning(
|
|
380
|
+
f"Artemis server on port {port} did not exit within {timeout}s; "
|
|
381
|
+
"falling back to process-tree termination."
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
for pid in pids:
|
|
385
|
+
if pid == current_pid:
|
|
386
|
+
# Don't terminate self during stop_server call
|
|
387
|
+
continue
|
|
388
|
+
try:
|
|
389
|
+
ProcessSupervisor.terminate_tree(
|
|
390
|
+
pid,
|
|
391
|
+
timeout_seconds=0.5 if force or graceful_requested else timeout,
|
|
392
|
+
)
|
|
393
|
+
stopped_pids.append(pid)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.warning(f"Failed to terminate process {pid}: {e}")
|
|
396
|
+
|
|
397
|
+
# Wait for the port to actually become free
|
|
398
|
+
deadline = time.time() + max(1.5, timeout)
|
|
399
|
+
while time.time() < deadline:
|
|
400
|
+
if not is_port_in_use(port):
|
|
401
|
+
break
|
|
402
|
+
time.sleep(0.15)
|
|
403
|
+
|
|
404
|
+
# Force kill if port is still occupied
|
|
405
|
+
if is_port_in_use(port) and not force:
|
|
406
|
+
remaining_pids = find_server_pids(port)
|
|
407
|
+
for pid in remaining_pids:
|
|
408
|
+
if pid != current_pid:
|
|
409
|
+
try:
|
|
410
|
+
ProcessSupervisor.terminate_tree(pid, timeout_seconds=0.5)
|
|
411
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
412
|
+
# Force-kill is best effort; the port check below reports the outcome.
|
|
413
|
+
logger.debug(f"Force kill of pid {pid} failed: {exc}", exc_info=True)
|
|
414
|
+
time.sleep(0.3)
|
|
415
|
+
|
|
416
|
+
# Cleanup stale device execution locks left by terminated server
|
|
417
|
+
cleaned_locks = DeviceExecutionLock.cleanup_stale_locks()
|
|
418
|
+
reconciled_sessions = _reconcile_orphaned_sessions()
|
|
419
|
+
clear_server_info(port=port)
|
|
420
|
+
|
|
421
|
+
port_free = not is_port_in_use(port)
|
|
422
|
+
if port_free:
|
|
423
|
+
pid_str = f" (PID: {', '.join(map(str, stopped_pids))})" if stopped_pids else ""
|
|
424
|
+
msg = f"Artemis server stopped{pid_str}. Port {port} is released."
|
|
425
|
+
if cleaned_locks:
|
|
426
|
+
msg += f" Cleaned up {cleaned_locks} device lock(s)."
|
|
427
|
+
if reconciled_sessions:
|
|
428
|
+
msg += f" Reconciled {reconciled_sessions} interrupted session(s)."
|
|
429
|
+
return True, msg, stopped_pids
|
|
430
|
+
else:
|
|
431
|
+
return (
|
|
432
|
+
False,
|
|
433
|
+
f"Port {port} remains occupied after stop attempt. Some background process may require manual termination.",
|
|
434
|
+
stopped_pids,
|
|
435
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
"""Cross-platform Process Supervisor and Lifecycle Manager."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from artemis.platform import platform
|
|
23
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProcessSupervisor:
|
|
29
|
+
"""Manages process spawning, tree cleanup, signals, and UTF-8 stream handling."""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def prepare_environment(extra_env: dict[str, str] | None = None) -> dict[str, str]:
|
|
33
|
+
"""Prepare child process environment with guaranteed UTF-8 mode."""
|
|
34
|
+
env = os.environ.copy()
|
|
35
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
36
|
+
env["PYTHONUTF8"] = "1"
|
|
37
|
+
if extra_env:
|
|
38
|
+
env.update(extra_env)
|
|
39
|
+
return env
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
async def spawn_async(
|
|
43
|
+
cmd: Sequence[str],
|
|
44
|
+
cwd: Path | str | None = None,
|
|
45
|
+
env: dict[str, str] | None = None,
|
|
46
|
+
capture_output: bool = True,
|
|
47
|
+
) -> asyncio.subprocess.Process:
|
|
48
|
+
"""Spawn an asynchronous subprocess with clean environment and cross-platform flags."""
|
|
49
|
+
prepared_env = ProcessSupervisor.prepare_environment(env)
|
|
50
|
+
cwd_str = str(cwd) if cwd else None
|
|
51
|
+
|
|
52
|
+
stdout_dest = asyncio.subprocess.PIPE if capture_output else None
|
|
53
|
+
stderr_dest = asyncio.subprocess.PIPE if capture_output else None
|
|
54
|
+
|
|
55
|
+
return await asyncio.create_subprocess_exec(
|
|
56
|
+
*cmd,
|
|
57
|
+
stdout=stdout_dest,
|
|
58
|
+
stderr=stderr_dest,
|
|
59
|
+
cwd=cwd_str,
|
|
60
|
+
env=prepared_env,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def terminate_tree(pid: int, timeout_seconds: float = 3.0) -> bool:
|
|
65
|
+
"""Recursively terminate a process tree."""
|
|
66
|
+
return platform.process.terminate_process_tree(pid, timeout_seconds=timeout_seconds)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def terminate_tree_verified(
|
|
70
|
+
pid: int,
|
|
71
|
+
process_created_at: float,
|
|
72
|
+
timeout_seconds: float = 3.0,
|
|
73
|
+
) -> bool:
|
|
74
|
+
"""Terminate a process tree only if PID and creation time still match.
|
|
75
|
+
|
|
76
|
+
Cross-entry task control discovers the worker through a shared lease.
|
|
77
|
+
Verifying creation time prevents a stale lease from terminating an
|
|
78
|
+
unrelated process after the operating system reuses its PID.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
import psutil
|
|
82
|
+
except Exception:
|
|
83
|
+
return False
|
|
84
|
+
try:
|
|
85
|
+
process = psutil.Process(pid)
|
|
86
|
+
if process_created_at > 0 and abs(process.create_time() - process_created_at) >= 1.0:
|
|
87
|
+
return False
|
|
88
|
+
except psutil.NoSuchProcess:
|
|
89
|
+
return True
|
|
90
|
+
except Exception:
|
|
91
|
+
return False
|
|
92
|
+
return ProcessSupervisor.terminate_tree(pid, timeout_seconds=timeout_seconds)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
async def stop_process(
|
|
96
|
+
proc: asyncio.subprocess.Process | None,
|
|
97
|
+
timeout_seconds: float = 3.0,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Safely stop an asyncio Process and kill its children if necessary."""
|
|
100
|
+
if proc is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if proc.returncode is not None:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if proc.pid:
|
|
108
|
+
ProcessSupervisor.terminate_tree(proc.pid, timeout_seconds=timeout_seconds)
|
|
109
|
+
else:
|
|
110
|
+
proc.terminate()
|
|
111
|
+
await asyncio.wait_for(proc.wait(), timeout=timeout_seconds)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.debug(f"Failed graceful stop for process {proc}: {e}")
|
|
114
|
+
try:
|
|
115
|
+
proc.kill()
|
|
116
|
+
except (ProcessLookupError, OSError) as kill_exc:
|
|
117
|
+
# ProcessLookupError: already exited, nothing to do.
|
|
118
|
+
logger.debug(f"Failed to kill process {proc}: {kill_exc}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
process_supervisor: ProcessSupervisor = ProcessSupervisor()
|