2121import io .agentscope .core .tool .ToolCallParam ;
2222import java .io .BufferedReader ;
2323import java .io .IOException ;
24+ import java .io .InputStream ;
2425import java .io .InputStreamReader ;
2526import java .nio .charset .StandardCharsets ;
2627import java .time .Duration ;
2930import java .util .List ;
3031import java .util .Map ;
3132import java .util .Set ;
33+ import java .util .concurrent .Callable ;
3234import java .util .concurrent .ConcurrentHashMap ;
35+ import java .util .concurrent .ExecutionException ;
36+ import java .util .concurrent .ExecutorService ;
37+ import java .util .concurrent .Executors ;
38+ import java .util .concurrent .Future ;
39+ import java .util .concurrent .ThreadFactory ;
3340import java .util .concurrent .TimeUnit ;
3441import java .util .concurrent .TimeoutException ;
42+ import java .util .concurrent .atomic .AtomicInteger ;
3543import java .util .function .Function ;
3644import java .util .stream .Collectors ;
3745import org .slf4j .Logger ;
@@ -58,6 +66,28 @@ public class ShellCommandTool implements AgentTool {
5866 private static final Logger logger = LoggerFactory .getLogger (ShellCommandTool .class );
5967 private static final int DEFAULT_TIMEOUT = 300 ;
6068
69+ /**
70+ * Shared thread pool for asynchronous stream reading.
71+ * Uses cached thread pool for dynamic scaling with 60s idle timeout.
72+ * Daemon threads ensure they don't prevent JVM shutdown.
73+ */
74+ private static final ExecutorService STREAM_READER_POOL =
75+ Executors .newCachedThreadPool (
76+ new ThreadFactory () {
77+ private final AtomicInteger counter = new AtomicInteger (0 );
78+
79+ @ Override
80+ public Thread newThread (Runnable r ) {
81+ Thread t =
82+ new Thread (
83+ r ,
84+ "ShellCommand-StreamReader-"
85+ + counter .incrementAndGet ());
86+ t .setDaemon (true ); // Daemon thread won't prevent JVM shutdown
87+ return t ;
88+ }
89+ });
90+
6191 private final Set <String > allowedCommands ;
6292 private final Function <String , Boolean > approvalCallback ;
6393 private final CommandValidator commandValidator ;
@@ -337,13 +367,31 @@ private ToolResultBlock executeCommand(String command, int timeoutSeconds) {
337367 }
338368
339369 Process process = null ;
370+ Future <String > stdoutFuture = null ;
371+ Future <String > stderrFuture = null ;
372+
340373 try {
341374 long startTime = System .currentTimeMillis ();
342375 logger .debug ("Starting command execution: {}" , command );
343376
344377 // Start the process
345378 process = processBuilder .start ();
346379
380+ // CRITICAL FIX: Start asynchronous stream readers immediately to prevent pipe buffer
381+ // deadlock
382+ // When a child process writes more data than the pipe buffer can hold (typically
383+ // 4-64KB),
384+ // it will block waiting for the parent process to read the data. If the parent is
385+ // blocked
386+ // in waitFor(), this creates a deadlock. By reading streams asynchronously in
387+ // background
388+ // threads from the thread pool, we ensure the pipe buffers are continuously drained,
389+ // preventing the deadlock.
390+ stdoutFuture =
391+ STREAM_READER_POOL .submit (new StreamReader (process .getInputStream (), "stdout" ));
392+ stderrFuture =
393+ STREAM_READER_POOL .submit (new StreamReader (process .getErrorStream (), "stderr" ));
394+
347395 // Wait for the process to complete with timeout
348396 logger .debug ("Waiting for process with timeout: {} seconds" , timeoutSeconds );
349397 boolean completed = process .waitFor (timeoutSeconds , TimeUnit .SECONDS );
@@ -359,13 +407,13 @@ private ToolResultBlock executeCommand(String command, int timeoutSeconds) {
359407 timeoutSeconds ,
360408 waitElapsed );
361409
362- // Try to capture partial output before terminating
363- String stdout = readStream (process .getInputStream ());
364- String stderr = readStream (process .getErrorStream ());
365-
366410 // Terminate the process
367411 process .destroyForcibly ();
368412
413+ // Get partial output from async readers with short timeout
414+ String stdout = getOutputWithTimeout (stdoutFuture , 1 , TimeUnit .SECONDS );
415+ String stderr = getOutputWithTimeout (stderrFuture , 1 , TimeUnit .SECONDS );
416+
369417 String timeoutMessage =
370418 String .format (
371419 "TimeoutError: The command execution exceeded the timeout of %d"
@@ -384,8 +432,11 @@ private ToolResultBlock executeCommand(String command, int timeoutSeconds) {
384432
385433 // Process completed normally
386434 int returnCode = process .exitValue ();
387- String stdout = readStream (process .getInputStream ());
388- String stderr = readStream (process .getErrorStream ());
435+
436+ // Get complete output from async readers
437+ // Process has finished, so readers should complete quickly
438+ String stdout = getOutputWithTimeout (stdoutFuture , 5 , TimeUnit .SECONDS );
439+ String stderr = getOutputWithTimeout (stderrFuture , 5 , TimeUnit .SECONDS );
389440
390441 logger .debug ("Command '{}' completed with return code: {}" , command , returnCode );
391442
@@ -411,37 +462,40 @@ private ToolResultBlock executeCommand(String command, int timeoutSeconds) {
411462 // Clean up process resources
412463 if (process != null && process .isAlive ()) {
413464 // Destroy the process if still alive
414- // Note: Streams are already closed by try-with-resources in readStream()
465+ // Note: Streams are already closed by try-with-resources in StreamReader
415466 process .destroyForcibly ();
416467 }
417468 }
418469 }
419470
420471 /**
421- * Read all content from an input stream .
472+ * Get output from a Future with timeout .
422473 *
423- * @param inputStream The input stream to read from
424- * @return The content as a string
474+ * <p>This helper method safely retrieves the output from an asynchronous stream reader,
475+ * with proper timeout and error handling. If the future times out or fails, it will be
476+ * cancelled and an empty string will be returned.
477+ *
478+ * @param future The Future containing the output string
479+ * @param timeout The timeout value
480+ * @param unit The timeout unit
481+ * @return The output string, or empty string if timeout or error occurs
425482 */
426- private String readStream (java .io .InputStream inputStream ) {
427- if (inputStream == null ) {
483+ private String getOutputWithTimeout (Future <String > future , long timeout , TimeUnit unit ) {
484+ try {
485+ return future .get (timeout , unit );
486+ } catch (TimeoutException e ) {
487+ logger .warn ("Timeout waiting for stream reader to complete" );
488+ future .cancel (true ); // Cancel the task
489+ return "" ;
490+ } catch (InterruptedException e ) {
491+ Thread .currentThread ().interrupt ();
492+ logger .warn ("Interrupted while waiting for stream reader" );
493+ future .cancel (true ); // Cancel the task
494+ return "" ;
495+ } catch (ExecutionException e ) {
496+ logger .error ("Error in stream reader: {}" , e .getCause ().getMessage (), e .getCause ());
428497 return "" ;
429498 }
430-
431- StringBuilder output = new StringBuilder ();
432- try (BufferedReader reader =
433- new BufferedReader (new InputStreamReader (inputStream , StandardCharsets .UTF_8 ))) {
434- String line ;
435- while ((line = reader .readLine ()) != null ) {
436- if (output .length () > 0 ) {
437- output .append ("\n " );
438- }
439- output .append (line );
440- }
441- } catch (IOException e ) {
442- logger .error ("Error reading stream: {}" , e .getMessage (), e );
443- }
444- return output .toString ();
445499 }
446500
447501 /**
@@ -491,4 +545,44 @@ private boolean requestUserApproval(String command) {
491545 return false ;
492546 }
493547 }
548+
549+ /**
550+ * Callable task for reading process output streams asynchronously.
551+ * This prevents pipe buffer deadlock by continuously draining stdout/stderr.
552+ */
553+ private static class StreamReader implements Callable <String > {
554+ private final InputStream inputStream ;
555+ private final String streamType ;
556+
557+ StreamReader (InputStream inputStream , String streamType ) {
558+ this .inputStream = inputStream ;
559+ this .streamType = streamType ;
560+ }
561+
562+ @ Override
563+ public String call () throws Exception {
564+ if (inputStream == null ) {
565+ return "" ;
566+ }
567+
568+ logger .debug ("StreamReader [{}] started" , streamType );
569+ StringBuilder output = new StringBuilder ();
570+ try (BufferedReader reader =
571+ new BufferedReader (
572+ new InputStreamReader (inputStream , StandardCharsets .UTF_8 ))) {
573+ String line ;
574+ while ((line = reader .readLine ()) != null ) {
575+ if (output .length () > 0 ) {
576+ output .append ("\n " );
577+ }
578+ output .append (line );
579+ }
580+ } catch (IOException e ) {
581+ logger .error ("Error reading {} stream: {}" , streamType , e .getMessage (), e );
582+ throw e ;
583+ }
584+ logger .debug ("StreamReader [{}] completed, read {} bytes" , streamType , output .length ());
585+ return output .toString ();
586+ }
587+ }
494588}
0 commit comments