@@ -89,11 +89,13 @@ private async Task<ProcessExecutionResult> RunDirectAsync(
8989 Task < string > standardOutputTask = ReadToEndCappedAsync (
9090 process . StandardOutput ,
9191 request . MaxOutputCharacters ,
92- cancellationToken ) ;
92+ cancellationToken ,
93+ request . OnOutputLine ) ;
9394 Task < string > standardErrorTask = ReadToEndCappedAsync (
9495 process . StandardError ,
9596 request . MaxOutputCharacters ,
96- cancellationToken ) ;
97+ cancellationToken ,
98+ request . OnOutputLine ) ;
9799
98100 if ( request . StandardInput is not null )
99101 {
@@ -552,20 +554,20 @@ private static void TryKillProcess(Process process)
552554 private static async Task < string > ReadToEndCappedAsync (
553555 TextReader reader ,
554556 int ? maxCharacters ,
555- CancellationToken cancellationToken )
557+ CancellationToken cancellationToken ,
558+ Action < string > ? onLine = null )
556559 {
557560 const int BufferSize = 4096 ;
558561
559- if ( maxCharacters is <= 0 )
560- {
561- await DrainAsync ( reader , cancellationToken ) ;
562- return string . Empty ;
563- }
562+ // Capture stops once the cap is exhausted (or is non-positive), but line
563+ // streaming must keep draining so 'onLine' sees every line to the end.
564+ bool capture = maxCharacters is not <= 0 ;
564565
565566 char [ ] buffer = new char [ BufferSize ] ;
566- StringBuilder builder = maxCharacters is null
567- ? new StringBuilder ( )
568- : new StringBuilder ( Math . Min ( maxCharacters . Value , BufferSize ) ) ;
567+ StringBuilder builder = maxCharacters is > 0
568+ ? new StringBuilder ( Math . Min ( maxCharacters . Value , BufferSize ) )
569+ : new StringBuilder ( ) ;
570+ StringBuilder ? lineBuffer = onLine is null ? null : new StringBuilder ( ) ;
569571 bool truncated = false ;
570572
571573 while ( true )
@@ -578,6 +580,16 @@ private static async Task<string> ReadToEndCappedAsync(
578580 break ;
579581 }
580582
583+ if ( lineBuffer is not null )
584+ {
585+ EmitCompletedLines ( buffer , read , lineBuffer , onLine ! ) ;
586+ }
587+
588+ if ( ! capture )
589+ {
590+ continue ;
591+ }
592+
581593 if ( maxCharacters is null )
582594 {
583595 builder . Append ( buffer , 0 , read ) ;
@@ -596,6 +608,11 @@ private static async Task<string> ReadToEndCappedAsync(
596608 truncated |= charactersToAppend < read ;
597609 }
598610
611+ if ( lineBuffer is { Length : > 0 } )
612+ {
613+ onLine ! ( lineBuffer . ToString ( ) ) ;
614+ }
615+
599616 if ( truncated && maxCharacters is > 3 )
600617 {
601618 builder . Length = Math . Min ( builder . Length , maxCharacters . Value - 3 ) ;
@@ -605,13 +622,29 @@ private static async Task<string> ReadToEndCappedAsync(
605622 return builder . ToString ( ) ;
606623 }
607624
608- private static async Task DrainAsync (
609- TextReader reader ,
610- CancellationToken cancellationToken )
625+ private static void EmitCompletedLines (
626+ char [ ] buffer ,
627+ int count ,
628+ StringBuilder lineBuffer ,
629+ Action < string > onLine )
611630 {
612- char [ ] buffer = new char [ 4096 ] ;
613- while ( await reader . ReadAsync ( buffer . AsMemory ( 0 , buffer . Length ) , cancellationToken ) > 0 )
631+ for ( int index = 0 ; index < count ; index ++ )
614632 {
633+ char character = buffer [ index ] ;
634+ if ( character != '\n ' )
635+ {
636+ lineBuffer . Append ( character ) ;
637+ continue ;
638+ }
639+
640+ // Strip a trailing '\r' so CRLF-terminated lines stream cleanly.
641+ if ( lineBuffer . Length > 0 && lineBuffer [ ^ 1 ] == '\r ' )
642+ {
643+ lineBuffer . Length -- ;
644+ }
645+
646+ onLine ( lineBuffer . ToString ( ) ) ;
647+ lineBuffer . Clear ( ) ;
615648 }
616649 }
617650
0 commit comments