Skip to content

Commit 7004773

Browse files
committed
Improve timeout handling in execution
1 parent 529ce1d commit 7004773

File tree

2 files changed

+35
-13
lines changed

2 files changed

+35
-13
lines changed

moatless/actions/run_python_script.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async def execute(self, args: ActionArguments, file_context: FileContext | None
107107
else:
108108
patch = None
109109

110-
output = await self.workspace.environment.execute(command, patch=patch, fail_on_error=True, timeout=args.timeout)
110+
output = await self.workspace.environment.execute(command, patch=patch, fail_on_error=True)
111111

112112
# Truncate output if it exceeds max_output_tokens
113113
truncated_output, was_truncated = self._truncate_output_by_tokens(output, args.max_output_tokens)

moatless/runtime/local.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import logging
33
import os
4+
import signal
45
from datetime import datetime
56
from pathlib import Path
67
from typing import Tuple
@@ -14,7 +15,7 @@
1415
from moatless.runtime.runtime import RuntimeEnvironment
1516
from moatless.storage.base import BaseStorage
1617
from moatless.testing.python.parser_registry import parse_log
17-
from moatless.testing.schema import TestResult
18+
from moatless.testing.schema import TestResult, TestStatus
1819
from moatless.context_data import current_node_id
1920
from unidiff import PatchSet
2021

@@ -163,8 +164,6 @@ async def run_tests(
163164

164165
# If no results were parsed but we ran a test (even if it timed out), create a basic result
165166
if not testbed_results:
166-
from moatless.testing.schema import TestResult, TestStatus
167-
168167
basic_result = TestResult(
169168
status=TestStatus.ERROR if timed_out else TestStatus.UNKNOWN,
170169
file_path=test_file,
@@ -307,27 +306,50 @@ async def _execute_command(
307306
env=os.environ.copy(),
308307
stdout=asyncio.subprocess.PIPE,
309308
stderr=asyncio.subprocess.STDOUT, # redirect stderr to stdout
309+
start_new_session=True # Create new process group for proper cleanup
310310
)
311311

312312
try:
313313
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=timeout)
314314
return stdout.decode(), process.returncode or 0
315315
except asyncio.TimeoutError:
316-
# Kill the process if it times out
317-
process.kill()
316+
# Kill the entire process group to ensure all child processes are terminated
318317
try:
319-
await process.wait()
320-
except:
318+
# Use process group ID (negative PID) to kill all processes in the group
319+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
320+
except ProcessLookupError:
321+
# Process already dead
321322
pass
323+
except AttributeError:
324+
# Fallback for systems without killpg
325+
process.kill()
326+
327+
try:
328+
await asyncio.wait_for(process.wait(), timeout=1.0)
329+
except asyncio.TimeoutError:
330+
# Force kill if still not dead
331+
try:
332+
process.kill()
333+
except Exception:
334+
pass
335+
except Exception:
336+
pass
337+
322338
# Return partial output if any was captured before timeout
323339
partial_output = ""
324340
if process.stdout:
325341
try:
326342
partial_data = await asyncio.wait_for(process.stdout.read(), timeout=1.0)
327343
partial_output = partial_data.decode()
328-
except:
344+
except Exception:
329345
pass
330-
return partial_output, -1 # Use -1 to indicate timeout
346+
347+
# Add timeout message to the output
348+
timeout_msg = f"\n\n[ERROR] Command execution timed out after {timeout} seconds\n"
349+
combined_output = partial_output + timeout_msg
350+
351+
logger.warning(f"Command timed out after {timeout} seconds: {command}")
352+
return combined_output, -1 # Use -1 to indicate timeout
331353

332354
async def _apply_patch(self, patch: str) -> bool:
333355
"""Apply a git patch to the repository."""
@@ -517,8 +539,8 @@ def make_eval_script_list_py(self) -> list:
517539
eval_commands += [
518540
f"git config --global --add safe.directory {repo_directory}", # for nonroot user
519541
f"cd {repo_directory}",
520-
f"which python",
521-
f"python --version",
542+
"which python",
543+
"python --version",
522544
# This is just informational, so we have a record
523545
"git status",
524546
"git show",
@@ -563,7 +585,7 @@ async def _save_execution_log(self, command: str, patch: str | None, output: str
563585
log_content = f"Command: {command}\n"
564586
log_content += f"Return Code: {return_code}\n"
565587
if patch:
566-
log_content += f"Patch Applied: Yes\n"
588+
log_content += "Patch Applied: Yes\n"
567589
log_content += f"Patch Content:\n{patch}\n"
568590
log_content += "=" * 80 + "\n"
569591
else:

0 commit comments

Comments
 (0)