77using System . Text ;
88using System . Threading ;
99using System . Threading . Tasks ;
10+ using Buildvana . Core . Process . Internal ;
1011using CliWrap ;
1112using CommunityToolkit . Diagnostics ;
1213
@@ -17,7 +18,8 @@ namespace Buildvana.Core.Process;
1718/// </summary>
1819public sealed class ProcessRunner : IProcessRunner
1920{
20- private const int TailCapBytes = 4096 ;
21+ private const int OutputHeadLines = 20 ;
22+ private const int OutputTailLines = 20 ;
2123
2224 /// <inheritdoc cref="IProcessRunner.RunAsync"/>
2325 public async Task < ProcessResult > RunAsync (
@@ -35,20 +37,27 @@ public async Task<ProcessResult> RunAsync(
3537
3638 var stdoutBuffer = new StringBuilder ( ) ;
3739 var stderrBuffer = new StringBuilder ( ) ;
40+ var stdoutCapture = new HeadTailPipeTarget ( maxHeadLines : OutputHeadLines , maxTailLines : OutputTailLines ) ;
41+ var stderrCapture = new HeadTailPipeTarget ( maxHeadLines : OutputHeadLines , maxTailLines : OutputTailLines ) ;
3842
39- // When the caller wants line-by-line stdout, fan the stream out to both the buffer and their callback.
43+ // When the caller wants line-by-line stdout, fan the stream out to their callback too .
4044 var stdoutPipe = onStdout is null
41- ? PipeTarget . ToStringBuilder ( stdoutBuffer )
45+ ? PipeTarget . Merge (
46+ stdoutCapture ,
47+ PipeTarget . ToStringBuilder ( stdoutBuffer ) )
4248 : PipeTarget . Merge (
49+ stdoutCapture ,
4350 PipeTarget . ToStringBuilder ( stdoutBuffer ) ,
4451 PipeTarget . ToDelegate ( onStdout ) ) ;
4552
53+ var stderrPipe = PipeTarget . Merge ( stderrCapture , PipeTarget . ToStringBuilder ( stderrBuffer ) ) ;
54+
4655 var command = Cli . Wrap ( executable )
4756
4857 // ReSharper disable once PossibleMultipleEnumeration
4958 . WithArguments ( args )
5059 . WithStandardOutputPipe ( stdoutPipe )
51- . WithStandardErrorPipe ( PipeTarget . ToStringBuilder ( stderrBuffer ) )
60+ . WithStandardErrorPipe ( stderrPipe )
5261 . WithValidation ( CommandResultValidation . None ) ;
5362
5463 if ( workingDirectory is not null )
@@ -59,65 +68,26 @@ public async Task<ProcessResult> RunAsync(
5968 var commandResult = await command . ExecuteAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
6069
6170 var result = new ProcessResult (
71+ command . ToString ( ) ,
6272 commandResult . ExitCode ,
6373 stdoutBuffer . ToString ( ) ,
6474 stderrBuffer . ToString ( ) ,
6575 commandResult . RunTime ) ;
6676
6777 if ( throwOnNonZero && result . ExitCode != 0 )
6878 {
69- throw new BuildFailedException ( result . ExitCode , BuildFailureMessage ( executable , result ) ) ;
79+ throw new BuildFailedException ( result . ExitCode , BuildFailureMessage ( executable , result , stdoutCapture , stderrCapture ) ) ;
7080 }
7181
7282 return result ;
7383 }
7484
75- private static string BuildFailureMessage ( string executable , ProcessResult result )
76- {
77- var stdout = FormatTail ( result . StandardOutput ) ;
78- var stderr = FormatTail ( result . StandardError ) ;
79- var header = string . Create (
80- CultureInfo . InvariantCulture ,
81- $ "Command '{ executable } ' exited with code { result . ExitCode } .") ;
82- if ( stdout is null && stderr is null )
83- {
84- return header ;
85- }
86-
87- var sb = new StringBuilder ( header ) ;
88- if ( stdout is not null )
89- {
90- _ = sb . Append ( "\n --- stdout ---\n " ) . Append ( stdout ) ;
91- }
92-
93- if ( stderr is not null )
94- {
95- _ = sb . Append ( "\n --- stderr ---\n " ) . Append ( stderr ) ;
96- }
97-
98- return sb . ToString ( ) ;
99- }
100-
101- private static string ? FormatTail ( string text )
102- {
103- var trimmed = text . TrimEnd ( ) ;
104- if ( trimmed . Length == 0 )
105- {
106- return null ;
107- }
108-
109- var bytes = Encoding . UTF8 . GetBytes ( trimmed ) ;
110- if ( bytes . Length <= TailCapBytes )
111- {
112- return trimmed ;
113- }
114-
115- var start = bytes . Length - TailCapBytes ;
116- while ( start < bytes . Length && ( bytes [ start ] & 0xC0 ) == 0x80 )
117- {
118- start ++ ;
119- }
120-
121- return "…" + Encoding . UTF8 . GetString ( bytes , start , bytes . Length - start ) . TrimStart ( ) ;
122- }
85+ private static string BuildFailureMessage ( string executable , ProcessResult result , HeadTailPipeTarget stdoutCapture , HeadTailPipeTarget stderrCapture )
86+ => new StringBuilder ( )
87+ . AppendLine ( CultureInfo . InvariantCulture , $ "Command '{ executable } ' exited with code { result . ExitCode } .")
88+ . AppendHeader ( "full command line" )
89+ . AppendLine ( result . CommandLine )
90+ . AppendHeadTail ( stdoutCapture , "stdout" )
91+ . AppendHeadTail ( stderrCapture , "stderr" )
92+ . ToString ( ) ;
12393}
0 commit comments