danielutils 1.1.28__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.
- danielutils/__init__.py +50 -0
- danielutils/abstractions/__init__.py +3 -0
- danielutils/abstractions/db/__init__.py +10 -0
- danielutils/abstractions/db/database.py +206 -0
- danielutils/abstractions/db/database_definitions.py +219 -0
- danielutils/abstractions/db/database_exceptions.py +34 -0
- danielutils/abstractions/db/database_factory.py +70 -0
- danielutils/abstractions/db/database_initializer.py +261 -0
- danielutils/abstractions/db/database_models.py +213 -0
- danielutils/abstractions/db/dependencies.py +6 -0
- danielutils/abstractions/db/implementations/__init__.py +4 -0
- danielutils/abstractions/db/implementations/in_memory_database.py +365 -0
- danielutils/abstractions/db/implementations/persistent_in_memory_database.py +174 -0
- danielutils/abstractions/db/implementations/redis_database.py +403 -0
- danielutils/abstractions/db/implementations/sqlite_database.py +491 -0
- danielutils/abstractions/multiprogramming/__init__.py +3 -0
- danielutils/abstractions/multiprogramming/multi_id.py +26 -0
- danielutils/abstractions/multiprogramming/worker.py +104 -0
- danielutils/abstractions/multiprogramming/worker_pool.py +98 -0
- danielutils/abstractions/repl.py +125 -0
- danielutils/aliases.py +174 -0
- danielutils/async_/__init__.py +7 -0
- danielutils/async_/async_cmd.py +28 -0
- danielutils/async_/async_command.py +879 -0
- danielutils/async_/async_layered_command.py +125 -0
- danielutils/async_/async_retry_executor.py +86 -0
- danielutils/async_/async_worker_pool.py +110 -0
- danielutils/async_/time_strategy.py +73 -0
- danielutils/async_/utils.py +57 -0
- danielutils/better_builtins/__init__.py +3 -0
- danielutils/better_builtins/counter.py +48 -0
- danielutils/better_builtins/frange.py +266 -0
- danielutils/better_builtins/typed_builtins/__init__.py +4 -0
- danielutils/better_builtins/typed_builtins/factory.py +126 -0
- danielutils/better_builtins/typed_builtins/tdict.py +36 -0
- danielutils/better_builtins/typed_builtins/tlist.py +223 -0
- danielutils/better_builtins/typed_builtins/tset.py +67 -0
- danielutils/better_builtins/typed_builtins/ttuple.py +15 -0
- danielutils/colors.py +203 -0
- danielutils/context_managers/__init__.py +5 -0
- danielutils/context_managers/attr_context.py +40 -0
- danielutils/context_managers/multi_context.py +34 -0
- danielutils/context_managers/optional_context.py +33 -0
- danielutils/context_managers/state_context.py +25 -0
- danielutils/context_managers/temporary_file.py +108 -0
- danielutils/convenience.py +12 -0
- danielutils/conversions/__init__.py +2 -0
- danielutils/conversions/main_conversions.py +131 -0
- danielutils/conversions/specialized_conversions/__init__.py +2 -0
- danielutils/conversions/specialized_conversions/to_hex.py +25 -0
- danielutils/conversions/specialized_conversions/to_int.py +19 -0
- danielutils/custom_types.py +18 -0
- danielutils/data_structures/__init__.py +8 -0
- danielutils/data_structures/algorithms.py +52 -0
- danielutils/data_structures/comparer.py +47 -0
- danielutils/data_structures/default_dict.py +23 -0
- danielutils/data_structures/functions.py +35 -0
- danielutils/data_structures/graph/__init__.py +4 -0
- danielutils/data_structures/graph/binary_node.py +71 -0
- danielutils/data_structures/graph/graph.py +208 -0
- danielutils/data_structures/graph/multinode.py +101 -0
- danielutils/data_structures/graph/node.py +60 -0
- danielutils/data_structures/heap/__init__.py +3 -0
- danielutils/data_structures/heap/heap.py +119 -0
- danielutils/data_structures/heap/max_heap.py +18 -0
- danielutils/data_structures/heap/min_heap.py +18 -0
- danielutils/data_structures/queue/__init__.py +3 -0
- danielutils/data_structures/queue/atomic_queue.py +15 -0
- danielutils/data_structures/queue/priority_queue.py +110 -0
- danielutils/data_structures/queue/queue.py +92 -0
- danielutils/data_structures/stack.py +102 -0
- danielutils/data_structures/trees/__init__.py +2 -0
- danielutils/data_structures/trees/binary_syntax_tree.py +84 -0
- danielutils/data_structures/trees/binary_tree.py +63 -0
- danielutils/date.py +43 -0
- danielutils/date_time.py +23 -0
- danielutils/decorators/__init__.py +18 -0
- danielutils/decorators/atomic.py +45 -0
- danielutils/decorators/attach.py +61 -0
- danielutils/decorators/chain_decorators.py +39 -0
- danielutils/decorators/decorate_conditionally.py +32 -0
- danielutils/decorators/delay_call.py +47 -0
- danielutils/decorators/deprecate.py +46 -0
- danielutils/decorators/final.py +39 -0
- danielutils/decorators/limit_recursion.py +63 -0
- danielutils/decorators/memo.py +68 -0
- danielutils/decorators/normalize_decorator.py +55 -0
- danielutils/decorators/overload.py +204 -0
- danielutils/decorators/partially_implemented.py +32 -0
- danielutils/decorators/processify.py +74 -0
- danielutils/decorators/property.py +42 -0
- danielutils/decorators/singleton.py +59 -0
- danielutils/decorators/threadify.py +40 -0
- danielutils/decorators/timeout.py +82 -0
- danielutils/decorators/total_ordering.py +82 -0
- danielutils/decorators/validate.py +227 -0
- danielutils/exceptions.py +90 -0
- danielutils/functions/__init__.py +13 -0
- danielutils/functions/areoneof.py +47 -0
- danielutils/functions/check_foreach.py +39 -0
- danielutils/functions/factorial.py +24 -0
- danielutils/functions/flatten.py +33 -0
- danielutils/functions/foreach.py +20 -0
- danielutils/functions/isoftype.py +475 -0
- danielutils/functions/isoneof.py +71 -0
- danielutils/functions/multiloop.py +51 -0
- danielutils/functions/parallel_for.py +47 -0
- danielutils/functions/partition.py +45 -0
- danielutils/functions/powerset.py +31 -0
- danielutils/functions/subseteq.py +25 -0
- danielutils/functions/types_subseteq.py +51 -0
- danielutils/generators/__init__.py +3 -0
- danielutils/generators/conditional_generator.py +59 -0
- danielutils/generators/generator_from_stream.py +31 -0
- danielutils/generators/join_generators.py +109 -0
- danielutils/internet.py +92 -0
- danielutils/io_.py +451 -0
- danielutils/java/__init__.py +18 -0
- danielutils/java/interfaces/__init__.py +1 -0
- danielutils/java/interfaces/comparable.py +31 -0
- danielutils/java/java_interface.py +59 -0
- danielutils/logging_/__init__.py +2 -0
- danielutils/logging_/_impl/__init__.py +4 -0
- danielutils/logging_/_impl/builtin_impls/__init__.py +2 -0
- danielutils/logging_/_impl/builtin_impls/file_logger.py +35 -0
- danielutils/logging_/_impl/builtin_impls/print_logger.py +21 -0
- danielutils/logging_/_impl/log_level.py +13 -0
- danielutils/logging_/_impl/logger.py +109 -0
- danielutils/logging_/_impl/logger_strategy_impl_base.py +37 -0
- danielutils/logging_/utils.py +199 -0
- danielutils/lombok/__init__.py +1 -0
- danielutils/lombok/builder.py +68 -0
- danielutils/math_/__init__.py +2 -0
- danielutils/math_/constants.py +103 -0
- danielutils/math_/functions.py +34 -0
- danielutils/math_/math_print.py +52 -0
- danielutils/math_/math_symbols.py +125 -0
- danielutils/metaclasses/__init__.py +5 -0
- danielutils/metaclasses/atomic_class_meta.py +36 -0
- danielutils/metaclasses/implicit_data_deleter_meta.py +87 -0
- danielutils/metaclasses/instance_cache_meta.py +66 -0
- danielutils/metaclasses/interface.py +328 -0
- danielutils/metaclasses/overload_meta.py +48 -0
- danielutils/mock_/__init__.py +1 -0
- danielutils/mock_/mock_module.py +27 -0
- danielutils/path.py +55 -0
- danielutils/print_.py +123 -0
- danielutils/progress_bar/__init__.py +3 -0
- danielutils/progress_bar/ascii_progress_bar.py +161 -0
- danielutils/progress_bar/progress_bar.py +75 -0
- danielutils/progress_bar/progress_bar_pool.py +72 -0
- danielutils/protocols/__init__.py +3 -0
- danielutils/protocols/dictable.py +38 -0
- danielutils/protocols/evaluable.py +26 -0
- danielutils/protocols/serializable.py +51 -0
- danielutils/py.typed +0 -0
- danielutils/random_.py +47 -0
- danielutils/reflection/__init__.py +6 -0
- danielutils/reflection/declarations.py +288 -0
- danielutils/reflection/file/__init__.py +1 -0
- danielutils/reflection/file/file_reflection.py +85 -0
- danielutils/reflection/function/__init__.py +1 -0
- danielutils/reflection/function/function_reflections.py +152 -0
- danielutils/reflection/info_classes/__init__.py +6 -0
- danielutils/reflection/info_classes/argument_info.py +278 -0
- danielutils/reflection/info_classes/class_info.py +204 -0
- danielutils/reflection/info_classes/decorator_info.py +54 -0
- danielutils/reflection/info_classes/file_info.py +1180 -0
- danielutils/reflection/info_classes/folder_info.py +592 -0
- danielutils/reflection/info_classes/function_info.py +551 -0
- danielutils/reflection/info_classes/import_info.py +207 -0
- danielutils/reflection/interpreter/__init__.py +9 -0
- danielutils/reflection/interpreter/callstack.py +57 -0
- danielutils/reflection/interpreter/get_traceback.py +27 -0
- danielutils/reflection/interpreter/interpreter.py +41 -0
- danielutils/reflection/interpreter/is_debugging.py +13 -0
- danielutils/reflection/interpreter/os_.py +33 -0
- danielutils/reflection/interpreter/packages.py +33 -0
- danielutils/reflection/interpreter/python_version.py +33 -0
- danielutils/reflection/interpreter/signals.py +18 -0
- danielutils/reflection/interpreter/tracer.py +175 -0
- danielutils/reflection/module/__init__.py +3 -0
- danielutils/reflection/module/lazy_module.py +104 -0
- danielutils/reflection/module/module_reflections.py +34 -0
- danielutils/reflection/module/package_reflection.py +161 -0
- danielutils/retry_executor/__init__.py +3 -0
- danielutils/retry_executor/backoff_strategies/__init__.py +6 -0
- danielutils/retry_executor/backoff_strategies/constant_backoff.py +25 -0
- danielutils/retry_executor/backoff_strategies/exponential_backoff.py +26 -0
- danielutils/retry_executor/backoff_strategies/functional_backoff.py +29 -0
- danielutils/retry_executor/backoff_strategies/linear_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/multiplicative_backoff.py +23 -0
- danielutils/retry_executor/backoff_strategies/no_backoff.py +17 -0
- danielutils/retry_executor/backoff_strategy.py +28 -0
- danielutils/retry_executor/retry_executor.py +55 -0
- danielutils/snippets/__init__.py +1 -0
- danielutils/snippets/try_get.py +28 -0
- danielutils/system/__init__.py +3 -0
- danielutils/system/independent.py +197 -0
- danielutils/system/layered_command.py +130 -0
- danielutils/system/windows/__init__.py +2 -0
- danielutils/system/windows/utils/__init__.py +1 -0
- danielutils/system/windows/utils/filetime.py +19 -0
- danielutils/system/windows/win32_ctime.py +183 -0
- danielutils/system/windows/windows.py +78 -0
- danielutils/testing/__init__.py +1 -0
- danielutils/testing/unittest_/__init__.py +2 -0
- danielutils/testing/unittest_/always_teardown_testcase.py +75 -0
- danielutils/testing/unittest_/auto_cwd_testcase.py +114 -0
- danielutils/text.py +169 -0
- danielutils/time.py +64 -0
- danielutils/tools/__init__.py +1 -0
- danielutils/tools/unittest_test_runner/__init__.py +9 -0
- danielutils/tools/unittest_test_runner/cli.py +70 -0
- danielutils/tools/unittest_test_runner/core/__init__.py +17 -0
- danielutils/tools/unittest_test_runner/core/discovery.py +322 -0
- danielutils/tools/unittest_test_runner/core/execution.py +244 -0
- danielutils/tools/unittest_test_runner/core/parser.py +163 -0
- danielutils/tools/unittest_test_runner/core/runner.py +561 -0
- danielutils/tools/unittest_test_runner/models.py +82 -0
- danielutils/university/__init__.py +7 -0
- danielutils/university/computability_and_complexity/__init__.py +2 -0
- danielutils/university/computability_and_complexity/discreate_finite_automaton.py +89 -0
- danielutils/university/computability_and_complexity/languages/__init__.py +0 -0
- danielutils/university/computability_and_complexity/languages/language.py +17 -0
- danielutils/university/computability_and_complexity/languages/sat.py +71 -0
- danielutils/university/computability_and_complexity/turing_machine.py +47 -0
- danielutils/university/databases/__init__.py +1 -0
- danielutils/university/databases/all.py +756 -0
- danielutils/university/image_proccesing/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/__init__.py +3 -0
- danielutils/university/image_proccesing/encoding/encoding.py +35 -0
- danielutils/university/image_proccesing/encoding/lossless/__init__.py +4 -0
- danielutils/university/image_proccesing/encoding/lossless/huffman.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/lossless_encoding.py +10 -0
- danielutils/university/image_proccesing/encoding/lossless/lzw.py +0 -0
- danielutils/university/image_proccesing/encoding/lossless/run_length.py +22 -0
- danielutils/university/image_proccesing/encoding/lossy/__init__.py +1 -0
- danielutils/university/image_proccesing/encoding/lossy/lossy_encoding.py +10 -0
- danielutils/university/image_proccesing/tansformations/__init__.py +0 -0
- danielutils/university/image_proccesing/tansformations/gaussian.py +0 -0
- danielutils/university/image_proccesing/tansformations/gradient.py +0 -0
- danielutils/university/image_proccesing/tansformations/hough.py +0 -0
- danielutils/university/image_proccesing/tansformations/laplacian.py +0 -0
- danielutils/university/image_proccesing/tansformations/transformation.py +11 -0
- danielutils/university/linear_algebra/__init__.py +1 -0
- danielutils/university/linear_algebra/matrix.py +404 -0
- danielutils/university/machine_learning/__init__.py +0 -0
- danielutils/university/machine_learning/activation_functions/__init__.py +2 -0
- danielutils/university/machine_learning/activation_functions/activation_function.py +11 -0
- danielutils/university/machine_learning/activation_functions/relu.py +11 -0
- danielutils/university/machine_learning/neuron.py +15 -0
- danielutils/university/oop/__init__.py +1 -0
- danielutils/university/oop/observer.py +31 -0
- danielutils/university/oop/strategy.py +9 -0
- danielutils/university/probability/__init__.py +7 -0
- danielutils/university/probability/conditional_variable/__init__.py +3 -0
- danielutils/university/probability/conditional_variable/conditional_variable.py +138 -0
- danielutils/university/probability/conditional_variable/continuous/__init__.py +0 -0
- danielutils/university/probability/conditional_variable/discrete/__init__.py +8 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli.py +38 -0
- danielutils/university/probability/conditional_variable/discrete/bernoulli_sum.py +31 -0
- danielutils/university/probability/conditional_variable/discrete/binomial.py +40 -0
- danielutils/university/probability/conditional_variable/discrete/conditional_from_discrete_probability_func.py +36 -0
- danielutils/university/probability/conditional_variable/discrete/discrete.py +42 -0
- danielutils/university/probability/conditional_variable/discrete/geometric.py +43 -0
- danielutils/university/probability/conditional_variable/discrete/poisson.py +32 -0
- danielutils/university/probability/conditional_variable/discrete/uniform.py +33 -0
- danielutils/university/probability/distributions.py +35 -0
- danielutils/university/probability/expressions/__init__.py +2 -0
- danielutils/university/probability/expressions/accumulation_expression.py +291 -0
- danielutils/university/probability/expressions/probability_expression.py +110 -0
- danielutils/university/probability/funcs/__init__.py +4 -0
- danielutils/university/probability/funcs/covariance.py +12 -0
- danielutils/university/probability/funcs/expected_value.py +41 -0
- danielutils/university/probability/funcs/probability_function.py +14 -0
- danielutils/university/probability/funcs/variance.py +17 -0
- danielutils/university/probability/operator.py +79 -0
- danielutils/university/probability/protocols.py +32 -0
- danielutils/university/probability/supp.py +101 -0
- danielutils/university/probability/transformation.py +44 -0
- danielutils/versioned_imports.py +31 -0
- danielutils-1.1.28.dist-info/METADATA +390 -0
- danielutils-1.1.28.dist-info/RECORD +287 -0
- danielutils-1.1.28.dist-info/WHEEL +5 -0
- danielutils-1.1.28.dist-info/licenses/LICENSE +21 -0
- danielutils-1.1.28.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic AsyncCommand class for standardized async command execution.
|
|
3
|
+
|
|
4
|
+
This module provides a comprehensive AsyncCommand class that encapsulates command execution
|
|
5
|
+
with state tracking, result handling, and CLI/GUI strategy support using async patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import functools
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import List, Optional, Union, Callable, Dict, Literal
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from ..logging_.utils import get_logger
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _to_thread(func: Callable, /, *args, **kwargs):
|
|
24
|
+
if hasattr(asyncio, "to_thread"):
|
|
25
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
|
26
|
+
loop = asyncio.get_event_loop()
|
|
27
|
+
return await loop.run_in_executor(None, functools.partial(func, *args, **kwargs))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CommandResult:
|
|
32
|
+
"""Comprehensive result object for command execution."""
|
|
33
|
+
args: List[str]
|
|
34
|
+
returncode: int
|
|
35
|
+
stdout: str
|
|
36
|
+
stderr: str
|
|
37
|
+
pid: int
|
|
38
|
+
start_time: str
|
|
39
|
+
end_time: str
|
|
40
|
+
duration_seconds: float
|
|
41
|
+
timeout_occurred: bool
|
|
42
|
+
killed: bool
|
|
43
|
+
success: bool
|
|
44
|
+
command: str # The original command string
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CommandResponse:
|
|
49
|
+
success: bool
|
|
50
|
+
output: str
|
|
51
|
+
error: Optional[str] = None
|
|
52
|
+
result: Optional[CommandResult] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CommandState(Enum):
|
|
56
|
+
"""Enumeration of possible command states."""
|
|
57
|
+
PENDING = "pending"
|
|
58
|
+
RUNNING = "running"
|
|
59
|
+
COMPLETED = "completed"
|
|
60
|
+
FAILED = "failed"
|
|
61
|
+
TIMEOUT = "timeout"
|
|
62
|
+
KILLED = "killed"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CommandType(Enum):
|
|
66
|
+
"""Enumeration of command types."""
|
|
67
|
+
CLI = "cli"
|
|
68
|
+
GUI = "gui"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CommandExecutionResult:
|
|
73
|
+
"""Result of command execution with comprehensive details."""
|
|
74
|
+
command: 'AsyncCommand'
|
|
75
|
+
success: bool
|
|
76
|
+
return_code: int
|
|
77
|
+
stdout: str
|
|
78
|
+
stderr: str
|
|
79
|
+
execution_time: float
|
|
80
|
+
state: CommandState
|
|
81
|
+
killed: bool = False
|
|
82
|
+
timeout_occurred: bool = False
|
|
83
|
+
start_time: Optional[datetime] = None
|
|
84
|
+
end_time: Optional[datetime] = None
|
|
85
|
+
pid: Optional[int] = None
|
|
86
|
+
command_type: CommandType = CommandType.CLI
|
|
87
|
+
exception: Optional[Exception] = None
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
"""String representation of the result."""
|
|
91
|
+
status = "SUCCESS" if self.success else "FAILED"
|
|
92
|
+
if self.timeout_occurred:
|
|
93
|
+
status = "TIMEOUT"
|
|
94
|
+
elif self.killed:
|
|
95
|
+
status = "KILLED"
|
|
96
|
+
|
|
97
|
+
return f"CommandExecutionResult(command={self.command}, success={self.success}, return_code={self.return_code}, stdout='{self.stdout}', stderr='{self.stderr}', execution_time={self.execution_time}, state={self.state}, killed={self.killed}, timeout_occurred={self.timeout_occurred}, start_time={self.start_time}, end_time={self.end_time}, pid={self.pid}, command_type={self.command_type}, exception={self.exception}) - {status}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AsyncCommand:
|
|
101
|
+
"""
|
|
102
|
+
Generic async command execution class with comprehensive state tracking and result handling.
|
|
103
|
+
|
|
104
|
+
This class provides a unified interface for executing commands with support for:
|
|
105
|
+
- Async execution with proper event loop management
|
|
106
|
+
- State tracking (pending, running, completed, failed, timeout, killed)
|
|
107
|
+
- Comprehensive result reporting (stdout, stderr, return code, execution time)
|
|
108
|
+
- CLI/GUI execution strategies
|
|
109
|
+
- Callback support for lifecycle events
|
|
110
|
+
- Timeout handling
|
|
111
|
+
- Process management (kill, wait)
|
|
112
|
+
"""
|
|
113
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
args: List[str],
|
|
118
|
+
command_type: CommandType = CommandType.CLI,
|
|
119
|
+
timeout: Optional[float] = None,
|
|
120
|
+
cwd: Optional[Union[str, Path]] = None,
|
|
121
|
+
env: Optional[Dict[str, str]] = None,
|
|
122
|
+
on_start: Optional[Callable[['AsyncCommand'], None]] = None,
|
|
123
|
+
on_complete: Optional[Callable[['AsyncCommand', CommandExecutionResult], None]] = None,
|
|
124
|
+
on_error: Optional[Callable[['AsyncCommand', Exception], None]] = None,
|
|
125
|
+
):
|
|
126
|
+
"""
|
|
127
|
+
Initialize the AsyncCommand.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
args: Command arguments as a list of strings
|
|
131
|
+
command_type: Type of command (CLI or GUI)
|
|
132
|
+
timeout: Timeout in seconds (None for no timeout)
|
|
133
|
+
cwd: Working directory for command execution
|
|
134
|
+
env: Environment variables for command execution
|
|
135
|
+
on_start: Callback called when command starts
|
|
136
|
+
on_complete: Callback called when command completes
|
|
137
|
+
on_error: Callback called when command fails
|
|
138
|
+
"""
|
|
139
|
+
logger.debug("Initializing AsyncCommand with args=%s, command_type=%s, timeout=%s", args, command_type, timeout)
|
|
140
|
+
self.args = args
|
|
141
|
+
self.command_type = command_type
|
|
142
|
+
if timeout is not None:
|
|
143
|
+
if timeout <= 0.0:
|
|
144
|
+
raise ValueError("timeout must be strictly positive")
|
|
145
|
+
self.timeout = timeout
|
|
146
|
+
self.cwd = Path(cwd) if cwd else None
|
|
147
|
+
self.env = env or {}
|
|
148
|
+
self.on_start = on_start
|
|
149
|
+
self.on_complete = on_complete
|
|
150
|
+
self.on_error = on_error
|
|
151
|
+
|
|
152
|
+
# State management
|
|
153
|
+
self._state = CommandState.PENDING
|
|
154
|
+
self._process: Optional[subprocess.Popen] = None
|
|
155
|
+
self._result: Optional[CommandExecutionResult] = None
|
|
156
|
+
self._start_time: Optional[datetime] = None
|
|
157
|
+
logger.debug("AsyncCommand initialized successfully with state=%s", self._state)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def state(self) -> CommandState:
|
|
161
|
+
"""Get the current state of the command."""
|
|
162
|
+
return self._state
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def is_running(self) -> bool:
|
|
166
|
+
"""Check if the command is currently running."""
|
|
167
|
+
return self._state == CommandState.RUNNING
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def is_completed(self) -> bool:
|
|
171
|
+
"""Check if the command has completed (successfully or not)."""
|
|
172
|
+
return self._state in [CommandState.COMPLETED, CommandState.FAILED, CommandState.TIMEOUT, CommandState.KILLED]
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def result(self) -> Optional[CommandExecutionResult]:
|
|
176
|
+
"""Get the execution result if available."""
|
|
177
|
+
return self._result
|
|
178
|
+
|
|
179
|
+
async def execute(self, timeout: Optional[float] = None) -> CommandExecutionResult:
|
|
180
|
+
"""
|
|
181
|
+
Execute the command asynchronously.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
timeout: Override the default timeout for this execution
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
CommandExecutionResult: Comprehensive execution result
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
RuntimeError: If command is not in pending state
|
|
191
|
+
"""
|
|
192
|
+
# Log command execution start
|
|
193
|
+
self.logger.info("Executing command (async)", extra={
|
|
194
|
+
"data": {
|
|
195
|
+
"command": " ".join(self.args),
|
|
196
|
+
"command_args": self.args,
|
|
197
|
+
"command_type": self.command_type.value,
|
|
198
|
+
"timeout": timeout or self.timeout,
|
|
199
|
+
"cwd": str(self.cwd) if self.cwd else None,
|
|
200
|
+
"env_keys": list(self.env.keys()) if self.env else None
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
# Check if command is in pending state
|
|
205
|
+
if self._state != CommandState.PENDING:
|
|
206
|
+
raise RuntimeError(f"Command is not in pending state: {self._state}")
|
|
207
|
+
|
|
208
|
+
# Check if args is empty - this is considered an error
|
|
209
|
+
if not self.args:
|
|
210
|
+
end_time = datetime.now()
|
|
211
|
+
result = CommandExecutionResult(
|
|
212
|
+
command=self,
|
|
213
|
+
success=False,
|
|
214
|
+
return_code=-1,
|
|
215
|
+
stdout="",
|
|
216
|
+
stderr="Empty command arguments provided",
|
|
217
|
+
execution_time=0.0,
|
|
218
|
+
start_time=self._start_time,
|
|
219
|
+
end_time=end_time,
|
|
220
|
+
state=CommandState.FAILED
|
|
221
|
+
)
|
|
222
|
+
self._result = result
|
|
223
|
+
self._state = CommandState.FAILED
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
# Set initial state
|
|
227
|
+
self._state = CommandState.RUNNING
|
|
228
|
+
self._start_time = datetime.now()
|
|
229
|
+
|
|
230
|
+
# Call on_start callback
|
|
231
|
+
if self.on_start:
|
|
232
|
+
self.on_start(self)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Execute based on command type
|
|
236
|
+
if self.command_type == CommandType.CLI:
|
|
237
|
+
result = await self._execute_cli_strategy(timeout)
|
|
238
|
+
else:
|
|
239
|
+
result = await self._execute_gui_strategy(timeout)
|
|
240
|
+
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
except (OSError, ValueError, subprocess.SubprocessError, asyncio.TimeoutError) as e:
|
|
244
|
+
# Handle unexpected errors
|
|
245
|
+
end_time = datetime.now()
|
|
246
|
+
execution_time = (end_time - self._start_time).total_seconds()
|
|
247
|
+
|
|
248
|
+
result = CommandExecutionResult(
|
|
249
|
+
command=self,
|
|
250
|
+
success=False,
|
|
251
|
+
return_code=-1,
|
|
252
|
+
stdout="",
|
|
253
|
+
stderr=f"Command execution failed: {str(e)}",
|
|
254
|
+
execution_time=execution_time,
|
|
255
|
+
state=CommandState.FAILED,
|
|
256
|
+
start_time=self._start_time,
|
|
257
|
+
end_time=end_time,
|
|
258
|
+
command_type=self.command_type,
|
|
259
|
+
exception=e
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
self._state = CommandState.FAILED
|
|
263
|
+
self._result = result
|
|
264
|
+
|
|
265
|
+
if self.on_error:
|
|
266
|
+
self.on_error(self, e)
|
|
267
|
+
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
async def _execute_cli_strategy(self, timeout: Optional[float] = None) -> CommandExecutionResult:
|
|
271
|
+
"""Execute using CLI strategy asynchronously."""
|
|
272
|
+
start_time = self._start_time or datetime.now()
|
|
273
|
+
effective_timeout = timeout if timeout is not None else self.timeout
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Create subprocess
|
|
277
|
+
# Merge environment variables with current environment
|
|
278
|
+
env = os.environ.copy()
|
|
279
|
+
if self.env:
|
|
280
|
+
env.update(self.env)
|
|
281
|
+
|
|
282
|
+
# Use asyncio.create_subprocess_exec instead of subprocess.Popen to avoid threading issues
|
|
283
|
+
# Set encoding to UTF-8 for proper unicode support
|
|
284
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
285
|
+
*self.args,
|
|
286
|
+
stdout=asyncio.subprocess.PIPE,
|
|
287
|
+
stderr=asyncio.subprocess.PIPE,
|
|
288
|
+
cwd=str(self.cwd) if self.cwd else None,
|
|
289
|
+
env=env
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
pid = self._process.pid
|
|
293
|
+
self.logger.debug("CLI subprocess created (async)", extra={
|
|
294
|
+
"data": {
|
|
295
|
+
"pid": pid,
|
|
296
|
+
"command": " ".join(self.args),
|
|
297
|
+
"timeout": effective_timeout
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
# Wait for completion with timeout
|
|
302
|
+
try:
|
|
303
|
+
# Use asyncio.wait_for with the process directly instead of asyncio.to_thread
|
|
304
|
+
stdout, stderr = await asyncio.wait_for(self._process.communicate(),
|
|
305
|
+
timeout=effective_timeout)
|
|
306
|
+
returncode = self._process.returncode
|
|
307
|
+
|
|
308
|
+
# Decode bytes to strings
|
|
309
|
+
stdout = stdout.decode('utf8') if stdout else ""
|
|
310
|
+
stderr = stderr.decode('utf8') if stderr else ""
|
|
311
|
+
|
|
312
|
+
if returncode == 0:
|
|
313
|
+
self.logger.info("CLI subprocess completed successfully (async)", extra={
|
|
314
|
+
"data": {
|
|
315
|
+
"pid": pid,
|
|
316
|
+
"command": " ".join(self.args),
|
|
317
|
+
"returncode": returncode,
|
|
318
|
+
"stdout_length": len(stdout),
|
|
319
|
+
"stderr_length": len(stderr)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
else:
|
|
323
|
+
self.logger.warning("CLI subprocess completed with non-zero exit code (async)", extra={
|
|
324
|
+
"data": {
|
|
325
|
+
"pid": pid,
|
|
326
|
+
"command": " ".join(self.args),
|
|
327
|
+
"returncode": returncode,
|
|
328
|
+
"stderr_length": len(stderr)
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
except asyncio.TimeoutError:
|
|
333
|
+
# Kill the process on timeout
|
|
334
|
+
self.logger.warning("CLI subprocess timed out, killing process (async)", extra={
|
|
335
|
+
"data": {
|
|
336
|
+
"pid": pid,
|
|
337
|
+
"command": " ".join(self.args),
|
|
338
|
+
"timeout": effective_timeout
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
self._process.kill()
|
|
344
|
+
stdout, stderr = await self._process.communicate()
|
|
345
|
+
# Decode bytes to strings
|
|
346
|
+
stdout = stdout.decode('utf-8', errors='replace') if stdout else ""
|
|
347
|
+
stderr = stderr.decode('utf-8', errors='replace') if stderr else ""
|
|
348
|
+
except (OSError, subprocess.SubprocessError, BaseException):
|
|
349
|
+
stdout, stderr = "", ""
|
|
350
|
+
returncode = -1
|
|
351
|
+
|
|
352
|
+
self.logger.error("CLI subprocess killed due to timeout (async)", extra={
|
|
353
|
+
"data": {
|
|
354
|
+
"pid": pid,
|
|
355
|
+
"command": " ".join(self.args),
|
|
356
|
+
"timeout": effective_timeout,
|
|
357
|
+
"stderr_length": len(stderr),
|
|
358
|
+
"stdout_length": len(stdout)
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
# Set timeout state and return early
|
|
363
|
+
end_time = datetime.now()
|
|
364
|
+
execution_time = (end_time - start_time).total_seconds()
|
|
365
|
+
|
|
366
|
+
result = CommandExecutionResult(
|
|
367
|
+
command=self,
|
|
368
|
+
success=False,
|
|
369
|
+
return_code=returncode,
|
|
370
|
+
stdout=stdout or "",
|
|
371
|
+
stderr=stderr or "",
|
|
372
|
+
execution_time=execution_time,
|
|
373
|
+
state=CommandState.TIMEOUT,
|
|
374
|
+
timeout_occurred=True,
|
|
375
|
+
start_time=start_time,
|
|
376
|
+
end_time=end_time,
|
|
377
|
+
pid=pid,
|
|
378
|
+
command_type=self.command_type
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
self._state = CommandState.TIMEOUT
|
|
382
|
+
self._result = result
|
|
383
|
+
|
|
384
|
+
if self.on_complete:
|
|
385
|
+
self.on_complete(self, result)
|
|
386
|
+
|
|
387
|
+
return result
|
|
388
|
+
|
|
389
|
+
except (OSError, ValueError, subprocess.SubprocessError) as e:
|
|
390
|
+
returncode = -1
|
|
391
|
+
stdout = ""
|
|
392
|
+
stderr = f"CLI command failed to start: {str(e)}"
|
|
393
|
+
|
|
394
|
+
self.logger.error("CLI subprocess execution failed (async)", extra={
|
|
395
|
+
"data": {
|
|
396
|
+
"command": " ".join(self.args),
|
|
397
|
+
"error": str(e),
|
|
398
|
+
"timeout": effective_timeout
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
end_time = datetime.now()
|
|
403
|
+
execution_time = (end_time - start_time).total_seconds()
|
|
404
|
+
success = returncode == 0
|
|
405
|
+
state = CommandState.COMPLETED if success else CommandState.FAILED
|
|
406
|
+
|
|
407
|
+
# Check if this was a timeout
|
|
408
|
+
timeout_occurred = False
|
|
409
|
+
if returncode == -1 and effective_timeout is not None and execution_time >= effective_timeout * 0.9:
|
|
410
|
+
state = CommandState.TIMEOUT
|
|
411
|
+
timeout_occurred = True
|
|
412
|
+
|
|
413
|
+
result = CommandExecutionResult(
|
|
414
|
+
command=self,
|
|
415
|
+
success=success,
|
|
416
|
+
return_code=returncode,
|
|
417
|
+
stdout=stdout or "",
|
|
418
|
+
stderr=stderr or "",
|
|
419
|
+
execution_time=execution_time,
|
|
420
|
+
state=state,
|
|
421
|
+
timeout_occurred=timeout_occurred,
|
|
422
|
+
start_time=start_time,
|
|
423
|
+
end_time=end_time,
|
|
424
|
+
pid=self._process.pid if self._process else None,
|
|
425
|
+
command_type=self.command_type
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
self._state = state
|
|
429
|
+
self._result = result
|
|
430
|
+
|
|
431
|
+
# Call appropriate callback based on result
|
|
432
|
+
if not success and self.on_error:
|
|
433
|
+
# Create a generic exception for non-zero return codes
|
|
434
|
+
error = RuntimeError(f"Command failed with return code {returncode}")
|
|
435
|
+
self.on_error(self, error)
|
|
436
|
+
elif self.on_complete:
|
|
437
|
+
self.on_complete(self, result)
|
|
438
|
+
|
|
439
|
+
return result
|
|
440
|
+
|
|
441
|
+
async def _execute_gui_strategy(self, timeout: Optional[float] = None) -> CommandExecutionResult:
|
|
442
|
+
"""Execute using GUI strategy asynchronously."""
|
|
443
|
+
start_time = self._start_time or datetime.now()
|
|
444
|
+
effective_timeout = timeout if timeout is not None else self.timeout
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
# Create subprocess without capturing output
|
|
448
|
+
# Merge environment variables with current environment
|
|
449
|
+
env = os.environ.copy()
|
|
450
|
+
if self.env:
|
|
451
|
+
env.update(self.env)
|
|
452
|
+
|
|
453
|
+
self._process = subprocess.Popen(
|
|
454
|
+
self.args,
|
|
455
|
+
shell=True,
|
|
456
|
+
cwd=str(self.cwd) if self.cwd else None,
|
|
457
|
+
env=env
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
pid = self._process.pid
|
|
461
|
+
self.logger.debug("GUI subprocess created (async)", extra={
|
|
462
|
+
"data": {
|
|
463
|
+
"pid": pid,
|
|
464
|
+
"command": " ".join(self.args),
|
|
465
|
+
"timeout": effective_timeout
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
# Wait for completion with timeout
|
|
470
|
+
try:
|
|
471
|
+
returncode = await asyncio.wait_for(
|
|
472
|
+
_to_thread(self._process.wait),
|
|
473
|
+
timeout=effective_timeout
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if returncode == 0:
|
|
477
|
+
self.logger.info("GUI subprocess completed successfully (async)", extra={
|
|
478
|
+
"data": {
|
|
479
|
+
"pid": pid,
|
|
480
|
+
"command": " ".join(self.args),
|
|
481
|
+
"returncode": returncode
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
else:
|
|
485
|
+
self.logger.warning("GUI subprocess completed with non-zero exit code (async)", extra={
|
|
486
|
+
"data": {
|
|
487
|
+
"pid": pid,
|
|
488
|
+
"command": " ".join(self.args),
|
|
489
|
+
"returncode": returncode
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
except asyncio.TimeoutError:
|
|
494
|
+
# Kill the process on timeout
|
|
495
|
+
self.logger.warning("GUI subprocess timed out, killing process (async)", extra={
|
|
496
|
+
"data": {
|
|
497
|
+
"pid": pid,
|
|
498
|
+
"command": " ".join(self.args),
|
|
499
|
+
"timeout": effective_timeout
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
self._process.kill()
|
|
505
|
+
await _to_thread(self._process.wait)
|
|
506
|
+
except (OSError, subprocess.SubprocessError, BaseException):
|
|
507
|
+
pass
|
|
508
|
+
returncode = -1
|
|
509
|
+
|
|
510
|
+
self.logger.error("GUI subprocess killed due to timeout (async)", extra={
|
|
511
|
+
"data": {
|
|
512
|
+
"pid": pid,
|
|
513
|
+
"command": " ".join(self.args),
|
|
514
|
+
"timeout": effective_timeout
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
# Set timeout state and return early
|
|
519
|
+
end_time = datetime.now()
|
|
520
|
+
execution_time = (end_time - start_time).total_seconds()
|
|
521
|
+
|
|
522
|
+
result = CommandExecutionResult(
|
|
523
|
+
command=self,
|
|
524
|
+
success=False,
|
|
525
|
+
return_code=returncode,
|
|
526
|
+
stdout="", # GUI commands don't capture output
|
|
527
|
+
stderr="", # GUI commands don't capture output
|
|
528
|
+
execution_time=execution_time,
|
|
529
|
+
state=CommandState.TIMEOUT,
|
|
530
|
+
timeout_occurred=True,
|
|
531
|
+
start_time=start_time,
|
|
532
|
+
end_time=end_time,
|
|
533
|
+
pid=pid,
|
|
534
|
+
command_type=self.command_type
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
self._state = CommandState.TIMEOUT
|
|
538
|
+
self._result = result
|
|
539
|
+
|
|
540
|
+
if self.on_complete:
|
|
541
|
+
self.on_complete(self, result)
|
|
542
|
+
|
|
543
|
+
return result
|
|
544
|
+
|
|
545
|
+
except (OSError, ValueError, subprocess.SubprocessError) as e:
|
|
546
|
+
returncode = -1
|
|
547
|
+
self.logger.error("GUI subprocess execution failed (async)", extra={
|
|
548
|
+
"data": {
|
|
549
|
+
"command": " ".join(self.args),
|
|
550
|
+
"error": str(e),
|
|
551
|
+
"timeout": effective_timeout
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
end_time = datetime.now()
|
|
556
|
+
execution_time = (end_time - start_time).total_seconds()
|
|
557
|
+
success = returncode == 0
|
|
558
|
+
state = CommandState.COMPLETED if success else CommandState.FAILED
|
|
559
|
+
|
|
560
|
+
# Check if this was a timeout
|
|
561
|
+
timeout_occurred = False
|
|
562
|
+
if returncode == -1 and effective_timeout is not None and execution_time >= effective_timeout * 0.9:
|
|
563
|
+
state = CommandState.TIMEOUT
|
|
564
|
+
timeout_occurred = True
|
|
565
|
+
|
|
566
|
+
result = CommandExecutionResult(
|
|
567
|
+
command=self,
|
|
568
|
+
success=success,
|
|
569
|
+
return_code=returncode,
|
|
570
|
+
stdout="", # GUI commands don't capture output
|
|
571
|
+
stderr="", # GUI commands don't capture output
|
|
572
|
+
execution_time=execution_time,
|
|
573
|
+
state=state,
|
|
574
|
+
timeout_occurred=timeout_occurred,
|
|
575
|
+
start_time=start_time,
|
|
576
|
+
end_time=end_time,
|
|
577
|
+
pid=self._process.pid if self._process else None,
|
|
578
|
+
command_type=self.command_type
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
self._state = state
|
|
582
|
+
self._result = result
|
|
583
|
+
|
|
584
|
+
# Call appropriate callback based on result
|
|
585
|
+
if not success and self.on_error:
|
|
586
|
+
# Create a generic exception for non-zero return codes
|
|
587
|
+
error = RuntimeError(f"Command failed with return code {returncode}")
|
|
588
|
+
self.on_error(self, error)
|
|
589
|
+
elif self.on_complete:
|
|
590
|
+
self.on_complete(self, result)
|
|
591
|
+
|
|
592
|
+
return result
|
|
593
|
+
|
|
594
|
+
def kill(self) -> bool:
|
|
595
|
+
"""
|
|
596
|
+
Kill the running command.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
bool: True if the command was killed, False otherwise
|
|
600
|
+
"""
|
|
601
|
+
self.logger.info("Attempting to kill command", extra={
|
|
602
|
+
"data": {
|
|
603
|
+
"command": " ".join(self.args),
|
|
604
|
+
"state": self._state.value,
|
|
605
|
+
"has_process": self._process is not None
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
if not self._process or not self.is_running:
|
|
610
|
+
self.logger.warning("Attempted to kill command that is not running", extra={
|
|
611
|
+
"data": {
|
|
612
|
+
"command": " ".join(self.args),
|
|
613
|
+
"state": self._state.value,
|
|
614
|
+
"has_process": self._process is not None,
|
|
615
|
+
"is_running": self.is_running
|
|
616
|
+
}
|
|
617
|
+
})
|
|
618
|
+
return False
|
|
619
|
+
|
|
620
|
+
pid = self._process.pid
|
|
621
|
+
try:
|
|
622
|
+
self._process.kill()
|
|
623
|
+
self._state = CommandState.KILLED
|
|
624
|
+
|
|
625
|
+
self.logger.info("Command killed successfully", extra={
|
|
626
|
+
"data": {
|
|
627
|
+
"command": " ".join(self.args),
|
|
628
|
+
"pid": pid,
|
|
629
|
+
"state": self._state.value
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
return True
|
|
633
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
634
|
+
self.logger.error("Failed to kill command", extra={
|
|
635
|
+
"data": {
|
|
636
|
+
"command": " ".join(self.args),
|
|
637
|
+
"pid": pid,
|
|
638
|
+
"error": str(e),
|
|
639
|
+
"error_type": type(e).__name__
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
return False
|
|
643
|
+
|
|
644
|
+
def cleanup(self) -> None:
|
|
645
|
+
"""Clean up resources and ensure proper shutdown."""
|
|
646
|
+
if self._process:
|
|
647
|
+
try:
|
|
648
|
+
if hasattr(self._process, "poll"):
|
|
649
|
+
if self._process.poll() is None:
|
|
650
|
+
self._process.kill()
|
|
651
|
+
self._process.wait(timeout=1.0)
|
|
652
|
+
elif getattr(self._process, "returncode", None) is None:
|
|
653
|
+
self._process.kill()
|
|
654
|
+
if hasattr(self._process, "wait"):
|
|
655
|
+
self._process.wait()
|
|
656
|
+
except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
|
|
657
|
+
pass
|
|
658
|
+
finally:
|
|
659
|
+
self._process = None
|
|
660
|
+
|
|
661
|
+
async def wait(self) -> CommandExecutionResult:
|
|
662
|
+
"""
|
|
663
|
+
Wait for the command to complete.
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
CommandExecutionResult: The execution result
|
|
667
|
+
"""
|
|
668
|
+
self.logger.info("Waiting for command to complete", extra={
|
|
669
|
+
"data": {
|
|
670
|
+
"command": " ".join(self.args),
|
|
671
|
+
"state": self._state.value
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
if self._result:
|
|
676
|
+
self.logger.debug("Command already completed, returning cached result", extra={
|
|
677
|
+
"data": {
|
|
678
|
+
"command": " ".join(self.args),
|
|
679
|
+
"state": self._state.value,
|
|
680
|
+
"success": self._result.success
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
return self._result
|
|
684
|
+
|
|
685
|
+
# If not running, execute first
|
|
686
|
+
if self._state == CommandState.PENDING:
|
|
687
|
+
return await self.execute()
|
|
688
|
+
|
|
689
|
+
# Wait for completion
|
|
690
|
+
while not self.is_completed:
|
|
691
|
+
await asyncio.sleep(0.1)
|
|
692
|
+
|
|
693
|
+
self.logger.info("Command completed", extra={
|
|
694
|
+
"data": {
|
|
695
|
+
"command": " ".join(self.args),
|
|
696
|
+
"state": self._state.value,
|
|
697
|
+
"success": self._result.success if self._result else False
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
return self._result
|
|
702
|
+
|
|
703
|
+
def to_command_result(self) -> CommandResult:
|
|
704
|
+
"""Convert to CommandResult schema."""
|
|
705
|
+
if not self._result:
|
|
706
|
+
return CommandResult(
|
|
707
|
+
success=False,
|
|
708
|
+
return_code=-1,
|
|
709
|
+
stdout="",
|
|
710
|
+
stderr="Command not executed",
|
|
711
|
+
execution_time=0.0
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
return CommandResult(
|
|
715
|
+
success=self._result.success,
|
|
716
|
+
return_code=self._result.return_code,
|
|
717
|
+
stdout=self._result.stdout,
|
|
718
|
+
stderr=self._result.stderr,
|
|
719
|
+
execution_time=self._result.execution_time
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
def to_command_response(self) -> CommandResponse:
|
|
723
|
+
"""Convert to CommandResponse schema."""
|
|
724
|
+
if not self._result:
|
|
725
|
+
return CommandResponse(
|
|
726
|
+
success=False,
|
|
727
|
+
message="Command not executed",
|
|
728
|
+
data={}
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return CommandResponse(
|
|
732
|
+
success=self._result.success,
|
|
733
|
+
message="Command executed successfully" if self._result.success else "Command failed",
|
|
734
|
+
data={
|
|
735
|
+
"return_code": self._result.return_code,
|
|
736
|
+
"stdout": self._result.stdout,
|
|
737
|
+
"stderr": self._result.stderr,
|
|
738
|
+
"execution_time": self._result.execution_time,
|
|
739
|
+
"state": self._state.value,
|
|
740
|
+
"pid": self._result.pid
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
@staticmethod
|
|
745
|
+
def _parse_command_string(command: str) -> List[str]:
|
|
746
|
+
"""
|
|
747
|
+
Parse a command string into a list of arguments, handling quoted strings properly.
|
|
748
|
+
|
|
749
|
+
This method intelligently splits command strings while preserving spaces within
|
|
750
|
+
single or double quotes. It handles both single and double quotes correctly.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
command: The command string to parse
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
List[str]: List of parsed command arguments
|
|
757
|
+
|
|
758
|
+
Examples:
|
|
759
|
+
>>> _parse_command_string('echo "hello world"')
|
|
760
|
+
['echo', 'hello world']
|
|
761
|
+
>>> _parse_command_string("git commit -m 'fix bug'")
|
|
762
|
+
['git', 'commit', '-m', 'fix bug']
|
|
763
|
+
>>> _parse_command_string('cmd "arg with spaces" --flag value')
|
|
764
|
+
['cmd', 'arg with spaces', '--flag', 'value']
|
|
765
|
+
"""
|
|
766
|
+
if not command.strip():
|
|
767
|
+
return []
|
|
768
|
+
|
|
769
|
+
args = []
|
|
770
|
+
current_arg = ""
|
|
771
|
+
in_quotes = False
|
|
772
|
+
quote_char = None
|
|
773
|
+
|
|
774
|
+
i = 0
|
|
775
|
+
while i < len(command):
|
|
776
|
+
char = command[i]
|
|
777
|
+
|
|
778
|
+
if not in_quotes:
|
|
779
|
+
if char in ['"', "'"]:
|
|
780
|
+
# Start of quoted string
|
|
781
|
+
in_quotes = True
|
|
782
|
+
quote_char = char
|
|
783
|
+
elif char == ' ':
|
|
784
|
+
# Space outside quotes - end of current argument
|
|
785
|
+
if current_arg:
|
|
786
|
+
args.append(current_arg)
|
|
787
|
+
current_arg = ""
|
|
788
|
+
else:
|
|
789
|
+
# Regular character
|
|
790
|
+
current_arg += char
|
|
791
|
+
else:
|
|
792
|
+
if char == quote_char:
|
|
793
|
+
# End of quoted string
|
|
794
|
+
in_quotes = False
|
|
795
|
+
quote_char = None
|
|
796
|
+
else:
|
|
797
|
+
# Character inside quotes
|
|
798
|
+
current_arg += char
|
|
799
|
+
|
|
800
|
+
i += 1
|
|
801
|
+
|
|
802
|
+
# Add the last argument if there is one
|
|
803
|
+
if current_arg:
|
|
804
|
+
args.append(current_arg)
|
|
805
|
+
|
|
806
|
+
return args
|
|
807
|
+
|
|
808
|
+
@classmethod
|
|
809
|
+
def from_str(cls, command: str, **kwargs) -> 'AsyncCommand':
|
|
810
|
+
"""
|
|
811
|
+
Create a shell command.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
command: Shell command string
|
|
815
|
+
**kwargs: Additional arguments for AsyncCommand
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
AsyncCommand: Configured command instance
|
|
819
|
+
"""
|
|
820
|
+
kwargs['command_type'] = kwargs.get('command_type', CommandType.CLI)
|
|
821
|
+
if '"' in command or "'" in command:
|
|
822
|
+
cls.logger.warning(
|
|
823
|
+
"Command '%s' contains quotes which can lead to the command failing due to parsing. It is recommended to build explicitly using the constructor to be able to escape the quotes correctly.",
|
|
824
|
+
command)
|
|
825
|
+
return cls(
|
|
826
|
+
args=AsyncCommand._parse_command_string(command),
|
|
827
|
+
**kwargs
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
@classmethod
|
|
831
|
+
def cmd(cls, command: str, **kwargs) -> 'AsyncCommand':
|
|
832
|
+
"""Create a Windows CMD command with proper argument handling."""
|
|
833
|
+
stripped_command = command.strip()
|
|
834
|
+
# For cmd, we pass the command directly to /c without additional quoting
|
|
835
|
+
# The command will be parsed by cmd.exe itself
|
|
836
|
+
args = ["cmd", "/c", stripped_command]
|
|
837
|
+
return cls(args, **kwargs)
|
|
838
|
+
|
|
839
|
+
@classmethod
|
|
840
|
+
def powershell(cls, command: str, **kwargs) -> 'AsyncCommand':
|
|
841
|
+
"""Create a PowerShell command with proper argument handling."""
|
|
842
|
+
stripped_command = command.strip()
|
|
843
|
+
# For PowerShell, we pass the command directly to -Command without additional quoting
|
|
844
|
+
# PowerShell will handle the command parsing internally
|
|
845
|
+
args = ["powershell", "-Command", stripped_command]
|
|
846
|
+
return cls(args, **kwargs)
|
|
847
|
+
|
|
848
|
+
@classmethod
|
|
849
|
+
def wsl(cls, command: str, **kwargs) -> 'AsyncCommand':
|
|
850
|
+
"""Create a WSL command with proper argument handling."""
|
|
851
|
+
stripped_command = command.strip()
|
|
852
|
+
# For WSL, we pass the command directly to bash -c
|
|
853
|
+
args = ["wsl", "bash", "-c", stripped_command]
|
|
854
|
+
return cls(args, **kwargs)
|
|
855
|
+
|
|
856
|
+
def __await__(self):
|
|
857
|
+
"""Make AsyncCommand awaitable by delegating to execute()."""
|
|
858
|
+
return self.execute().__await__()
|
|
859
|
+
|
|
860
|
+
def __repr__(self) -> str:
|
|
861
|
+
"""String representation of the command."""
|
|
862
|
+
return f"AsyncCommand(args={self.args}, state={self._state.value})"
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
class CommonCommands:
|
|
866
|
+
@staticmethod
|
|
867
|
+
def kill_pid(pid: int, **kwargs) -> 'AsyncCommand':
|
|
868
|
+
return AsyncCommand.powershell(f'taskkill /PID {pid} /F', **kwargs)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
__all__ = [
|
|
872
|
+
"CommandResult",
|
|
873
|
+
"CommandResponse",
|
|
874
|
+
"CommandState",
|
|
875
|
+
"CommandType",
|
|
876
|
+
"CommandExecutionResult",
|
|
877
|
+
"AsyncCommand",
|
|
878
|
+
"CommonCommands"
|
|
879
|
+
]
|