auto-coder 1.0.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- auto_coder-2.0.1.dist-info/LICENSE +158 -0
- auto_coder-2.0.1.dist-info/METADATA +558 -0
- auto_coder-2.0.1.dist-info/RECORD +795 -0
- {auto_coder-1.0.0.dist-info → auto_coder-2.0.1.dist-info}/WHEEL +1 -1
- {auto_coder-1.0.0.dist-info → auto_coder-2.0.1.dist-info}/entry_points.txt +3 -3
- autocoder/__init__.py +31 -0
- autocoder/agent/auto_filegroup.py +32 -13
- autocoder/agent/auto_learn_from_commit.py +9 -1
- autocoder/agent/base_agentic/__init__.py +3 -0
- autocoder/agent/base_agentic/agent_hub.py +1 -1
- autocoder/agent/base_agentic/base_agent.py +235 -136
- autocoder/agent/base_agentic/default_tools.py +119 -118
- autocoder/agent/base_agentic/test_base_agent.py +1 -1
- autocoder/agent/base_agentic/tool_registry.py +32 -20
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +24 -3
- autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +24 -11
- autocoder/agent/base_agentic/types.py +42 -0
- autocoder/agent/entry_command_agent/chat.py +77 -73
- autocoder/auto_coder.py +31 -40
- autocoder/auto_coder_rag.py +11 -1084
- autocoder/auto_coder_runner.py +962 -2345
- autocoder/auto_coder_terminal.py +26 -0
- autocoder/auto_coder_terminal_v3.py +190 -0
- autocoder/chat/conf_command.py +224 -124
- autocoder/chat/models_command.py +361 -299
- autocoder/chat/rules_command.py +79 -31
- autocoder/chat_auto_coder.py +988 -398
- autocoder/chat_auto_coder_lang.py +23 -732
- autocoder/commands/auto_command.py +25 -8
- autocoder/commands/auto_web.py +1 -1
- autocoder/commands/tools.py +44 -44
- autocoder/common/__init__.py +150 -128
- autocoder/common/ac_style_command_parser/__init__.py +39 -2
- autocoder/common/ac_style_command_parser/config.py +422 -0
- autocoder/common/ac_style_command_parser/parser.py +292 -78
- autocoder/common/ac_style_command_parser/test_parser.py +241 -16
- autocoder/common/ac_style_command_parser/test_typed_parser.py +342 -0
- autocoder/common/ac_style_command_parser/typed_parser.py +653 -0
- autocoder/common/action_yml_file_manager.py +25 -13
- autocoder/common/agent_events/__init__.py +52 -0
- autocoder/common/agent_events/agent_event_emitter.py +193 -0
- autocoder/common/agent_events/event_factory.py +177 -0
- autocoder/common/agent_events/examples.py +307 -0
- autocoder/common/agent_events/types.py +113 -0
- autocoder/common/agent_events/utils.py +68 -0
- autocoder/common/agent_hooks/__init__.py +44 -0
- autocoder/common/agent_hooks/examples.py +582 -0
- autocoder/common/agent_hooks/hook_executor.py +217 -0
- autocoder/common/agent_hooks/hook_manager.py +288 -0
- autocoder/common/agent_hooks/types.py +133 -0
- autocoder/common/agent_hooks/utils.py +99 -0
- autocoder/common/agent_query_queue/queue_executor.py +324 -0
- autocoder/common/agent_query_queue/queue_manager.py +325 -0
- autocoder/common/agents/__init__.py +11 -0
- autocoder/common/agents/agent_manager.py +323 -0
- autocoder/common/agents/agent_parser.py +189 -0
- autocoder/common/agents/example_usage.py +344 -0
- autocoder/common/agents/integration_example.py +330 -0
- autocoder/common/agents/test_agent_parser.py +545 -0
- autocoder/common/async_utils.py +101 -0
- autocoder/common/auto_coder_lang.py +23 -972
- autocoder/common/autocoderargs_parser/__init__.py +14 -0
- autocoder/common/autocoderargs_parser/parser.py +184 -0
- autocoder/common/autocoderargs_parser/tests/__init__.py +1 -0
- autocoder/common/autocoderargs_parser/tests/test_args_parser.py +235 -0
- autocoder/common/autocoderargs_parser/tests/test_token_parser.py +195 -0
- autocoder/common/autocoderargs_parser/token_parser.py +290 -0
- autocoder/common/buildin_tokenizer.py +2 -4
- autocoder/common/code_auto_generate.py +149 -74
- autocoder/common/code_auto_generate_diff.py +163 -70
- autocoder/common/code_auto_generate_editblock.py +179 -89
- autocoder/common/code_auto_generate_strict_diff.py +167 -72
- autocoder/common/code_auto_merge_editblock.py +13 -6
- autocoder/common/code_modification_ranker.py +1 -1
- autocoder/common/command_completer.py +3 -3
- autocoder/common/command_file_manager/manager.py +183 -47
- autocoder/common/command_file_manager/test_command_file_manager.py +507 -0
- autocoder/common/command_templates.py +1 -1
- autocoder/common/conf_utils.py +2 -4
- autocoder/common/conversations/config.py +11 -3
- autocoder/common/conversations/get_conversation_manager.py +100 -2
- autocoder/common/conversations/llm_stats_models.py +264 -0
- autocoder/common/conversations/manager.py +112 -28
- autocoder/common/conversations/models.py +16 -2
- autocoder/common/conversations/storage/index_manager.py +134 -10
- autocoder/common/core_config/__init__.py +63 -0
- autocoder/common/core_config/agentic_mode_manager.py +109 -0
- autocoder/common/core_config/base_manager.py +123 -0
- autocoder/common/core_config/compatibility.py +151 -0
- autocoder/common/core_config/config_manager.py +156 -0
- autocoder/common/core_config/conversation_manager.py +31 -0
- autocoder/common/core_config/exclude_manager.py +72 -0
- autocoder/common/core_config/file_manager.py +177 -0
- autocoder/common/core_config/human_as_model_manager.py +129 -0
- autocoder/common/core_config/lib_manager.py +54 -0
- autocoder/common/core_config/main_manager.py +81 -0
- autocoder/common/core_config/mode_manager.py +126 -0
- autocoder/common/core_config/models.py +70 -0
- autocoder/common/core_config/test_memory_manager.py +1056 -0
- autocoder/common/env_manager.py +282 -0
- autocoder/common/env_manager_usage_example.py +211 -0
- autocoder/common/file_checkpoint/conversation_checkpoint.py +19 -19
- autocoder/common/file_checkpoint/manager.py +264 -48
- autocoder/common/file_checkpoint/test_backup.py +1 -18
- autocoder/common/file_checkpoint/test_manager.py +270 -1
- autocoder/common/file_checkpoint/test_store.py +1 -17
- autocoder/common/file_handler/__init__.py +23 -0
- autocoder/common/file_handler/active_context_handler.py +159 -0
- autocoder/common/file_handler/add_files_handler.py +409 -0
- autocoder/common/file_handler/chat_handler.py +180 -0
- autocoder/common/file_handler/coding_handler.py +409 -0
- autocoder/common/file_handler/commit_handler.py +200 -0
- autocoder/common/file_handler/lib_handler.py +156 -0
- autocoder/common/file_handler/list_files_handler.py +111 -0
- autocoder/common/file_handler/mcp_handler.py +268 -0
- autocoder/common/file_handler/models_handler.py +493 -0
- autocoder/common/file_handler/remove_files_handler.py +172 -0
- autocoder/common/git_utils.py +44 -8
- autocoder/common/global_cancel.py +15 -6
- autocoder/common/ignorefiles/test_ignore_file_utils.py +1 -1
- autocoder/common/international/__init__.py +31 -0
- autocoder/common/international/demo_international.py +92 -0
- autocoder/common/international/message_manager.py +157 -0
- autocoder/common/international/messages/__init__.py +56 -0
- autocoder/common/international/messages/async_command_messages.py +507 -0
- autocoder/common/international/messages/auto_coder_messages.py +2208 -0
- autocoder/common/international/messages/chat_auto_coder_messages.py +1547 -0
- autocoder/common/international/messages/command_help_messages.py +986 -0
- autocoder/common/international/messages/conversation_command_messages.py +191 -0
- autocoder/common/international/messages/git_helper_plugin_messages.py +159 -0
- autocoder/common/international/messages/queue_command_messages.py +751 -0
- autocoder/common/international/messages/rules_command_messages.py +77 -0
- autocoder/common/international/messages/sdk_messages.py +1707 -0
- autocoder/common/international/messages/token_helper_plugin_messages.py +361 -0
- autocoder/common/international/messages/tool_display_messages.py +1212 -0
- autocoder/common/international/messages/workflow_exception_messages.py +473 -0
- autocoder/common/international/test_international.py +612 -0
- autocoder/common/linter_core/__init__.py +28 -0
- autocoder/common/linter_core/base_linter.py +61 -0
- autocoder/common/linter_core/config_loader.py +271 -0
- autocoder/common/linter_core/formatters/__init__.py +0 -0
- autocoder/common/linter_core/formatters/base_formatter.py +38 -0
- autocoder/common/linter_core/formatters/raw_formatter.py +17 -0
- autocoder/common/linter_core/linter.py +166 -0
- autocoder/common/linter_core/linter_factory.py +216 -0
- autocoder/common/linter_core/linter_manager.py +333 -0
- autocoder/common/linter_core/linters/__init__.py +9 -0
- autocoder/common/linter_core/linters/java_linter.py +342 -0
- autocoder/common/linter_core/linters/python_linter.py +115 -0
- autocoder/common/linter_core/linters/typescript_linter.py +119 -0
- autocoder/common/linter_core/models/__init__.py +7 -0
- autocoder/common/linter_core/models/lint_result.py +91 -0
- autocoder/common/linter_core/models.py +33 -0
- autocoder/common/linter_core/tests/__init__.py +3 -0
- autocoder/common/linter_core/tests/test_config_loader.py +323 -0
- autocoder/common/linter_core/tests/test_config_loading.py +308 -0
- autocoder/common/linter_core/tests/test_factory_manager.py +234 -0
- autocoder/common/linter_core/tests/test_formatters.py +147 -0
- autocoder/common/linter_core/tests/test_integration.py +317 -0
- autocoder/common/linter_core/tests/test_java_linter.py +496 -0
- autocoder/common/linter_core/tests/test_linters.py +265 -0
- autocoder/common/linter_core/tests/test_models.py +81 -0
- autocoder/common/linter_core/tests/verify_config_loading.py +296 -0
- autocoder/common/linter_core/tests/verify_fixes.py +183 -0
- autocoder/common/llm_friendly_package/__init__.py +31 -0
- autocoder/common/llm_friendly_package/base_manager.py +102 -0
- autocoder/common/llm_friendly_package/docs_manager.py +121 -0
- autocoder/common/llm_friendly_package/library_manager.py +171 -0
- autocoder/common/{llm_friendly_package.py → llm_friendly_package/main_manager.py} +204 -231
- autocoder/common/llm_friendly_package/models.py +40 -0
- autocoder/common/llm_friendly_package/test_llm_friendly_package.py +536 -0
- autocoder/common/llms/__init__.py +15 -0
- autocoder/common/llms/demo_error_handling.py +85 -0
- autocoder/common/llms/factory.py +142 -0
- autocoder/common/llms/manager.py +264 -0
- autocoder/common/llms/pricing.py +121 -0
- autocoder/common/llms/registry.py +316 -0
- autocoder/common/llms/schema.py +77 -0
- autocoder/common/llms/simple_demo.py +45 -0
- autocoder/common/llms/test_quick_model.py +116 -0
- autocoder/common/llms/test_remove_functionality.py +182 -0
- autocoder/common/llms/tests/__init__.py +1 -0
- autocoder/common/llms/tests/test_manager.py +330 -0
- autocoder/common/llms/tests/test_registry.py +364 -0
- autocoder/common/mcp_tools/__init__.py +62 -0
- autocoder/common/{mcp_tools.py → mcp_tools/executor.py} +49 -40
- autocoder/common/{mcp_hub.py → mcp_tools/hub.py} +42 -68
- autocoder/common/{mcp_server_install.py → mcp_tools/installer.py} +16 -28
- autocoder/common/{mcp_server.py → mcp_tools/server.py} +176 -48
- autocoder/common/mcp_tools/test_keyboard_interrupt.py +93 -0
- autocoder/common/mcp_tools/test_mcp_tools.py +391 -0
- autocoder/common/{mcp_server_types.py → mcp_tools/types.py} +121 -48
- autocoder/common/mcp_tools/verify_functionality.py +202 -0
- autocoder/common/model_speed_tester.py +32 -26
- autocoder/common/priority_directory_finder/__init__.py +142 -0
- autocoder/common/priority_directory_finder/examples.py +230 -0
- autocoder/common/priority_directory_finder/finder.py +283 -0
- autocoder/common/priority_directory_finder/models.py +236 -0
- autocoder/common/priority_directory_finder/test_priority_directory_finder.py +431 -0
- autocoder/common/project_scanner/__init__.py +18 -0
- autocoder/common/project_scanner/compat.py +77 -0
- autocoder/common/project_scanner/scanner.py +436 -0
- autocoder/common/project_tracker/__init__.py +27 -0
- autocoder/common/project_tracker/api.py +228 -0
- autocoder/common/project_tracker/demo.py +272 -0
- autocoder/common/project_tracker/tracker.py +487 -0
- autocoder/common/project_tracker/types.py +53 -0
- autocoder/common/pruner/__init__.py +67 -0
- autocoder/common/pruner/agentic_conversation_pruner.py +651 -102
- autocoder/common/pruner/conversation_message_ids_api.py +386 -0
- autocoder/common/pruner/conversation_message_ids_manager.py +347 -0
- autocoder/common/pruner/conversation_message_ids_pruner.py +473 -0
- autocoder/common/pruner/conversation_normalizer.py +347 -0
- autocoder/common/pruner/conversation_pruner.py +26 -6
- autocoder/common/pruner/test_agentic_conversation_pruner.py +554 -112
- autocoder/common/pruner/test_conversation_normalizer.py +502 -0
- autocoder/common/pruner/test_tool_content_detector.py +324 -0
- autocoder/common/pruner/tool_content_detector.py +227 -0
- autocoder/common/pruner/tools/__init__.py +18 -0
- autocoder/common/pruner/tools/query_message_ids.py +264 -0
- autocoder/common/pruner/tools/test_agentic_pruning_logic.py +432 -0
- autocoder/common/pruner/tools/test_message_ids_pruning_only.py +192 -0
- autocoder/common/pull_requests/__init__.py +9 -1
- autocoder/common/pull_requests/utils.py +122 -1
- autocoder/common/rag_manager/rag_manager.py +36 -40
- autocoder/common/rulefiles/__init__.py +53 -1
- autocoder/common/rulefiles/api.py +250 -0
- autocoder/common/rulefiles/core/__init__.py +14 -0
- autocoder/common/rulefiles/core/manager.py +241 -0
- autocoder/common/rulefiles/core/selector.py +805 -0
- autocoder/common/rulefiles/models/__init__.py +20 -0
- autocoder/common/rulefiles/models/index.py +16 -0
- autocoder/common/rulefiles/models/init_rule.py +18 -0
- autocoder/common/rulefiles/models/rule_file.py +18 -0
- autocoder/common/rulefiles/models/rule_relevance.py +14 -0
- autocoder/common/rulefiles/models/summary.py +16 -0
- autocoder/common/rulefiles/test_rulefiles.py +776 -0
- autocoder/common/rulefiles/utils/__init__.py +34 -0
- autocoder/common/rulefiles/utils/monitor.py +86 -0
- autocoder/common/rulefiles/utils/parser.py +230 -0
- autocoder/common/save_formatted_log.py +67 -10
- autocoder/common/search_replace.py +8 -1
- autocoder/common/search_replace_patch/__init__.py +24 -0
- autocoder/common/search_replace_patch/base.py +115 -0
- autocoder/common/search_replace_patch/manager.py +248 -0
- autocoder/common/search_replace_patch/patch_replacer.py +304 -0
- autocoder/common/search_replace_patch/similarity_replacer.py +306 -0
- autocoder/common/search_replace_patch/string_replacer.py +181 -0
- autocoder/common/search_replace_patch/tests/__init__.py +3 -0
- autocoder/common/search_replace_patch/tests/run_tests.py +126 -0
- autocoder/common/search_replace_patch/tests/test_base.py +188 -0
- autocoder/common/search_replace_patch/tests/test_empty_line_insert.py +233 -0
- autocoder/common/search_replace_patch/tests/test_integration.py +389 -0
- autocoder/common/search_replace_patch/tests/test_manager.py +351 -0
- autocoder/common/search_replace_patch/tests/test_patch_replacer.py +316 -0
- autocoder/common/search_replace_patch/tests/test_regex_replacer.py +306 -0
- autocoder/common/search_replace_patch/tests/test_similarity_replacer.py +384 -0
- autocoder/common/shell_commands/__init__.py +197 -0
- autocoder/common/shell_commands/background_process_notifier.py +346 -0
- autocoder/common/shell_commands/command_executor.py +1127 -0
- autocoder/common/shell_commands/error_recovery.py +541 -0
- autocoder/common/shell_commands/exceptions.py +120 -0
- autocoder/common/shell_commands/interactive_executor.py +476 -0
- autocoder/common/shell_commands/interactive_pexpect_process.py +623 -0
- autocoder/common/shell_commands/interactive_process.py +744 -0
- autocoder/common/shell_commands/interactive_session_manager.py +1014 -0
- autocoder/common/shell_commands/monitoring.py +529 -0
- autocoder/common/shell_commands/process_cleanup.py +386 -0
- autocoder/common/shell_commands/process_manager.py +606 -0
- autocoder/common/shell_commands/test_interactive_pexpect_process.py +281 -0
- autocoder/common/shell_commands/tests/__init__.py +6 -0
- autocoder/common/shell_commands/tests/conftest.py +118 -0
- autocoder/common/shell_commands/tests/test_background_process_notifier.py +703 -0
- autocoder/common/shell_commands/tests/test_command_executor.py +448 -0
- autocoder/common/shell_commands/tests/test_error_recovery.py +305 -0
- autocoder/common/shell_commands/tests/test_exceptions.py +299 -0
- autocoder/common/shell_commands/tests/test_execute_batch.py +588 -0
- autocoder/common/shell_commands/tests/test_indented_batch_commands.py +244 -0
- autocoder/common/shell_commands/tests/test_integration.py +664 -0
- autocoder/common/shell_commands/tests/test_monitoring.py +546 -0
- autocoder/common/shell_commands/tests/test_performance.py +632 -0
- autocoder/common/shell_commands/tests/test_process_cleanup.py +397 -0
- autocoder/common/shell_commands/tests/test_process_manager.py +606 -0
- autocoder/common/shell_commands/tests/test_timeout_config.py +343 -0
- autocoder/common/shell_commands/tests/test_timeout_manager.py +520 -0
- autocoder/common/shell_commands/timeout_config.py +315 -0
- autocoder/common/shell_commands/timeout_manager.py +352 -0
- autocoder/common/terminal_paste/__init__.py +14 -0
- autocoder/common/terminal_paste/demo.py +145 -0
- autocoder/common/terminal_paste/demo_paste_functionality.py +95 -0
- autocoder/common/terminal_paste/paste_handler.py +200 -0
- autocoder/common/terminal_paste/paste_manager.py +118 -0
- autocoder/common/terminal_paste/tests/__init__.py +1 -0
- autocoder/common/terminal_paste/tests/test_paste_handler.py +182 -0
- autocoder/common/terminal_paste/tests/test_paste_manager.py +126 -0
- autocoder/common/terminal_paste/utils.py +163 -0
- autocoder/common/test_autocoder_args.py +232 -0
- autocoder/common/test_env_manager.py +173 -0
- autocoder/common/test_env_manager_integration.py +159 -0
- autocoder/common/text_similarity/__init__.py +9 -0
- autocoder/common/text_similarity/demo.py +216 -0
- autocoder/common/text_similarity/examples.py +266 -0
- autocoder/common/text_similarity/test_text_similarity.py +306 -0
- autocoder/common/text_similarity/text_similarity.py +194 -0
- autocoder/common/text_similarity/utils.py +125 -0
- autocoder/common/todos/__init__.py +61 -0
- autocoder/common/todos/cache/__init__.py +16 -0
- autocoder/common/todos/cache/base_cache.py +89 -0
- autocoder/common/todos/cache/cache_manager.py +228 -0
- autocoder/common/todos/cache/memory_cache.py +225 -0
- autocoder/common/todos/config.py +155 -0
- autocoder/common/todos/exceptions.py +35 -0
- autocoder/common/todos/get_todo_manager.py +161 -0
- autocoder/common/todos/manager.py +537 -0
- autocoder/common/todos/models.py +239 -0
- autocoder/common/todos/storage/__init__.py +14 -0
- autocoder/common/todos/storage/base_storage.py +76 -0
- autocoder/common/todos/storage/file_storage.py +278 -0
- autocoder/common/tokens/counter.py +24 -2
- autocoder/common/tools_manager/__init__.py +17 -0
- autocoder/common/tools_manager/examples.py +162 -0
- autocoder/common/tools_manager/manager.py +385 -0
- autocoder/common/tools_manager/models.py +39 -0
- autocoder/common/tools_manager/test_tools_manager.py +303 -0
- autocoder/common/tools_manager/utils.py +191 -0
- autocoder/common/v2/agent/agentic_callbacks.py +270 -0
- autocoder/common/v2/agent/agentic_edit.py +2699 -1856
- autocoder/common/v2/agent/agentic_edit_change_manager.py +474 -0
- autocoder/common/v2/agent/agentic_edit_tools/__init__.py +35 -1
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_list_tool_resolver.py +279 -0
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_write_tool_resolver.py +10 -1
- autocoder/common/v2/agent/agentic_edit_tools/background_task_tool_resolver.py +1167 -0
- autocoder/common/v2/agent/agentic_edit_tools/base_tool_resolver.py +2 -2
- autocoder/common/v2/agent/agentic_edit_tools/conversation_message_ids_read_tool_resolver.py +214 -0
- autocoder/common/v2/agent/agentic_edit_tools/conversation_message_ids_write_tool_resolver.py +299 -0
- autocoder/common/v2/agent/agentic_edit_tools/count_tokens_tool_resolver.py +290 -0
- autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +564 -29
- autocoder/common/v2/agent/agentic_edit_tools/execute_workflow_tool_resolver.py +485 -0
- autocoder/common/v2/agent/agentic_edit_tools/extract_to_text_tool_resolver.py +225 -0
- autocoder/common/v2/agent/agentic_edit_tools/lint_report.py +79 -0
- autocoder/common/v2/agent/agentic_edit_tools/linter_config_models.py +343 -0
- autocoder/common/v2/agent/agentic_edit_tools/linter_enabled_tool_resolver.py +189 -0
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +169 -101
- autocoder/common/v2/agent/agentic_edit_tools/load_extra_document_tool_resolver.py +356 -0
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +243 -50
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +667 -147
- autocoder/common/v2/agent/agentic_edit_tools/run_named_subagents_tool_resolver.py +691 -0
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +410 -86
- autocoder/common/v2/agent/agentic_edit_tools/session_interactive_tool_resolver.py +115 -0
- autocoder/common/v2/agent/agentic_edit_tools/session_start_tool_resolver.py +190 -0
- autocoder/common/v2/agent/agentic_edit_tools/session_stop_tool_resolver.py +76 -0
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +207 -192
- autocoder/common/v2/agent/agentic_edit_tools/todo_read_tool_resolver.py +80 -63
- autocoder/common/v2/agent/agentic_edit_tools/todo_write_tool_resolver.py +237 -233
- autocoder/common/v2/agent/agentic_edit_tools/use_mcp_tool_resolver.py +2 -2
- autocoder/common/v2/agent/agentic_edit_tools/web_crawl_tool_resolver.py +557 -0
- autocoder/common/v2/agent/agentic_edit_tools/web_search_tool_resolver.py +600 -0
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +56 -121
- autocoder/common/v2/agent/agentic_edit_types.py +343 -9
- autocoder/common/v2/agent/runner/__init__.py +3 -3
- autocoder/common/v2/agent/runner/base_runner.py +12 -26
- autocoder/common/v2/agent/runner/{event_runner.py → file_based_event_runner.py} +3 -2
- autocoder/common/v2/agent/runner/sdk_runner.py +150 -8
- autocoder/common/v2/agent/runner/terminal_runner.py +170 -57
- autocoder/common/v2/agent/runner/tool_display.py +557 -159
- autocoder/common/v2/agent/test_agentic_callbacks.py +265 -0
- autocoder/common/v2/agent/test_agentic_edit.py +194 -0
- autocoder/common/v2/agent/tool_caller/__init__.py +24 -0
- autocoder/common/v2/agent/tool_caller/default_tool_resolver_map.py +135 -0
- autocoder/common/v2/agent/tool_caller/integration_test.py +172 -0
- autocoder/common/v2/agent/tool_caller/plugins/__init__.py +14 -0
- autocoder/common/v2/agent/tool_caller/plugins/base_plugin.py +126 -0
- autocoder/common/v2/agent/tool_caller/plugins/examples/__init__.py +13 -0
- autocoder/common/v2/agent/tool_caller/plugins/examples/logging_plugin.py +164 -0
- autocoder/common/v2/agent/tool_caller/plugins/examples/security_filter_plugin.py +198 -0
- autocoder/common/v2/agent/tool_caller/plugins/plugin_interface.py +141 -0
- autocoder/common/v2/agent/tool_caller/test_tool_caller.py +278 -0
- autocoder/common/v2/agent/tool_caller/tool_call_plugin_manager.py +331 -0
- autocoder/common/v2/agent/tool_caller/tool_caller.py +337 -0
- autocoder/common/v2/agent/tool_caller/usage_example.py +193 -0
- autocoder/common/v2/code_agentic_editblock_manager.py +4 -4
- autocoder/common/v2/code_auto_generate.py +136 -78
- autocoder/common/v2/code_auto_generate_diff.py +135 -79
- autocoder/common/v2/code_auto_generate_editblock.py +174 -99
- autocoder/common/v2/code_auto_generate_strict_diff.py +151 -71
- autocoder/common/v2/code_auto_merge.py +1 -1
- autocoder/common/v2/code_auto_merge_editblock.py +13 -1
- autocoder/common/v2/code_diff_manager.py +3 -3
- autocoder/common/v2/code_editblock_manager.py +4 -14
- autocoder/common/v2/code_manager.py +1 -1
- autocoder/common/v2/code_strict_diff_manager.py +2 -2
- autocoder/common/wrap_llm_hint/__init__.py +10 -0
- autocoder/common/wrap_llm_hint/test_wrap_llm_hint.py +1067 -0
- autocoder/common/wrap_llm_hint/utils.py +432 -0
- autocoder/common/wrap_llm_hint/wrap_llm_hint.py +323 -0
- autocoder/completer/__init__.py +8 -0
- autocoder/completer/command_completer_v2.py +1094 -0
- autocoder/default_project/__init__.py +501 -0
- autocoder/dispacher/__init__.py +4 -12
- autocoder/dispacher/actions/action.py +400 -129
- autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
- autocoder/index/entry.py +117 -125
- autocoder/{agent → index/filter}/agentic_filter.py +322 -333
- autocoder/index/filter/normal_filter.py +5 -11
- autocoder/index/filter/quick_filter.py +1 -1
- autocoder/index/index.py +36 -9
- autocoder/index/tests/__init__.py +1 -0
- autocoder/index/tests/run_tests.py +195 -0
- autocoder/index/tests/test_entry.py +303 -0
- autocoder/index/tests/test_index_manager.py +314 -0
- autocoder/index/tests/test_module_integration.py +300 -0
- autocoder/index/tests/test_symbols_utils.py +183 -0
- autocoder/inner/__init__.py +4 -0
- autocoder/inner/agentic.py +923 -0
- autocoder/inner/async_command_handler.py +992 -0
- autocoder/inner/conversation_command_handlers.py +623 -0
- autocoder/inner/merge_command_handler.py +213 -0
- autocoder/inner/queue_command_handler.py +684 -0
- autocoder/models.py +95 -266
- autocoder/plugins/git_helper_plugin.py +31 -29
- autocoder/plugins/token_helper_plugin.py +65 -46
- autocoder/pyproject/__init__.py +32 -29
- autocoder/rag/agentic_rag.py +215 -75
- autocoder/rag/cache/simple_cache.py +1 -2
- autocoder/rag/loaders/image_loader.py +1 -1
- autocoder/rag/long_context_rag.py +42 -26
- autocoder/rag/qa_conversation_strategy.py +1 -1
- autocoder/rag/terminal/__init__.py +17 -0
- autocoder/rag/terminal/args.py +581 -0
- autocoder/rag/terminal/bootstrap.py +61 -0
- autocoder/rag/terminal/command_handlers.py +653 -0
- autocoder/rag/terminal/formatters/__init__.py +20 -0
- autocoder/rag/terminal/formatters/base.py +70 -0
- autocoder/rag/terminal/formatters/json_format.py +66 -0
- autocoder/rag/terminal/formatters/stream_json.py +95 -0
- autocoder/rag/terminal/formatters/text.py +28 -0
- autocoder/rag/terminal/init.py +120 -0
- autocoder/rag/terminal/utils.py +106 -0
- autocoder/rag/test_agentic_rag.py +389 -0
- autocoder/rag/test_doc_filter.py +3 -3
- autocoder/rag/test_long_context_rag.py +1 -1
- autocoder/rag/test_token_limiter.py +517 -10
- autocoder/rag/token_counter.py +3 -0
- autocoder/rag/token_limiter.py +19 -15
- autocoder/rag/tools/__init__.py +26 -2
- autocoder/rag/tools/bochaai_example.py +343 -0
- autocoder/rag/tools/bochaai_sdk.py +541 -0
- autocoder/rag/tools/metaso_example.py +268 -0
- autocoder/rag/tools/metaso_sdk.py +417 -0
- autocoder/rag/tools/recall_tool.py +28 -7
- autocoder/rag/tools/run_integration_tests.py +204 -0
- autocoder/rag/tools/test_all_providers.py +318 -0
- autocoder/rag/tools/test_bochaai_integration.py +482 -0
- autocoder/rag/tools/test_final_integration.py +215 -0
- autocoder/rag/tools/test_metaso_integration.py +424 -0
- autocoder/rag/tools/test_metaso_real.py +171 -0
- autocoder/rag/tools/test_web_crawl_tool.py +639 -0
- autocoder/rag/tools/test_web_search_tool.py +509 -0
- autocoder/rag/tools/todo_read_tool.py +202 -0
- autocoder/rag/tools/todo_write_tool.py +412 -0
- autocoder/rag/tools/web_crawl_tool.py +634 -0
- autocoder/rag/tools/web_search_tool.py +558 -0
- autocoder/rag/tools/web_tools_example.py +119 -0
- autocoder/rag/types.py +16 -0
- autocoder/rag/variable_holder.py +4 -2
- autocoder/rags.py +86 -79
- autocoder/regexproject/__init__.py +23 -21
- autocoder/sdk/__init__.py +46 -190
- autocoder/sdk/api.py +370 -0
- autocoder/sdk/async_runner/__init__.py +26 -0
- autocoder/sdk/async_runner/async_executor.py +650 -0
- autocoder/sdk/async_runner/async_handler.py +356 -0
- autocoder/sdk/async_runner/markdown_processor.py +595 -0
- autocoder/sdk/async_runner/task_metadata.py +284 -0
- autocoder/sdk/async_runner/worktree_manager.py +438 -0
- autocoder/sdk/cli/__init__.py +2 -5
- autocoder/sdk/cli/formatters.py +28 -204
- autocoder/sdk/cli/handlers.py +77 -44
- autocoder/sdk/cli/main.py +154 -171
- autocoder/sdk/cli/options.py +95 -22
- autocoder/sdk/constants.py +139 -51
- autocoder/sdk/core/auto_coder_core.py +484 -109
- autocoder/sdk/core/bridge.py +297 -115
- autocoder/sdk/exceptions.py +18 -12
- autocoder/sdk/formatters/__init__.py +19 -0
- autocoder/sdk/formatters/input.py +64 -0
- autocoder/sdk/formatters/output.py +247 -0
- autocoder/sdk/formatters/stream.py +54 -0
- autocoder/sdk/models/__init__.py +6 -5
- autocoder/sdk/models/options.py +55 -18
- autocoder/sdk/utils/formatters.py +27 -195
- autocoder/suffixproject/__init__.py +28 -25
- autocoder/terminal/__init__.py +14 -0
- autocoder/terminal/app.py +454 -0
- autocoder/terminal/args.py +32 -0
- autocoder/terminal/bootstrap.py +178 -0
- autocoder/terminal/command_processor.py +521 -0
- autocoder/terminal/command_registry.py +57 -0
- autocoder/terminal/help.py +97 -0
- autocoder/terminal/tasks/__init__.py +5 -0
- autocoder/terminal/tasks/background.py +77 -0
- autocoder/terminal/tasks/task_event.py +70 -0
- autocoder/terminal/ui/__init__.py +13 -0
- autocoder/terminal/ui/completer.py +268 -0
- autocoder/terminal/ui/keybindings.py +75 -0
- autocoder/terminal/ui/session.py +41 -0
- autocoder/terminal/ui/toolbar.py +64 -0
- autocoder/terminal/utils/__init__.py +13 -0
- autocoder/terminal/utils/errors.py +18 -0
- autocoder/terminal/utils/paths.py +19 -0
- autocoder/terminal/utils/shell.py +43 -0
- autocoder/terminal_v3/__init__.py +10 -0
- autocoder/terminal_v3/app.py +201 -0
- autocoder/terminal_v3/handlers/__init__.py +5 -0
- autocoder/terminal_v3/handlers/command_handler.py +131 -0
- autocoder/terminal_v3/models/__init__.py +6 -0
- autocoder/terminal_v3/models/conversation_buffer.py +214 -0
- autocoder/terminal_v3/models/message.py +50 -0
- autocoder/terminal_v3/models/tool_display.py +247 -0
- autocoder/terminal_v3/ui/__init__.py +7 -0
- autocoder/terminal_v3/ui/keybindings.py +56 -0
- autocoder/terminal_v3/ui/layout.py +141 -0
- autocoder/terminal_v3/ui/styles.py +43 -0
- autocoder/tsproject/__init__.py +23 -23
- autocoder/utils/auto_coder_utils/chat_stream_out.py +1 -1
- autocoder/utils/llms.py +88 -80
- autocoder/utils/math_utils.py +101 -0
- autocoder/utils/model_provider_selector.py +16 -4
- autocoder/utils/operate_config_api.py +33 -5
- autocoder/utils/thread_utils.py +2 -2
- autocoder/version.py +4 -2
- autocoder/workflow_agents/__init__.py +84 -0
- autocoder/workflow_agents/agent.py +143 -0
- autocoder/workflow_agents/exceptions.py +573 -0
- autocoder/workflow_agents/executor.py +665 -0
- autocoder/workflow_agents/loader.py +749 -0
- autocoder/workflow_agents/runner.py +267 -0
- autocoder/workflow_agents/types.py +173 -0
- autocoder/workflow_agents/utils.py +434 -0
- autocoder/workflow_agents/workflow_manager.py +211 -0
- auto_coder-1.0.0.dist-info/METADATA +0 -396
- auto_coder-1.0.0.dist-info/RECORD +0 -442
- auto_coder-1.0.0.dist-info/licenses/LICENSE +0 -201
- autocoder/auto_coder_server.py +0 -672
- autocoder/benchmark.py +0 -138
- autocoder/common/ac_style_command_parser/example.py +0 -7
- autocoder/common/cleaner.py +0 -31
- autocoder/common/command_completer_v2.py +0 -615
- autocoder/common/context_pruner.py +0 -477
- autocoder/common/conversation_pruner.py +0 -132
- autocoder/common/directory_cache/__init__.py +0 -1
- autocoder/common/directory_cache/cache.py +0 -192
- autocoder/common/directory_cache/test_cache.py +0 -190
- autocoder/common/file_checkpoint/examples.py +0 -217
- autocoder/common/llm_friendly_package_example.py +0 -138
- autocoder/common/llm_friendly_package_test.py +0 -63
- autocoder/common/pull_requests/test_module.py +0 -1
- autocoder/common/rulefiles/autocoderrules_utils.py +0 -484
- autocoder/common/text.py +0 -30
- autocoder/common/v2/agent/agentic_edit_tools/list_package_info_tool_resolver.py +0 -42
- autocoder/common/v2/agent/agentic_edit_tools/test_execute_command_tool_resolver.py +0 -70
- autocoder/common/v2/agent/agentic_edit_tools/test_search_files_tool_resolver.py +0 -163
- autocoder/common/v2/agent/agentic_tool_display.py +0 -183
- autocoder/plugins/dynamic_completion_example.py +0 -148
- autocoder/plugins/sample_plugin.py +0 -160
- autocoder/sdk/cli/__main__.py +0 -26
- autocoder/sdk/cli/completion_wrapper.py +0 -38
- autocoder/sdk/cli/install_completion.py +0 -301
- autocoder/sdk/models/messages.py +0 -209
- autocoder/sdk/session/__init__.py +0 -32
- autocoder/sdk/session/session.py +0 -106
- autocoder/sdk/session/session_manager.py +0 -56
- {auto_coder-1.0.0.dist-info → auto_coder-2.0.1.dist-info}/top_level.txt +0 -0
- /autocoder/{sdk/example.py → common/agent_query_queue/__init__.py} +0 -0
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command executor module for shell command execution with timeout support.
|
|
3
|
+
|
|
4
|
+
This module provides the main interface for executing shell commands with
|
|
5
|
+
comprehensive timeout control, process cleanup, and monitoring capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import select
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import threading
|
|
15
|
+
from typing import Optional, Dict, Tuple, Any, Generator, Union, List
|
|
16
|
+
from loguru import logger as log
|
|
17
|
+
|
|
18
|
+
from .timeout_config import TimeoutConfig
|
|
19
|
+
from .process_manager import ProcessManager, _command_to_string
|
|
20
|
+
from .monitoring import CommandExecutionLogger, PerformanceMonitor, get_logger, get_global_monitor
|
|
21
|
+
from .error_recovery import ErrorRecoveryManager, create_default_error_recovery_manager
|
|
22
|
+
from .exceptions import CommandExecutionError, CommandTimeoutError
|
|
23
|
+
from .timeout_manager import create_timeout_context
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import pexpect
|
|
27
|
+
PEXPECT_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
log.warning("pexpect not available, interactive command support will be limited")
|
|
30
|
+
PEXPECT_AVAILABLE = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CommandExecutor:
|
|
34
|
+
"""
|
|
35
|
+
Main command executor with timeout support and process management.
|
|
36
|
+
|
|
37
|
+
This class provides a comprehensive interface for executing shell commands
|
|
38
|
+
with timeout control, process cleanup, monitoring, and error recovery.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
config: Timeout configuration
|
|
42
|
+
process_manager: Process manager instance
|
|
43
|
+
logger: Command execution logger
|
|
44
|
+
monitor: Performance monitor
|
|
45
|
+
error_recovery: Error recovery manager
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
config: Optional[TimeoutConfig] = None,
|
|
51
|
+
logger: Optional[CommandExecutionLogger] = None,
|
|
52
|
+
monitor: Optional[PerformanceMonitor] = None,
|
|
53
|
+
error_recovery: Optional[ErrorRecoveryManager] = None,
|
|
54
|
+
verbose: bool = False
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Initialize command executor.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config: Timeout configuration (uses default if None)
|
|
61
|
+
logger: Command execution logger (uses global if None)
|
|
62
|
+
monitor: Performance monitor (uses global if None)
|
|
63
|
+
error_recovery: Error recovery manager (uses default if None)
|
|
64
|
+
verbose: Whether to enable verbose logging
|
|
65
|
+
"""
|
|
66
|
+
self.config = config or TimeoutConfig()
|
|
67
|
+
self.process_manager = ProcessManager(self.config)
|
|
68
|
+
self.logger = logger or get_logger(verbose)
|
|
69
|
+
self.monitor = monitor or get_global_monitor()
|
|
70
|
+
self.error_recovery = error_recovery or create_default_error_recovery_manager()
|
|
71
|
+
self.verbose = verbose
|
|
72
|
+
|
|
73
|
+
log.debug(f"CommandExecutor initialized (verbose={verbose})")
|
|
74
|
+
|
|
75
|
+
def execute(
|
|
76
|
+
self,
|
|
77
|
+
command: Union[str, List[str]],
|
|
78
|
+
timeout: Optional[float] = None,
|
|
79
|
+
cwd: Optional[str] = None,
|
|
80
|
+
env: Optional[Dict[str, str]] = None,
|
|
81
|
+
capture_output: bool = True,
|
|
82
|
+
text: bool = True,
|
|
83
|
+
encoding: str = 'utf-8',
|
|
84
|
+
shell: Optional[bool] = None,
|
|
85
|
+
**kwargs
|
|
86
|
+
) -> Tuple[int, str]:
|
|
87
|
+
"""
|
|
88
|
+
Execute a command with timeout support.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
command: Command to execute
|
|
92
|
+
timeout: Timeout in seconds (uses config default if None)
|
|
93
|
+
cwd: Working directory
|
|
94
|
+
env: Environment variables
|
|
95
|
+
capture_output: Whether to capture stdout/stderr
|
|
96
|
+
text: Whether to use text mode
|
|
97
|
+
encoding: Text encoding
|
|
98
|
+
shell: Whether to use shell
|
|
99
|
+
**kwargs: Additional arguments for subprocess.Popen
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Tuple of (exit_code, output)
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
CommandTimeoutError: If command times out
|
|
106
|
+
CommandExecutionError: If command execution fails
|
|
107
|
+
"""
|
|
108
|
+
# Convert command to string for timeout config and logging
|
|
109
|
+
command_str = _command_to_string(command)
|
|
110
|
+
|
|
111
|
+
# Determine timeout
|
|
112
|
+
if timeout is None:
|
|
113
|
+
timeout = self.config.get_timeout_for_command(command_str)
|
|
114
|
+
|
|
115
|
+
# Start logging
|
|
116
|
+
metrics = self.logger.log_command_start(command_str, timeout, cwd=cwd)
|
|
117
|
+
|
|
118
|
+
# Auto-determine shell parameter if not specified
|
|
119
|
+
if shell is None:
|
|
120
|
+
shell = isinstance(command, str) # True for strings, False for lists
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Execute command with subprocess
|
|
124
|
+
exit_code, output = self._execute_with_subprocess(
|
|
125
|
+
command, timeout, cwd, env, capture_output, text, encoding, shell, **kwargs
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Log completion
|
|
129
|
+
self.logger.log_command_complete(metrics, exit_code, output)
|
|
130
|
+
|
|
131
|
+
# Record metrics
|
|
132
|
+
self.monitor.record_execution(metrics)
|
|
133
|
+
|
|
134
|
+
return exit_code, output
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
# Log error
|
|
138
|
+
self.logger.log_command_complete(metrics, -1, "", e)
|
|
139
|
+
|
|
140
|
+
# Record failed metrics
|
|
141
|
+
self.monitor.record_execution(metrics)
|
|
142
|
+
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
def _execute_with_subprocess(
|
|
146
|
+
self,
|
|
147
|
+
command: Union[str, List[str]],
|
|
148
|
+
timeout: Optional[float],
|
|
149
|
+
cwd: Optional[str],
|
|
150
|
+
env: Optional[Dict[str, str]],
|
|
151
|
+
capture_output: bool,
|
|
152
|
+
text: bool,
|
|
153
|
+
encoding: str,
|
|
154
|
+
shell: bool,
|
|
155
|
+
**kwargs
|
|
156
|
+
) -> Tuple[int, str]:
|
|
157
|
+
"""
|
|
158
|
+
Execute command using subprocess with timeout support.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
command: Command to execute
|
|
162
|
+
timeout: Timeout in seconds
|
|
163
|
+
cwd: Working directory
|
|
164
|
+
env: Environment variables
|
|
165
|
+
capture_output: Whether to capture output
|
|
166
|
+
text: Whether to use text mode
|
|
167
|
+
encoding: Text encoding
|
|
168
|
+
shell: Whether to use shell
|
|
169
|
+
**kwargs: Additional subprocess arguments
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Tuple of (exit_code, output)
|
|
173
|
+
"""
|
|
174
|
+
# Create process
|
|
175
|
+
process = self.process_manager.create_process(
|
|
176
|
+
command=command,
|
|
177
|
+
timeout=timeout,
|
|
178
|
+
cwd=cwd,
|
|
179
|
+
env=env,
|
|
180
|
+
shell=shell,
|
|
181
|
+
capture_output=capture_output,
|
|
182
|
+
text=text,
|
|
183
|
+
encoding=encoding,
|
|
184
|
+
**kwargs
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Convert command to string for logging
|
|
188
|
+
command_str = _command_to_string(command)
|
|
189
|
+
metrics = self.logger.log_command_start(command_str, timeout, process.pid, cwd)
|
|
190
|
+
|
|
191
|
+
# Prepare timeout callback for TimeoutContext
|
|
192
|
+
timeout_occurred = [False] # Use list to allow modification in nested function
|
|
193
|
+
partial_output = [""]
|
|
194
|
+
|
|
195
|
+
def timeout_callback(timed_out_process, timeout_val):
|
|
196
|
+
"""Handle timeout event with proper logging and cleanup."""
|
|
197
|
+
timeout_occurred[0] = True
|
|
198
|
+
timeout_val = timeout_val if timeout_val is not None else 0.0
|
|
199
|
+
|
|
200
|
+
# Log timeout
|
|
201
|
+
self.logger.log_command_timeout(metrics, timeout_val, timed_out_process.pid)
|
|
202
|
+
log.warning(f"Command '{command_str}' timed out after {timeout_val} seconds (PID: {timed_out_process.pid})")
|
|
203
|
+
|
|
204
|
+
# Try to get partial output before cleanup
|
|
205
|
+
try:
|
|
206
|
+
stdout, stderr = timed_out_process.communicate(timeout=2)
|
|
207
|
+
partial_output[0] = stdout or ""
|
|
208
|
+
except:
|
|
209
|
+
partial_output[0] = ""
|
|
210
|
+
|
|
211
|
+
# The cleanup will be handled by TimeoutContext's cleanup_process_tree
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
# Use TimeoutContext for advanced timeout management
|
|
215
|
+
if timeout and timeout > 0:
|
|
216
|
+
with create_timeout_context(process, timeout, callback=timeout_callback):
|
|
217
|
+
stdout, stderr = process.communicate() # No timeout here - managed by TimeoutContext
|
|
218
|
+
|
|
219
|
+
# Check if timeout occurred during execution
|
|
220
|
+
if timeout_occurred[0]:
|
|
221
|
+
# Use partial output if timeout occurred
|
|
222
|
+
output_str = partial_output[0]
|
|
223
|
+
if self.verbose and output_str:
|
|
224
|
+
print(output_str, end="", flush=True)
|
|
225
|
+
raise CommandTimeoutError(command_str, timeout, process.pid)
|
|
226
|
+
|
|
227
|
+
output_str = stdout or ""
|
|
228
|
+
exit_code = process.returncode
|
|
229
|
+
else:
|
|
230
|
+
# No timeout specified, execute normally
|
|
231
|
+
stdout, stderr = process.communicate()
|
|
232
|
+
output_str = stdout or ""
|
|
233
|
+
exit_code = process.returncode
|
|
234
|
+
|
|
235
|
+
# Handle verbose output
|
|
236
|
+
if self.verbose and output_str:
|
|
237
|
+
print(output_str, end="", flush=True)
|
|
238
|
+
|
|
239
|
+
if self.verbose:
|
|
240
|
+
log.info(f"Command completed with exit code {exit_code}")
|
|
241
|
+
|
|
242
|
+
return exit_code, output_str
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
# Ensure process is cleaned up
|
|
246
|
+
try:
|
|
247
|
+
self.process_manager.cleanup_process_tree(process)
|
|
248
|
+
except Exception as cleanup_error:
|
|
249
|
+
log.error(f"Error during cleanup: {cleanup_error}")
|
|
250
|
+
|
|
251
|
+
raise e
|
|
252
|
+
|
|
253
|
+
def execute_generator(
|
|
254
|
+
self,
|
|
255
|
+
command: Union[str, List[str]],
|
|
256
|
+
timeout: Optional[float] = None,
|
|
257
|
+
cwd: Optional[str] = None,
|
|
258
|
+
env: Optional[Dict[str, str]] = None,
|
|
259
|
+
encoding: str = 'utf-8',
|
|
260
|
+
**kwargs
|
|
261
|
+
) -> Generator[str, None, int]:
|
|
262
|
+
"""
|
|
263
|
+
Execute command and yield output in real-time.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
command: Command to execute
|
|
267
|
+
timeout: Timeout in seconds
|
|
268
|
+
cwd: Working directory
|
|
269
|
+
env: Environment variables
|
|
270
|
+
encoding: Text encoding
|
|
271
|
+
**kwargs: Additional subprocess arguments
|
|
272
|
+
|
|
273
|
+
Yields:
|
|
274
|
+
Command output strings
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Final exit code
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
CommandTimeoutError: If command times out
|
|
281
|
+
CommandExecutionError: If command execution fails
|
|
282
|
+
"""
|
|
283
|
+
# Convert command to string for timeout config and logging
|
|
284
|
+
command_str = _command_to_string(command)
|
|
285
|
+
|
|
286
|
+
# Determine timeout
|
|
287
|
+
if timeout is None:
|
|
288
|
+
timeout = self.config.get_timeout_for_command(command_str)
|
|
289
|
+
|
|
290
|
+
# Start logging
|
|
291
|
+
metrics = self.logger.log_command_start(command_str, timeout, cwd=cwd)
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
# Create process
|
|
295
|
+
process = self.process_manager.create_process(
|
|
296
|
+
command=command,
|
|
297
|
+
timeout=timeout,
|
|
298
|
+
cwd=cwd,
|
|
299
|
+
env=env,
|
|
300
|
+
shell=True,
|
|
301
|
+
capture_output=True,
|
|
302
|
+
text=True,
|
|
303
|
+
encoding=encoding,
|
|
304
|
+
**kwargs
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
metrics.pid = process.pid
|
|
308
|
+
|
|
309
|
+
# Stream output
|
|
310
|
+
output_length = 0
|
|
311
|
+
|
|
312
|
+
while True:
|
|
313
|
+
# Check if process has finished
|
|
314
|
+
if process.poll() is not None:
|
|
315
|
+
# Read any remaining output
|
|
316
|
+
if process.stdout:
|
|
317
|
+
remaining = process.stdout.read()
|
|
318
|
+
if remaining:
|
|
319
|
+
output_length += len(remaining)
|
|
320
|
+
yield remaining
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
# Read output chunk
|
|
324
|
+
if process.stdout:
|
|
325
|
+
if platform.system() != "Windows":
|
|
326
|
+
ready, _, _ = select.select([process.stdout], [], [], 0.1)
|
|
327
|
+
if ready:
|
|
328
|
+
chunk = process.stdout.read(1024)
|
|
329
|
+
if chunk:
|
|
330
|
+
output_length += len(chunk)
|
|
331
|
+
yield chunk
|
|
332
|
+
else:
|
|
333
|
+
try:
|
|
334
|
+
chunk = process.stdout.read(1024)
|
|
335
|
+
if chunk:
|
|
336
|
+
output_length += len(chunk)
|
|
337
|
+
yield chunk
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
|
|
341
|
+
time.sleep(0.01)
|
|
342
|
+
|
|
343
|
+
# Wait for process completion with TimeoutContext
|
|
344
|
+
timeout_occurred = [False]
|
|
345
|
+
|
|
346
|
+
def generator_timeout_callback(timed_out_process, timeout_val):
|
|
347
|
+
"""Handle timeout for generator execution."""
|
|
348
|
+
timeout_occurred[0] = True
|
|
349
|
+
timeout_val = timeout_val if timeout_val is not None else 0.0
|
|
350
|
+
self.logger.log_command_timeout(metrics, timeout_val, timed_out_process.pid)
|
|
351
|
+
log.warning(f"Generator command '{command_str}' timed out after {timeout_val} seconds (PID: {timed_out_process.pid})")
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
if timeout and timeout > 0:
|
|
355
|
+
with create_timeout_context(process, timeout, callback=generator_timeout_callback):
|
|
356
|
+
exit_code = self.process_manager.wait_for_process(process, None) # No timeout in wait - managed by TimeoutContext
|
|
357
|
+
|
|
358
|
+
if timeout_occurred[0]:
|
|
359
|
+
raise CommandTimeoutError(command_str, timeout, process.pid)
|
|
360
|
+
else:
|
|
361
|
+
exit_code = self.process_manager.wait_for_process(process, None)
|
|
362
|
+
except subprocess.TimeoutExpired:
|
|
363
|
+
# This shouldn't happen with TimeoutContext, but keep as fallback
|
|
364
|
+
timeout_val = timeout if timeout is not None else 0.0
|
|
365
|
+
raise CommandTimeoutError(command_str, timeout_val, process.pid)
|
|
366
|
+
|
|
367
|
+
# Log completion
|
|
368
|
+
metrics.output_length = output_length
|
|
369
|
+
self.logger.log_command_complete(metrics, exit_code)
|
|
370
|
+
self.monitor.record_execution(metrics)
|
|
371
|
+
|
|
372
|
+
return exit_code
|
|
373
|
+
|
|
374
|
+
except Exception as e:
|
|
375
|
+
# Log error
|
|
376
|
+
self.logger.log_command_complete(metrics, -1, "", e)
|
|
377
|
+
self.monitor.record_execution(metrics)
|
|
378
|
+
|
|
379
|
+
raise
|
|
380
|
+
|
|
381
|
+
def execute_background(
|
|
382
|
+
self,
|
|
383
|
+
command: Union[str, List[str]],
|
|
384
|
+
cwd: Optional[str] = None,
|
|
385
|
+
env: Optional[Dict[str, str]] = None,
|
|
386
|
+
shell: bool = True,
|
|
387
|
+
**kwargs
|
|
388
|
+
) -> Dict[str, Any]:
|
|
389
|
+
"""
|
|
390
|
+
Execute a command in the background and return immediately with process info.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
command: Command to execute
|
|
394
|
+
cwd: Working directory
|
|
395
|
+
env: Environment variables
|
|
396
|
+
shell: Whether to use shell
|
|
397
|
+
**kwargs: Additional arguments for subprocess.Popen
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Dictionary containing process information:
|
|
401
|
+
{
|
|
402
|
+
"pid": int,
|
|
403
|
+
"command": str,
|
|
404
|
+
"working_directory": str,
|
|
405
|
+
"start_time": float,
|
|
406
|
+
"status": "running"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
CommandExecutionError: If command execution fails
|
|
411
|
+
"""
|
|
412
|
+
# Convert command to string for logging
|
|
413
|
+
command_str = _command_to_string(command)
|
|
414
|
+
|
|
415
|
+
# Start logging
|
|
416
|
+
metrics = self.logger.log_command_start(command_str, None, cwd=cwd)
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
# Create background process
|
|
420
|
+
process = self.process_manager.create_background_process(
|
|
421
|
+
command=command,
|
|
422
|
+
cwd=cwd,
|
|
423
|
+
env=env,
|
|
424
|
+
shell=shell,
|
|
425
|
+
**kwargs
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Update metrics with PID
|
|
429
|
+
metrics.pid = process.pid
|
|
430
|
+
|
|
431
|
+
# Note: We don't log completion here since the process is still running
|
|
432
|
+
# Completion will be logged when the process actually finishes
|
|
433
|
+
|
|
434
|
+
# Get process info
|
|
435
|
+
process_info = self.process_manager.get_background_process_info(process.pid)
|
|
436
|
+
|
|
437
|
+
if self.verbose:
|
|
438
|
+
log.info(f"Started background command: {command_str} (PID: {process.pid})")
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
"pid": process.pid,
|
|
442
|
+
"command": command_str,
|
|
443
|
+
"working_directory": cwd or os.getcwd(),
|
|
444
|
+
"start_time": process_info["start_time"] if process_info else time.time(),
|
|
445
|
+
"status": "running"
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
# Log error
|
|
450
|
+
self.logger.log_command_complete(metrics, -1, "", e)
|
|
451
|
+
self.monitor.record_execution(metrics)
|
|
452
|
+
|
|
453
|
+
raise CommandExecutionError(f"Failed to start background command: {str(e)}")
|
|
454
|
+
|
|
455
|
+
def get_background_processes(self) -> Dict[int, Dict[str, Any]]:
|
|
456
|
+
"""
|
|
457
|
+
Get information about all background processes.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Dictionary mapping PID to process information
|
|
461
|
+
"""
|
|
462
|
+
return self.process_manager.get_background_processes()
|
|
463
|
+
|
|
464
|
+
def get_background_process_info(self, pid: int) -> Optional[Dict[str, Any]]:
|
|
465
|
+
"""
|
|
466
|
+
Get information about a specific background process.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
pid: Process ID
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Process information or None if not found
|
|
473
|
+
"""
|
|
474
|
+
return self.process_manager.get_background_process_info(pid)
|
|
475
|
+
|
|
476
|
+
def cleanup_background_process(self, pid: int, timeout: Optional[float] = None) -> bool:
|
|
477
|
+
"""
|
|
478
|
+
Clean up a specific background process.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
pid: Process ID to cleanup
|
|
482
|
+
timeout: Timeout for cleanup
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
True if cleanup successful
|
|
486
|
+
"""
|
|
487
|
+
return self.process_manager.cleanup_background_process(pid, timeout)
|
|
488
|
+
|
|
489
|
+
def get_status(self) -> Dict[str, Any]:
|
|
490
|
+
"""
|
|
491
|
+
Get executor status information.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Dictionary with status information
|
|
495
|
+
"""
|
|
496
|
+
return {
|
|
497
|
+
'config': self.config.to_dict(),
|
|
498
|
+
'active_processes': len(self.process_manager.get_all_processes()),
|
|
499
|
+
'background_processes': self.process_manager.get_background_process_count(),
|
|
500
|
+
'process_groups': len(self.process_manager.get_process_groups()),
|
|
501
|
+
'performance_summary': self.monitor.get_performance_summary(),
|
|
502
|
+
'recent_alerts': self.monitor.get_alerts(5),
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
def execute_batch(
|
|
506
|
+
self,
|
|
507
|
+
commands: List[Union[str, List[str]]],
|
|
508
|
+
timeout: Optional[float] = None,
|
|
509
|
+
per_command_timeout: Optional[float] = None,
|
|
510
|
+
parallel: bool = True,
|
|
511
|
+
cwd: Optional[str] = None,
|
|
512
|
+
env: Optional[Dict[str, str]] = None,
|
|
513
|
+
capture_output: bool = True,
|
|
514
|
+
text: bool = True,
|
|
515
|
+
encoding: str = 'utf-8',
|
|
516
|
+
shell: Optional[bool] = None,
|
|
517
|
+
**kwargs
|
|
518
|
+
) -> List[Dict[str, Any]]:
|
|
519
|
+
"""
|
|
520
|
+
Execute multiple commands in batch, either in parallel or serial.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
commands: List of commands to execute
|
|
524
|
+
timeout: Overall timeout for all commands (seconds)
|
|
525
|
+
per_command_timeout: Timeout for each individual command (seconds)
|
|
526
|
+
parallel: Whether to execute commands in parallel (True) or serial (False)
|
|
527
|
+
cwd: Working directory
|
|
528
|
+
env: Environment variables
|
|
529
|
+
capture_output: Whether to capture stdout/stderr
|
|
530
|
+
text: Whether to use text mode
|
|
531
|
+
encoding: Text encoding
|
|
532
|
+
shell: Whether to use shell
|
|
533
|
+
**kwargs: Additional arguments for subprocess.Popen
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
List of dictionaries containing results for each command:
|
|
537
|
+
[
|
|
538
|
+
{
|
|
539
|
+
"command": str,
|
|
540
|
+
"index": int,
|
|
541
|
+
"exit_code": int,
|
|
542
|
+
"output": str,
|
|
543
|
+
"error": str or None,
|
|
544
|
+
"timed_out": bool,
|
|
545
|
+
"duration": float,
|
|
546
|
+
"start_time": float,
|
|
547
|
+
"end_time": float
|
|
548
|
+
},
|
|
549
|
+
...
|
|
550
|
+
]
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
CommandExecutionError: If batch execution setup fails
|
|
554
|
+
CommandTimeoutError: If overall timeout is exceeded
|
|
555
|
+
"""
|
|
556
|
+
if not commands:
|
|
557
|
+
return []
|
|
558
|
+
|
|
559
|
+
# Log batch start
|
|
560
|
+
batch_start_time = time.time()
|
|
561
|
+
log.info(f"Starting batch execution of {len(commands)} commands (parallel={parallel})")
|
|
562
|
+
|
|
563
|
+
# Results list with proper initialization
|
|
564
|
+
results: List[Optional[Dict[str, Any]]] = [None] * len(commands)
|
|
565
|
+
|
|
566
|
+
# Determine per-command timeout
|
|
567
|
+
if per_command_timeout is None:
|
|
568
|
+
# Use config default or overall timeout divided by command count
|
|
569
|
+
if timeout:
|
|
570
|
+
per_command_timeout = timeout / len(commands) if parallel else timeout
|
|
571
|
+
else:
|
|
572
|
+
per_command_timeout = None # Will use config default for each command
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
if parallel:
|
|
576
|
+
results = self._execute_batch_parallel(
|
|
577
|
+
commands, timeout, per_command_timeout, cwd, env,
|
|
578
|
+
capture_output, text, encoding, shell, **kwargs
|
|
579
|
+
)
|
|
580
|
+
else:
|
|
581
|
+
results = self._execute_batch_serial(
|
|
582
|
+
commands, timeout, per_command_timeout, cwd, env,
|
|
583
|
+
capture_output, text, encoding, shell, **kwargs
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Log batch completion
|
|
587
|
+
batch_duration = time.time() - batch_start_time
|
|
588
|
+
successful_count = sum(1 for r in results if r and r.get("exit_code") == 0)
|
|
589
|
+
failed_count = sum(1 for r in results if r and r.get("exit_code") != 0)
|
|
590
|
+
timeout_count = sum(1 for r in results if r and r.get("timed_out", False))
|
|
591
|
+
|
|
592
|
+
log.info(
|
|
593
|
+
f"Batch execution completed in {batch_duration:.2f}s: "
|
|
594
|
+
f"{successful_count} successful, {failed_count} failed, {timeout_count} timed out"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Ensure all results are non-None before returning
|
|
598
|
+
final_results = []
|
|
599
|
+
for i, r in enumerate(results):
|
|
600
|
+
if r is not None:
|
|
601
|
+
final_results.append(r)
|
|
602
|
+
else:
|
|
603
|
+
# This shouldn't happen in normal flow, but handle it
|
|
604
|
+
final_results.append({
|
|
605
|
+
"command": _command_to_string(commands[i]),
|
|
606
|
+
"index": i,
|
|
607
|
+
"exit_code": -1,
|
|
608
|
+
"output": "",
|
|
609
|
+
"error": "Command not executed",
|
|
610
|
+
"timed_out": False,
|
|
611
|
+
"duration": 0.0,
|
|
612
|
+
"start_time": batch_start_time,
|
|
613
|
+
"end_time": time.time()
|
|
614
|
+
})
|
|
615
|
+
return final_results
|
|
616
|
+
|
|
617
|
+
except Exception as e:
|
|
618
|
+
log.error(f"Batch execution failed: {e}")
|
|
619
|
+
# Return partial results if available
|
|
620
|
+
final_results = []
|
|
621
|
+
for i, r in enumerate(results):
|
|
622
|
+
if r is not None:
|
|
623
|
+
final_results.append(r)
|
|
624
|
+
else:
|
|
625
|
+
final_results.append({
|
|
626
|
+
"command": _command_to_string(commands[i]),
|
|
627
|
+
"index": i,
|
|
628
|
+
"exit_code": -1,
|
|
629
|
+
"output": "",
|
|
630
|
+
"error": str(e),
|
|
631
|
+
"timed_out": False,
|
|
632
|
+
"duration": 0.0,
|
|
633
|
+
"start_time": batch_start_time,
|
|
634
|
+
"end_time": time.time()
|
|
635
|
+
})
|
|
636
|
+
return final_results
|
|
637
|
+
|
|
638
|
+
def _execute_batch_serial(
|
|
639
|
+
self,
|
|
640
|
+
commands: List[Union[str, List[str]]],
|
|
641
|
+
timeout: Optional[float],
|
|
642
|
+
per_command_timeout: Optional[float],
|
|
643
|
+
cwd: Optional[str],
|
|
644
|
+
env: Optional[Dict[str, str]],
|
|
645
|
+
capture_output: bool,
|
|
646
|
+
text: bool,
|
|
647
|
+
encoding: str,
|
|
648
|
+
shell: bool,
|
|
649
|
+
**kwargs
|
|
650
|
+
) -> List[Dict[str, Any]]:
|
|
651
|
+
"""Execute commands serially."""
|
|
652
|
+
results = []
|
|
653
|
+
batch_start = time.time()
|
|
654
|
+
|
|
655
|
+
for i, command in enumerate(commands):
|
|
656
|
+
# Calculate remaining time if overall timeout is set
|
|
657
|
+
remaining_time = None
|
|
658
|
+
if timeout:
|
|
659
|
+
elapsed = time.time() - batch_start
|
|
660
|
+
remaining_time = timeout - elapsed
|
|
661
|
+
if remaining_time <= 0:
|
|
662
|
+
# Overall timeout exceeded
|
|
663
|
+
# Fill remaining results with timeout error
|
|
664
|
+
for j in range(i, len(commands)):
|
|
665
|
+
results.append({
|
|
666
|
+
"command": _command_to_string(commands[j]),
|
|
667
|
+
"index": j,
|
|
668
|
+
"exit_code": -1,
|
|
669
|
+
"output": "",
|
|
670
|
+
"error": "Batch timeout exceeded before execution",
|
|
671
|
+
"timed_out": True,
|
|
672
|
+
"duration": 0.0,
|
|
673
|
+
"start_time": time.time(),
|
|
674
|
+
"end_time": time.time()
|
|
675
|
+
})
|
|
676
|
+
break
|
|
677
|
+
|
|
678
|
+
# Use the smaller of per-command timeout and remaining time
|
|
679
|
+
cmd_timeout = per_command_timeout
|
|
680
|
+
if remaining_time is not None:
|
|
681
|
+
cmd_timeout = min(cmd_timeout, remaining_time) if cmd_timeout else remaining_time
|
|
682
|
+
|
|
683
|
+
# Execute command
|
|
684
|
+
start_time = time.time()
|
|
685
|
+
try:
|
|
686
|
+
exit_code, output = self.execute(
|
|
687
|
+
command, timeout=cmd_timeout, cwd=cwd, env=env,
|
|
688
|
+
capture_output=capture_output, text=text, encoding=encoding,
|
|
689
|
+
shell=shell, **kwargs
|
|
690
|
+
)
|
|
691
|
+
end_time = time.time()
|
|
692
|
+
|
|
693
|
+
results.append({
|
|
694
|
+
"command": _command_to_string(command),
|
|
695
|
+
"index": i,
|
|
696
|
+
"exit_code": exit_code,
|
|
697
|
+
"output": output,
|
|
698
|
+
"error": None,
|
|
699
|
+
"timed_out": False,
|
|
700
|
+
"duration": end_time - start_time,
|
|
701
|
+
"start_time": start_time,
|
|
702
|
+
"end_time": end_time
|
|
703
|
+
})
|
|
704
|
+
except CommandTimeoutError as e:
|
|
705
|
+
end_time = time.time()
|
|
706
|
+
results.append({
|
|
707
|
+
"command": _command_to_string(command),
|
|
708
|
+
"index": i,
|
|
709
|
+
"exit_code": -1,
|
|
710
|
+
"output": "",
|
|
711
|
+
"error": str(e),
|
|
712
|
+
"timed_out": True,
|
|
713
|
+
"duration": end_time - start_time,
|
|
714
|
+
"start_time": start_time,
|
|
715
|
+
"end_time": end_time
|
|
716
|
+
})
|
|
717
|
+
except Exception as e:
|
|
718
|
+
end_time = time.time()
|
|
719
|
+
results.append({
|
|
720
|
+
"command": _command_to_string(command),
|
|
721
|
+
"index": i,
|
|
722
|
+
"exit_code": -1,
|
|
723
|
+
"output": "",
|
|
724
|
+
"error": str(e),
|
|
725
|
+
"timed_out": False,
|
|
726
|
+
"duration": end_time - start_time,
|
|
727
|
+
"start_time": start_time,
|
|
728
|
+
"end_time": end_time
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
return results
|
|
732
|
+
|
|
733
|
+
def _execute_batch_parallel(
|
|
734
|
+
self,
|
|
735
|
+
commands: List[Union[str, List[str]]],
|
|
736
|
+
timeout: Optional[float],
|
|
737
|
+
per_command_timeout: Optional[float],
|
|
738
|
+
cwd: Optional[str],
|
|
739
|
+
env: Optional[Dict[str, str]],
|
|
740
|
+
capture_output: bool,
|
|
741
|
+
text: bool,
|
|
742
|
+
encoding: str,
|
|
743
|
+
shell: bool,
|
|
744
|
+
**kwargs
|
|
745
|
+
) -> List[Dict[str, Any]]:
|
|
746
|
+
"""Execute commands in parallel."""
|
|
747
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
748
|
+
import concurrent.futures
|
|
749
|
+
|
|
750
|
+
results: List[Optional[Dict[str, Any]]] = [None] * len(commands)
|
|
751
|
+
batch_start = time.time()
|
|
752
|
+
|
|
753
|
+
def execute_single_command(idx: int, cmd: Union[str, List[str]]) -> Tuple[int, Dict[str, Any]]:
|
|
754
|
+
"""Execute a single command and return (index, result)."""
|
|
755
|
+
start_time = time.time()
|
|
756
|
+
try:
|
|
757
|
+
exit_code, output = self.execute(
|
|
758
|
+
cmd, timeout=per_command_timeout, cwd=cwd, env=env,
|
|
759
|
+
capture_output=capture_output, text=text, encoding=encoding,
|
|
760
|
+
shell=shell, **kwargs
|
|
761
|
+
)
|
|
762
|
+
end_time = time.time()
|
|
763
|
+
|
|
764
|
+
return idx, {
|
|
765
|
+
"command": _command_to_string(cmd),
|
|
766
|
+
"index": idx,
|
|
767
|
+
"exit_code": exit_code,
|
|
768
|
+
"output": output,
|
|
769
|
+
"error": None,
|
|
770
|
+
"timed_out": False,
|
|
771
|
+
"duration": end_time - start_time,
|
|
772
|
+
"start_time": start_time,
|
|
773
|
+
"end_time": end_time
|
|
774
|
+
}
|
|
775
|
+
except CommandTimeoutError as e:
|
|
776
|
+
end_time = time.time()
|
|
777
|
+
return idx, {
|
|
778
|
+
"command": _command_to_string(cmd),
|
|
779
|
+
"index": idx,
|
|
780
|
+
"exit_code": -1,
|
|
781
|
+
"output": "",
|
|
782
|
+
"error": str(e),
|
|
783
|
+
"timed_out": True,
|
|
784
|
+
"duration": end_time - start_time,
|
|
785
|
+
"start_time": start_time,
|
|
786
|
+
"end_time": end_time
|
|
787
|
+
}
|
|
788
|
+
except Exception as e:
|
|
789
|
+
end_time = time.time()
|
|
790
|
+
return idx, {
|
|
791
|
+
"command": _command_to_string(cmd),
|
|
792
|
+
"index": idx,
|
|
793
|
+
"exit_code": -1,
|
|
794
|
+
"output": "",
|
|
795
|
+
"error": str(e),
|
|
796
|
+
"timed_out": False,
|
|
797
|
+
"duration": end_time - start_time,
|
|
798
|
+
"start_time": start_time,
|
|
799
|
+
"end_time": end_time
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
# Use ThreadPoolExecutor for parallel execution
|
|
803
|
+
max_workers = min(len(commands), os.cpu_count() or 4)
|
|
804
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
805
|
+
# Submit all commands
|
|
806
|
+
futures = {
|
|
807
|
+
executor.submit(execute_single_command, i, cmd): i
|
|
808
|
+
for i, cmd in enumerate(commands)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
# Wait for completion with overall timeout
|
|
812
|
+
try:
|
|
813
|
+
# Process completed futures as they finish
|
|
814
|
+
for future in as_completed(futures, timeout=timeout):
|
|
815
|
+
idx, result = future.result()
|
|
816
|
+
results[idx] = result
|
|
817
|
+
|
|
818
|
+
except concurrent.futures.TimeoutError:
|
|
819
|
+
# Overall timeout exceeded
|
|
820
|
+
log.warning(f"Batch execution timeout exceeded after {timeout}s")
|
|
821
|
+
|
|
822
|
+
# Cancel remaining futures
|
|
823
|
+
for future in futures:
|
|
824
|
+
if not future.done():
|
|
825
|
+
future.cancel()
|
|
826
|
+
|
|
827
|
+
# Fill in timeout results for uncompleted commands
|
|
828
|
+
for i, result in enumerate(results):
|
|
829
|
+
if result is None:
|
|
830
|
+
results[i] = {
|
|
831
|
+
"command": _command_to_string(commands[i]),
|
|
832
|
+
"index": i,
|
|
833
|
+
"exit_code": -1,
|
|
834
|
+
"output": "",
|
|
835
|
+
"error": "Batch timeout exceeded",
|
|
836
|
+
"timed_out": True,
|
|
837
|
+
"duration": time.time() - batch_start,
|
|
838
|
+
"start_time": batch_start,
|
|
839
|
+
"end_time": time.time()
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
# Ensure all results are non-None before returning
|
|
843
|
+
final_results = []
|
|
844
|
+
for r in results:
|
|
845
|
+
if r is not None:
|
|
846
|
+
final_results.append(r)
|
|
847
|
+
return final_results
|
|
848
|
+
|
|
849
|
+
def cleanup(self) -> None:
|
|
850
|
+
"""
|
|
851
|
+
Clean up all resources and processes.
|
|
852
|
+
"""
|
|
853
|
+
log.debug("Cleaning up CommandExecutor")
|
|
854
|
+
|
|
855
|
+
# Clean up all processes
|
|
856
|
+
failed_pids = self.process_manager.cleanup_all_processes()
|
|
857
|
+
|
|
858
|
+
if failed_pids:
|
|
859
|
+
log.warning(f"Failed to cleanup {len(failed_pids)} processes: {failed_pids}")
|
|
860
|
+
|
|
861
|
+
log.debug("CommandExecutor cleanup completed")
|
|
862
|
+
|
|
863
|
+
def __del__(self):
|
|
864
|
+
"""Cleanup when executor is destroyed."""
|
|
865
|
+
try:
|
|
866
|
+
self.cleanup()
|
|
867
|
+
except Exception:
|
|
868
|
+
pass # Ignore errors during cleanup
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
# Convenience functions for simple usage
|
|
872
|
+
def execute_command(
|
|
873
|
+
command: Union[str, List[str]],
|
|
874
|
+
timeout: Optional[float] = None,
|
|
875
|
+
cwd: Optional[str] = None,
|
|
876
|
+
verbose: bool = False,
|
|
877
|
+
**kwargs
|
|
878
|
+
) -> Tuple[int, str]:
|
|
879
|
+
"""
|
|
880
|
+
Convenience function to execute a command with timeout support.
|
|
881
|
+
|
|
882
|
+
Args:
|
|
883
|
+
command: Command to execute
|
|
884
|
+
timeout: Timeout in seconds
|
|
885
|
+
cwd: Working directory
|
|
886
|
+
verbose: Whether to enable verbose logging
|
|
887
|
+
**kwargs: Additional arguments
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
Tuple of (exit_code, output)
|
|
891
|
+
"""
|
|
892
|
+
# Create a timeout config if timeout is specified
|
|
893
|
+
config = None
|
|
894
|
+
if timeout is not None:
|
|
895
|
+
config = TimeoutConfig(default_timeout=timeout)
|
|
896
|
+
|
|
897
|
+
executor = CommandExecutor(config=config, verbose=verbose)
|
|
898
|
+
try:
|
|
899
|
+
return executor.execute(command, timeout, cwd, **kwargs)
|
|
900
|
+
finally:
|
|
901
|
+
executor.cleanup()
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def execute_command_generator(
|
|
905
|
+
command: Union[str, List[str]],
|
|
906
|
+
timeout: Optional[float] = None,
|
|
907
|
+
cwd: Optional[str] = None,
|
|
908
|
+
verbose: bool = False,
|
|
909
|
+
**kwargs
|
|
910
|
+
) -> Generator[str, None, int]:
|
|
911
|
+
"""
|
|
912
|
+
Convenience function to execute a command and stream output.
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
command: Command to execute
|
|
916
|
+
timeout: Timeout in seconds
|
|
917
|
+
cwd: Working directory
|
|
918
|
+
verbose: Whether to enable verbose logging
|
|
919
|
+
**kwargs: Additional arguments
|
|
920
|
+
|
|
921
|
+
Yields:
|
|
922
|
+
Command output strings
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
Final exit code
|
|
926
|
+
"""
|
|
927
|
+
executor = CommandExecutor(verbose=verbose)
|
|
928
|
+
return executor.execute_generator(command, timeout, cwd, **kwargs)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def execute_command_background(
|
|
932
|
+
command: Union[str, List[str]],
|
|
933
|
+
cwd: Optional[str] = None,
|
|
934
|
+
verbose: bool = False,
|
|
935
|
+
**kwargs
|
|
936
|
+
) -> Dict[str, Any]:
|
|
937
|
+
"""
|
|
938
|
+
Convenience function to execute a command in the background.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
command: Command to execute
|
|
942
|
+
cwd: Working directory
|
|
943
|
+
verbose: Whether to enable verbose logging
|
|
944
|
+
**kwargs: Additional arguments
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
Dictionary containing process information with PID
|
|
948
|
+
"""
|
|
949
|
+
import uuid
|
|
950
|
+
|
|
951
|
+
# Use global process manager to maintain background processes
|
|
952
|
+
manager = _get_global_process_manager()
|
|
953
|
+
|
|
954
|
+
# Convert command to string for logging
|
|
955
|
+
command_str = _command_to_string(command)
|
|
956
|
+
|
|
957
|
+
# Generate unique process ID
|
|
958
|
+
process_uniq_id = str(uuid.uuid4())
|
|
959
|
+
|
|
960
|
+
try:
|
|
961
|
+
# Create background process directly with process manager
|
|
962
|
+
process = manager.create_background_process(
|
|
963
|
+
command=command,
|
|
964
|
+
cwd=cwd,
|
|
965
|
+
process_uniq_id=process_uniq_id,
|
|
966
|
+
**kwargs
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
if verbose:
|
|
970
|
+
log.info(f"Started background command: {command_str} (PID: {process.pid}, ID: {process_uniq_id})")
|
|
971
|
+
|
|
972
|
+
return {
|
|
973
|
+
"pid": process.pid,
|
|
974
|
+
"process_uniq_id": process_uniq_id,
|
|
975
|
+
"command": command_str,
|
|
976
|
+
"working_directory": cwd or os.getcwd(),
|
|
977
|
+
"start_time": time.time(),
|
|
978
|
+
"status": "running"
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
except Exception as e:
|
|
982
|
+
raise CommandExecutionError(f"Failed to start background command: {str(e)}")
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# Global process manager for background processes
|
|
986
|
+
_global_process_manager: Optional[ProcessManager] = None
|
|
987
|
+
_global_process_manager_lock = threading.Lock()
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _get_global_process_manager() -> ProcessManager:
|
|
991
|
+
"""Get or create global process manager for background processes."""
|
|
992
|
+
global _global_process_manager
|
|
993
|
+
|
|
994
|
+
if _global_process_manager is None:
|
|
995
|
+
with _global_process_manager_lock:
|
|
996
|
+
if _global_process_manager is None:
|
|
997
|
+
config = TimeoutConfig()
|
|
998
|
+
_global_process_manager = ProcessManager(config)
|
|
999
|
+
|
|
1000
|
+
return _global_process_manager
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def get_background_processes() -> Dict[int, Dict[str, Any]]:
|
|
1004
|
+
"""
|
|
1005
|
+
Convenience function to get all background processes.
|
|
1006
|
+
|
|
1007
|
+
Returns:
|
|
1008
|
+
Dictionary mapping PID to process information
|
|
1009
|
+
"""
|
|
1010
|
+
# Use global process manager to maintain state across calls
|
|
1011
|
+
manager = _get_global_process_manager()
|
|
1012
|
+
return manager.get_background_processes()
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def cleanup_background_process(pid: int, timeout: Optional[float] = None) -> bool:
|
|
1016
|
+
"""
|
|
1017
|
+
Convenience function to cleanup a specific background process.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
pid: Process ID to cleanup
|
|
1021
|
+
timeout: Timeout for cleanup
|
|
1022
|
+
|
|
1023
|
+
Returns:
|
|
1024
|
+
True if cleanup successful
|
|
1025
|
+
"""
|
|
1026
|
+
# Use global process manager
|
|
1027
|
+
manager = _get_global_process_manager()
|
|
1028
|
+
return manager.cleanup_background_process(pid, timeout)
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def get_background_process_info(pid: int) -> Optional[Dict[str, Any]]:
|
|
1032
|
+
"""
|
|
1033
|
+
Convenience function to get information about a specific background process.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
pid: Process ID
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
Process information or None if not found
|
|
1040
|
+
"""
|
|
1041
|
+
# Use global process manager
|
|
1042
|
+
manager = _get_global_process_manager()
|
|
1043
|
+
return manager.get_background_process_info(pid)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def execute_commands(
|
|
1047
|
+
commands: List[Union[str, List[str]]],
|
|
1048
|
+
timeout: Optional[float] = None,
|
|
1049
|
+
per_command_timeout: Optional[float] = None,
|
|
1050
|
+
parallel: bool = True,
|
|
1051
|
+
cwd: Optional[str] = None,
|
|
1052
|
+
env: Optional[Dict[str, str]] = None,
|
|
1053
|
+
verbose: bool = False,
|
|
1054
|
+
**kwargs
|
|
1055
|
+
) -> List[Dict[str, Any]]:
|
|
1056
|
+
"""
|
|
1057
|
+
Execute multiple commands in batch and return all results.
|
|
1058
|
+
|
|
1059
|
+
This is a convenience function that creates a CommandExecutor and executes
|
|
1060
|
+
a batch of commands either in parallel or serially.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
commands: List of commands to execute
|
|
1064
|
+
timeout: Overall timeout for all commands (seconds)
|
|
1065
|
+
per_command_timeout: Timeout for each individual command (seconds)
|
|
1066
|
+
parallel: Whether to execute commands in parallel (True) or serial (False)
|
|
1067
|
+
cwd: Working directory
|
|
1068
|
+
env: Environment variables
|
|
1069
|
+
verbose: Whether to enable verbose logging
|
|
1070
|
+
**kwargs: Additional arguments for subprocess.Popen
|
|
1071
|
+
|
|
1072
|
+
Returns:
|
|
1073
|
+
List of dictionaries containing results for each command:
|
|
1074
|
+
[
|
|
1075
|
+
{
|
|
1076
|
+
"command": str,
|
|
1077
|
+
"index": int,
|
|
1078
|
+
"exit_code": int,
|
|
1079
|
+
"output": str,
|
|
1080
|
+
"error": str or None,
|
|
1081
|
+
"timed_out": bool,
|
|
1082
|
+
"duration": float,
|
|
1083
|
+
"start_time": float,
|
|
1084
|
+
"end_time": float
|
|
1085
|
+
},
|
|
1086
|
+
...
|
|
1087
|
+
]
|
|
1088
|
+
|
|
1089
|
+
Raises:
|
|
1090
|
+
CommandExecutionError: If batch execution setup fails
|
|
1091
|
+
|
|
1092
|
+
Examples:
|
|
1093
|
+
>>> # Execute commands in parallel with overall timeout
|
|
1094
|
+
>>> results = execute_commands(
|
|
1095
|
+
... ["echo Hello", "echo World"],
|
|
1096
|
+
... timeout=10.0,
|
|
1097
|
+
... parallel=True
|
|
1098
|
+
... )
|
|
1099
|
+
>>> for result in results:
|
|
1100
|
+
... print(f"{result['command']}: {result['output'].strip()}")
|
|
1101
|
+
|
|
1102
|
+
>>> # Execute commands serially with per-command timeout
|
|
1103
|
+
>>> results = execute_commands(
|
|
1104
|
+
... ["sleep 1", "echo Done"],
|
|
1105
|
+
... per_command_timeout=2.0,
|
|
1106
|
+
... parallel=False
|
|
1107
|
+
... )
|
|
1108
|
+
"""
|
|
1109
|
+
# Create executor with appropriate configuration
|
|
1110
|
+
config = TimeoutConfig()
|
|
1111
|
+
executor = CommandExecutor(config=config, verbose=verbose)
|
|
1112
|
+
|
|
1113
|
+
try:
|
|
1114
|
+
# Execute batch
|
|
1115
|
+
results = executor.execute_batch(
|
|
1116
|
+
commands=commands,
|
|
1117
|
+
timeout=timeout,
|
|
1118
|
+
per_command_timeout=per_command_timeout,
|
|
1119
|
+
parallel=parallel,
|
|
1120
|
+
cwd=cwd,
|
|
1121
|
+
env=env,
|
|
1122
|
+
**kwargs
|
|
1123
|
+
)
|
|
1124
|
+
return results
|
|
1125
|
+
finally:
|
|
1126
|
+
# Clean up
|
|
1127
|
+
executor.cleanup()
|