waldiez 0.5.8__py3-none-any.whl → 0.5.10__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 waldiez might be problematic. Click here for more details.
- waldiez/_version.py +1 -1
- waldiez/cli.py +112 -24
- waldiez/exporting/agent/exporter.py +3 -0
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +2 -2
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +6 -7
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/_ws.py +13 -5
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/user_input.py +4 -4
- waldiez/io/models/user_response.py +1 -0
- waldiez/io/mqtt.py +1 -1
- waldiez/io/structured.py +17 -17
- waldiez/io/utils.py +1 -1
- waldiez/io/ws.py +9 -11
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +28 -14
- waldiez/models/model/model.py +4 -1
- waldiez/models/model/model_data.py +18 -5
- waldiez/models/tool/predefined/_config.py +5 -1
- waldiez/models/tool/predefined/_duckduckgo.py +4 -0
- waldiez/models/tool/predefined/_email.py +474 -0
- waldiez/models/tool/predefined/_google.py +8 -6
- waldiez/models/tool/predefined/_perplexity.py +3 -0
- waldiez/models/tool/predefined/_searxng.py +3 -0
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +4 -1
- waldiez/models/tool/predefined/_youtube.py +4 -1
- waldiez/models/tool/predefined/protocol.py +3 -0
- waldiez/models/tool/tool.py +22 -4
- waldiez/models/waldiez.py +12 -0
- waldiez/runner.py +37 -54
- waldiez/running/__init__.py +6 -0
- waldiez/running/base_runner.py +310 -353
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +4 -4
- waldiez/running/pre_run.py +51 -40
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +84 -277
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
- waldiez/running/step_by_step/step_by_step_models.py +224 -0
- waldiez/running/step_by_step/step_by_step_runner.py +745 -0
- waldiez/running/subprocess_runner/__base__.py +282 -0
- waldiez/running/subprocess_runner/__init__.py +16 -0
- waldiez/running/subprocess_runner/_async_runner.py +362 -0
- waldiez/running/subprocess_runner/_sync_runner.py +455 -0
- waldiez/running/subprocess_runner/runner.py +561 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +376 -1
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +70 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +201 -0
- waldiez/ws/cli.py +211 -0
- waldiez/ws/client_manager.py +835 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +971 -0
- waldiez/ws/reloader.py +342 -0
- waldiez/ws/server.py +469 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +385 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
# pylint: disable=no-self-use,unused-argument,logging-fstring-interpolation
|
|
4
|
+
# pylint: disable=too-many-try-statements,broad-exception-caught,duplicate-code
|
|
5
|
+
# flake8: noqa: G004
|
|
6
|
+
"""Sync subprocess runner for Waldiez workflows."""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import queue
|
|
10
|
+
import subprocess
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Callable, Literal
|
|
15
|
+
|
|
16
|
+
from .__base__ import BaseSubprocessRunner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# noinspection PyUnusedLocal
|
|
20
|
+
class SyncSubprocessRunner(BaseSubprocessRunner):
|
|
21
|
+
"""Sync subprocess runner for Waldiez workflows using threading."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
on_output: Callable[[dict[str, Any]], None],
|
|
26
|
+
on_input_request: Callable[[str], None],
|
|
27
|
+
input_timeout: float = 120.0,
|
|
28
|
+
uploads_root: str | Path | None = None,
|
|
29
|
+
dot_env: str | Path | None = None,
|
|
30
|
+
logger: logging.Logger | None = None,
|
|
31
|
+
**kwargs: Any,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Initialize sync subprocess runner.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
on_output : Callable[[dict], None]
|
|
38
|
+
Callback for handling output messages
|
|
39
|
+
on_input_request : Callable[[str], None]
|
|
40
|
+
Callback for handling input requests
|
|
41
|
+
input_timeout : float
|
|
42
|
+
Timeout for user input
|
|
43
|
+
uploads_root : str | Path | None
|
|
44
|
+
Root directory for uploads
|
|
45
|
+
dot_env : str | Path | None
|
|
46
|
+
Path to .env file
|
|
47
|
+
logger : logging.Logger | None
|
|
48
|
+
Logger instance
|
|
49
|
+
**kwargs : Any
|
|
50
|
+
Additional arguments passed to base class
|
|
51
|
+
"""
|
|
52
|
+
super().__init__(
|
|
53
|
+
input_timeout=input_timeout,
|
|
54
|
+
**kwargs,
|
|
55
|
+
)
|
|
56
|
+
self.waiting_for_input = False
|
|
57
|
+
self.on_output = on_output
|
|
58
|
+
self.on_input_request = on_input_request
|
|
59
|
+
self.process: subprocess.Popen[Any] | None = None
|
|
60
|
+
self.input_queue: queue.Queue[str] = queue.Queue()
|
|
61
|
+
self.output_queue: queue.Queue[dict[str, Any]] = queue.Queue()
|
|
62
|
+
self._stop_event = threading.Event()
|
|
63
|
+
self._monitor_threads: list[threading.Thread] = []
|
|
64
|
+
|
|
65
|
+
def run_subprocess(
|
|
66
|
+
self,
|
|
67
|
+
flow_path: Path,
|
|
68
|
+
mode: Literal["debug", "run"],
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""Run subprocess with the given flow data.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
flow_path : Path
|
|
75
|
+
Path to the waldiez flow file
|
|
76
|
+
mode : Literal["debug", "run"]
|
|
77
|
+
Execution mode ('debug', 'run')
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
bool
|
|
82
|
+
True if subprocess completed successfully, False otherwise
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
# Build command
|
|
86
|
+
cmd = self.build_command(flow_path, mode=mode)
|
|
87
|
+
self.log_subprocess_start(cmd)
|
|
88
|
+
|
|
89
|
+
# Start subprocess
|
|
90
|
+
# pylint: disable=consider-using-with
|
|
91
|
+
self.process = subprocess.Popen(
|
|
92
|
+
cmd,
|
|
93
|
+
stdin=subprocess.PIPE,
|
|
94
|
+
stdout=subprocess.PIPE,
|
|
95
|
+
stderr=subprocess.PIPE,
|
|
96
|
+
text=True,
|
|
97
|
+
bufsize=1, # Line buffered
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Start monitoring threads
|
|
101
|
+
self._start_monitoring()
|
|
102
|
+
|
|
103
|
+
# Wait for completion
|
|
104
|
+
exit_code = self.process.wait()
|
|
105
|
+
self.log_subprocess_end(exit_code)
|
|
106
|
+
|
|
107
|
+
# Send completion message
|
|
108
|
+
completion_msg = self.create_completion_message(
|
|
109
|
+
success=exit_code == 0, exit_code=exit_code
|
|
110
|
+
)
|
|
111
|
+
self.on_output(completion_msg)
|
|
112
|
+
|
|
113
|
+
return exit_code == 0
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
self.logger.error(f"Error running subprocess: {e}")
|
|
117
|
+
error_msg = self.create_output_message(
|
|
118
|
+
f"Subprocess error: {str(e)}",
|
|
119
|
+
stream="stderr",
|
|
120
|
+
msg_type="error",
|
|
121
|
+
)
|
|
122
|
+
self.on_output(error_msg)
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
finally:
|
|
126
|
+
self._cleanup()
|
|
127
|
+
|
|
128
|
+
def provide_user_input(self, user_input: str) -> None:
|
|
129
|
+
"""Provide user input response.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
user_input : str
|
|
134
|
+
User's input response
|
|
135
|
+
"""
|
|
136
|
+
if self.waiting_for_input:
|
|
137
|
+
try:
|
|
138
|
+
self.input_queue.put(user_input, timeout=1.0)
|
|
139
|
+
self.logger.debug(f"User input queued: {user_input}")
|
|
140
|
+
except queue.Full:
|
|
141
|
+
self.logger.warning("Input queue is full, dropping input")
|
|
142
|
+
except Exception as e: # pragma: no cover
|
|
143
|
+
self.logger.error(f"Error queuing user input: {e}")
|
|
144
|
+
|
|
145
|
+
def stop(self) -> None:
|
|
146
|
+
"""Stop the subprocess."""
|
|
147
|
+
self._cleanup()
|
|
148
|
+
|
|
149
|
+
def _start_monitoring(self) -> None:
|
|
150
|
+
"""Start monitoring threads for subprocess I/O."""
|
|
151
|
+
if not self.process: # pragma: no cover
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Reset stop event
|
|
155
|
+
self._stop_event.clear()
|
|
156
|
+
|
|
157
|
+
# Create and start monitoring threads
|
|
158
|
+
self._monitor_threads = [
|
|
159
|
+
threading.Thread(target=self._read_stdout, daemon=True),
|
|
160
|
+
threading.Thread(target=self._read_stderr, daemon=True),
|
|
161
|
+
threading.Thread(target=self._send_queued_messages, daemon=True),
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
for thread in self._monitor_threads:
|
|
165
|
+
thread.start()
|
|
166
|
+
|
|
167
|
+
def _read_stdout(self) -> None:
|
|
168
|
+
"""Read and handle stdout from subprocess."""
|
|
169
|
+
if not self.process or not self.process.stdout: # pragma: no cover
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
while not self._stop_event.is_set() and self.process.poll() is None:
|
|
174
|
+
try:
|
|
175
|
+
# Use readline with timeout simulation
|
|
176
|
+
if self.process.stdout.readable(): # pragma: no branch
|
|
177
|
+
line = self.process.stdout.readline()
|
|
178
|
+
|
|
179
|
+
if not line: # pragma: no cover
|
|
180
|
+
time.sleep(0.1)
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
line_str = (
|
|
184
|
+
line.strip()
|
|
185
|
+
if isinstance(line, str)
|
|
186
|
+
else str(line).strip()
|
|
187
|
+
)
|
|
188
|
+
if line_str: # pragma: no branch
|
|
189
|
+
self._handle_stdout_line(line_str)
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
self.logger.error(f"Error reading stdout: {e}")
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
time.sleep(0.01) # Small delay to prevent busy waiting
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self.logger.error(f"Error in stdout reader: {e}")
|
|
199
|
+
|
|
200
|
+
# pylint: disable=too-complex,too-many-nested-blocks
|
|
201
|
+
def _read_stderr(self) -> None: # noqa: C901
|
|
202
|
+
"""Read and handle stderr from subprocess."""
|
|
203
|
+
if not self.process or not self.process.stderr: # pragma: no cover
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
while not self._stop_event.is_set() and self.process.poll() is None:
|
|
208
|
+
try:
|
|
209
|
+
# Use readline with timeout simulation
|
|
210
|
+
if self.process.stderr.readable(): # pragma: no branch
|
|
211
|
+
line = self.process.stderr.readline()
|
|
212
|
+
|
|
213
|
+
if not line:
|
|
214
|
+
time.sleep(0.1)
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
line_str = (
|
|
218
|
+
line.strip()
|
|
219
|
+
if isinstance(line, str)
|
|
220
|
+
else str(line).strip()
|
|
221
|
+
)
|
|
222
|
+
if line_str:
|
|
223
|
+
error_msg = self.create_output_message(
|
|
224
|
+
line_str,
|
|
225
|
+
stream="stderr",
|
|
226
|
+
msg_type="error",
|
|
227
|
+
)
|
|
228
|
+
try:
|
|
229
|
+
self.output_queue.put(error_msg, timeout=1.0)
|
|
230
|
+
except queue.Full:
|
|
231
|
+
self.logger.warning(
|
|
232
|
+
"Output queue full, dropping stderr message"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
self.logger.error(f"Error reading stderr: {e}")
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
time.sleep(0.01) # Small delay to prevent busy waiting
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
self.logger.error(f"Error in stderr reader: {e}")
|
|
243
|
+
|
|
244
|
+
def _send_queued_messages(self) -> None:
|
|
245
|
+
"""Send queued messages to output callback."""
|
|
246
|
+
try:
|
|
247
|
+
while not self._stop_event.is_set():
|
|
248
|
+
try:
|
|
249
|
+
message = self.output_queue.get(timeout=1.0)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
self.on_output(message)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.logger.error(f"Error in output callback: {e}")
|
|
255
|
+
|
|
256
|
+
self.output_queue.task_done()
|
|
257
|
+
|
|
258
|
+
except queue.Empty:
|
|
259
|
+
continue
|
|
260
|
+
except Exception as e:
|
|
261
|
+
self.logger.error(f"Error sending queued message: {e}")
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
self.logger.error(f"Error in message sender: {e}")
|
|
266
|
+
|
|
267
|
+
def _handle_stdout_line(self, line: str) -> None:
|
|
268
|
+
"""Handle a line from stdout.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
line : str
|
|
273
|
+
Decoded line from stdout
|
|
274
|
+
"""
|
|
275
|
+
# Try to parse as structured JSON first
|
|
276
|
+
parsed_data = self.parse_output(line, stream="stdout")
|
|
277
|
+
if not parsed_data:
|
|
278
|
+
return
|
|
279
|
+
if parsed_data.get("type") in ("input_request", "debug_input_request"):
|
|
280
|
+
prompt = parsed_data.get("prompt", "> ")
|
|
281
|
+
self.waiting_for_input = True
|
|
282
|
+
self.on_input_request(prompt)
|
|
283
|
+
self._handle_input_request(parsed_data)
|
|
284
|
+
else:
|
|
285
|
+
# Forward message to output
|
|
286
|
+
try:
|
|
287
|
+
self.output_queue.put(parsed_data, timeout=1.0)
|
|
288
|
+
except queue.Full:
|
|
289
|
+
self.logger.warning(
|
|
290
|
+
"Output queue full, dropping structured message"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _handle_input_request(self, data: dict[str, Any]) -> None:
|
|
294
|
+
"""Handle input request from subprocess.
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
data : dict[str, Any]
|
|
299
|
+
Input request data
|
|
300
|
+
"""
|
|
301
|
+
response_type = str(data.get("type", "input_request")).replace(
|
|
302
|
+
"request", "response"
|
|
303
|
+
)
|
|
304
|
+
request_id = data.get("request_id")
|
|
305
|
+
try:
|
|
306
|
+
# Wait for user response
|
|
307
|
+
user_input = self.input_queue.get(timeout=self.input_timeout)
|
|
308
|
+
|
|
309
|
+
# Create response
|
|
310
|
+
response = self.create_input_response(
|
|
311
|
+
response_type=response_type,
|
|
312
|
+
user_input=user_input,
|
|
313
|
+
request_id=request_id,
|
|
314
|
+
)
|
|
315
|
+
# Send response to subprocess
|
|
316
|
+
if self.process and self.process.stdin:
|
|
317
|
+
self.process.stdin.write(response)
|
|
318
|
+
self.process.stdin.flush()
|
|
319
|
+
|
|
320
|
+
self.logger.debug(f"Sent input response: {user_input}")
|
|
321
|
+
|
|
322
|
+
except queue.Empty:
|
|
323
|
+
self.logger.warning("Input request timed out")
|
|
324
|
+
# Send empty response on timeout
|
|
325
|
+
if self.process and self.process.stdin:
|
|
326
|
+
response = self.create_input_response(
|
|
327
|
+
response_type=response_type,
|
|
328
|
+
user_input="",
|
|
329
|
+
request_id=request_id,
|
|
330
|
+
)
|
|
331
|
+
self.process.stdin.write(response)
|
|
332
|
+
self.process.stdin.flush()
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
self.logger.error(f"Error handling input request: {e}")
|
|
336
|
+
|
|
337
|
+
finally:
|
|
338
|
+
self.waiting_for_input = False
|
|
339
|
+
|
|
340
|
+
def _cleanup_threads(self) -> None:
|
|
341
|
+
"""Cleanup threads."""
|
|
342
|
+
self._stop_event.set()
|
|
343
|
+
|
|
344
|
+
# Wait for threads to finish
|
|
345
|
+
for thread in self._monitor_threads:
|
|
346
|
+
if thread.is_alive():
|
|
347
|
+
thread.join(timeout=2.0)
|
|
348
|
+
if thread.is_alive():
|
|
349
|
+
self.logger.warning(
|
|
350
|
+
f"Thread {thread.name} did not stop gracefully"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# noinspection TryExceptPass,PyBroadException
|
|
354
|
+
def _cleanup_process(self) -> None:
|
|
355
|
+
"""Cleanup process resources."""
|
|
356
|
+
if self.process:
|
|
357
|
+
if self.process.stdin:
|
|
358
|
+
try:
|
|
359
|
+
self.process.stdin.close()
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
if self.process.stdout:
|
|
364
|
+
try:
|
|
365
|
+
self.process.stdout.close()
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
if self.process.stderr:
|
|
370
|
+
try:
|
|
371
|
+
self.process.stderr.close()
|
|
372
|
+
except Exception:
|
|
373
|
+
pass
|
|
374
|
+
self.logger.debug("Terminating subprocess")
|
|
375
|
+
try:
|
|
376
|
+
self.process.terminate()
|
|
377
|
+
self.process.wait(timeout=2.0)
|
|
378
|
+
except (TimeoutError, subprocess.TimeoutExpired):
|
|
379
|
+
self.logger.warning("Force killing subprocess")
|
|
380
|
+
self.process.kill()
|
|
381
|
+
self.process.wait()
|
|
382
|
+
except BaseException as e:
|
|
383
|
+
self.logger.error(f"Error stopping subprocess: {e}")
|
|
384
|
+
finally:
|
|
385
|
+
self.process = None
|
|
386
|
+
|
|
387
|
+
def _cleanup_queues(self) -> None:
|
|
388
|
+
"""Cleanup input and output queues."""
|
|
389
|
+
try:
|
|
390
|
+
while not self.input_queue.empty():
|
|
391
|
+
self.input_queue.get_nowait()
|
|
392
|
+
except queue.Empty:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
while not self.output_queue.empty():
|
|
397
|
+
self.output_queue.get_nowait()
|
|
398
|
+
except queue.Empty:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
def _cleanup(self) -> None:
|
|
402
|
+
"""Cleanup resources."""
|
|
403
|
+
self._stop_event.set()
|
|
404
|
+
self._cleanup_process()
|
|
405
|
+
self._cleanup_threads()
|
|
406
|
+
self._monitor_threads.clear()
|
|
407
|
+
self.waiting_for_input = False
|
|
408
|
+
self._cleanup_queues()
|
|
409
|
+
|
|
410
|
+
def is_running(self) -> bool:
|
|
411
|
+
"""Check if subprocess is currently running.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
bool
|
|
416
|
+
True if subprocess is running, False otherwise
|
|
417
|
+
"""
|
|
418
|
+
return self.process is not None and self.process.poll() is None
|
|
419
|
+
|
|
420
|
+
def get_exit_code(self) -> int | None:
|
|
421
|
+
"""Get the exit code of the subprocess.
|
|
422
|
+
|
|
423
|
+
Returns
|
|
424
|
+
-------
|
|
425
|
+
int | None
|
|
426
|
+
Exit code if process has finished, None if still running
|
|
427
|
+
"""
|
|
428
|
+
if self.process is None:
|
|
429
|
+
return None
|
|
430
|
+
return self.process.poll()
|
|
431
|
+
|
|
432
|
+
def wait_for_completion(self, timeout: float | None = None) -> int:
|
|
433
|
+
"""Wait for subprocess to complete.
|
|
434
|
+
|
|
435
|
+
Parameters
|
|
436
|
+
----------
|
|
437
|
+
timeout : float | None
|
|
438
|
+
Maximum time to wait in seconds. None means wait indefinitely.
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
int
|
|
443
|
+
Exit code of the subprocess
|
|
444
|
+
|
|
445
|
+
Raises
|
|
446
|
+
------
|
|
447
|
+
subprocess.TimeoutExpired
|
|
448
|
+
If timeout is reached before subprocess completes
|
|
449
|
+
RuntimeError
|
|
450
|
+
If no subprocess is running
|
|
451
|
+
"""
|
|
452
|
+
if not self.process:
|
|
453
|
+
raise RuntimeError("No subprocess is running")
|
|
454
|
+
|
|
455
|
+
return self.process.wait(timeout=timeout)
|