From 0f5db9a7ffb3e9ea8b64a760bacad9b171d88b31 Mon Sep 17 00:00:00 2001 From: Hua Date: Sun, 15 Mar 2026 19:13:22 +0800 Subject: [PATCH] fix(exec): avoid flaky async subprocess timeouts --- nanobot/agent/tools/shell.py | 45 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index bf1b082..f69d942 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -3,6 +3,8 @@ import asyncio import os import re +import subprocess +import tempfile from pathlib import Path from typing import Any @@ -91,26 +93,31 @@ class ExecTool(Tool): env["PATH"] = env.get("PATH", "") + os.pathsep + self.path_append try: - process = await asyncio.create_subprocess_shell( - command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=cwd, - env=env, - ) - - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), - timeout=effective_timeout, + with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file: + process = subprocess.Popen( + command, + stdout=stdout_file, + stderr=stderr_file, + cwd=cwd, + env=env, + shell=True, ) - except asyncio.TimeoutError: - process.kill() - try: - await asyncio.wait_for(process.wait(), timeout=5.0) - except asyncio.TimeoutError: - pass - return f"Error: Command timed out after {effective_timeout} seconds" + + deadline = asyncio.get_running_loop().time() + effective_timeout + while process.poll() is None: + if asyncio.get_running_loop().time() >= deadline: + process.kill() + try: + process.wait(timeout=5.0) + except subprocess.TimeoutExpired: + pass + return f"Error: Command timed out after {effective_timeout} seconds" + await asyncio.sleep(0.05) + + stdout_file.seek(0) + stderr_file.seek(0) + stdout = stdout_file.read() + stderr = stderr_file.read() output_parts = []