|
18 | 18 |
|
19 | 19 | import asyncio |
20 | 20 | import dataclasses |
| 21 | +import logging |
| 22 | +import os |
21 | 23 | import pathlib |
22 | 24 | import shlex |
| 25 | +import signal |
23 | 26 | from typing import Any |
24 | 27 | from typing import Optional |
25 | 28 |
|
@@ -132,26 +135,74 @@ async def run_async( |
132 | 135 | elif not tool_context.tool_confirmation.confirmed: |
133 | 136 | return {"error": "This tool call is rejected."} |
134 | 137 |
|
135 | | - process = await asyncio.create_subprocess_exec( |
136 | | - *shlex.split(command), |
137 | | - cwd=str(self._workspace), |
138 | | - stdout=asyncio.subprocess.PIPE, |
139 | | - stderr=asyncio.subprocess.PIPE, |
140 | | - ) |
| 138 | + stdout = None |
| 139 | + stderr = None |
141 | 140 | try: |
142 | | - stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30) |
| 141 | + process = await asyncio.create_subprocess_exec( |
| 142 | + *shlex.split(command), |
| 143 | + cwd=str(self._workspace), |
| 144 | + stdout=asyncio.subprocess.PIPE, |
| 145 | + stderr=asyncio.subprocess.PIPE, |
| 146 | + start_new_session=True, |
| 147 | + ) |
| 148 | + |
| 149 | + try: |
| 150 | + stdout, stderr = await asyncio.wait_for( |
| 151 | + process.communicate(), timeout=30 |
| 152 | + ) |
| 153 | + except asyncio.TimeoutError: |
| 154 | + try: |
| 155 | + os.killpg(process.pid, signal.SIGKILL) |
| 156 | + except ProcessLookupError: |
| 157 | + pass |
| 158 | + stdout, stderr = await process.communicate() |
| 159 | + return { |
| 160 | + "error": "Command timed out after 30 seconds.", |
| 161 | + "stdout": ( |
| 162 | + stdout.decode(errors="replace") |
| 163 | + if stdout |
| 164 | + else "<no stdout captured>" |
| 165 | + ), |
| 166 | + "stderr": ( |
| 167 | + stderr.decode(errors="replace") |
| 168 | + if stderr |
| 169 | + else "<no stderr captured>" |
| 170 | + ), |
| 171 | + "returncode": process.returncode, |
| 172 | + } |
| 173 | + finally: |
| 174 | + try: |
| 175 | + if process.pid: |
| 176 | + os.killpg(process.pid, signal.SIGKILL) |
| 177 | + except ProcessLookupError: |
| 178 | + pass |
| 179 | + |
143 | 180 | return { |
144 | 181 | "stdout": ( |
145 | | - stdout.decode() if stdout is not None else "<No stdout captured>" |
| 182 | + stdout.decode(errors="replace") |
| 183 | + if stdout |
| 184 | + else "<no stdout captured>" |
146 | 185 | ), |
147 | 186 | "stderr": ( |
148 | | - stderr.decode() if stderr is not None else "<No stderr captured>" |
| 187 | + stderr.decode(errors="replace") |
| 188 | + if stderr |
| 189 | + else "<no stderr captured>" |
149 | 190 | ), |
150 | 191 | "returncode": process.returncode, |
151 | 192 | } |
152 | | - except asyncio.TimeoutError: |
153 | | - try: |
154 | | - process.kill() |
155 | | - except ProcessLookupError: |
156 | | - pass |
157 | | - return {"error": "Command timed out after 30 seconds."} |
| 193 | + except Exception as e: # pylint: disable=broad-except |
| 194 | + logger = logging.getLogger("google_adk." + __name__) |
| 195 | + logger.exception("ExecuteBashTool execution failed") |
| 196 | + |
| 197 | + stdout_res = ( |
| 198 | + stdout.decode(errors="replace") if stdout else "<no stdout captured>" |
| 199 | + ) |
| 200 | + stderr_res = ( |
| 201 | + stderr.decode(errors="replace") if stderr else "<no stderr captured>" |
| 202 | + ) |
| 203 | + |
| 204 | + return { |
| 205 | + "error": f"Execution failed: {str(e)}", |
| 206 | + "stdout": stdout_res, |
| 207 | + "stderr": stderr_res, |
| 208 | + } |
0 commit comments