Skip to content

Commit aa8d67c

Browse files
authored
+semver:minor - Log commands standard output and standard error even if they throw ex… (#383)
* Log commands standard output and standard error even if they throw exceptions * Fix compilation error * Formatting Markdown --------- Co-authored-by: Tom Longhurst <thomhurst@users.noreply.github.com>
1 parent a84da9d commit aa8d67c

File tree

8 files changed

+106
-46
lines changed

8 files changed

+106
-46
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ Define your pipeline in .NET! Strong types, intellisense, parallelisation, and t
6464
| ModularPipelines.WinGet | Helpers for interacting with the Windows Package Manager. | [![nuget](https://img.shields.io/nuget/v/ModularPipelines.WinGet.svg)](https://www.nuget.org/packages/ModularPipelines.WinGet/) |
6565
| ModularPipelines.Yarn | Helpers for interacting with Yarn CLI. | [![nuget](https://img.shields.io/nuget/v/ModularPipelines.Yarn.svg)](https://www.nuget.org/packages/ModularPipelines.Yarn/) |
6666

67-
6867
## Getting Started
6968

7069
If you want to see how to get started, or want to know more about ModularPipelines, [read the Documentation here](https://thomhurst.github.io/ModularPipelines)

src/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class AddLocalNugetSourceModule : Module<CommandResult>
1515
protected override Task<bool> ShouldIgnoreFailures(IPipelineContext context, Exception exception)
1616
{
1717
return Task.FromResult(exception is CommandException commandException &&
18-
commandException.CommandResult.StandardOutput.Contains("The name specified has already been added to the list of available package sources"));
18+
commandException.StandardOutput.Contains("The name specified has already been added to the list of available package sources"));
1919
}
2020

2121
/// <inheritdoc/>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
* Log commands standard output and standard error even if they throw exceptions
2+
3+
# Breaking
4+
5+
* CommandException now doesn't expose a CommandResult object. Things like StandardOutput are available directly from the exception

src/ModularPipelines/Context/Command.cs

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System.Collections.ObjectModel;
2+
using System.Diagnostics;
23
using System.Reflection;
4+
using System.Text;
35
using CliWrap;
46
using CliWrap.Buffered;
7+
using CliWrap.Exceptions;
58
using ModularPipelines.Attributes;
69
using ModularPipelines.Exceptions;
710
using ModularPipelines.Helpers;
@@ -61,43 +64,84 @@ public async Task<CommandResult> ExecuteCommandLineTool(CommandLineToolOptions o
6164

6265
if (options.InternalDryRun)
6366
{
64-
_commandLogger.Log(options, command.ToString(),
65-
new BufferedCommandResult(0,
66-
DateTimeOffset.UtcNow,
67-
DateTimeOffset.UtcNow,
68-
"Dummy Output Response",
69-
"Dummy Error Response"));
67+
_commandLogger.Log(options,
68+
inputToLog: command.ToString(),
69+
exitCode: 0,
70+
runTime: TimeSpan.Zero,
71+
standardOutput: "Dummy Output Response",
72+
standardError: "Dummy Error Response"
73+
);
7074

7175
return new CommandResult(command);
7276
}
7377

74-
var result = await Of(command, options, cancellationToken);
75-
76-
return new CommandResult(command, result);
78+
return await Of(command, options, cancellationToken);
7779
}
7880

7981
private static object GetOptionsObject(CommandLineToolOptions options)
8082
{
8183
return options.OptionsObject ?? options;
8284
}
8385

84-
private async Task<BufferedCommandResult> Of(CliWrap.Command command,
86+
private async Task<CommandResult> Of(CliWrap.Command command,
8587
CommandLineToolOptions options, CancellationToken cancellationToken = default)
8688
{
87-
var result = await command
88-
.WithValidation(CommandResultValidation.None)
89-
.ExecuteBufferedAsync(cancellationToken);
90-
91-
_commandLogger.Log(options: options,
92-
inputToLog: command.ToString(),
93-
result);
94-
95-
if (result.ExitCode != 0 && options.ThrowOnNonZeroExitCode)
89+
StringBuilder standardOutputStringBuilder = new();
90+
StringBuilder standardErrorStringBuilder = new();
91+
var stopwatch = Stopwatch.StartNew();
92+
93+
var standardOutput = string.Empty;
94+
var standardError = string.Empty;
95+
96+
try
9697
{
97-
var input = command.ToString();
98-
throw new CommandException(input, result);
98+
var result = await command
99+
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(standardOutputStringBuilder))
100+
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(standardErrorStringBuilder))
101+
.WithValidation(CommandResultValidation.None)
102+
.ExecuteAsync(cancellationToken);
103+
104+
standardOutput = standardOutputStringBuilder.ToString();
105+
standardError = standardErrorStringBuilder.ToString();
106+
107+
_commandLogger.Log(options: options,
108+
inputToLog: command.ToString(),
109+
result.ExitCode,
110+
result.RunTime,
111+
standardOutput,
112+
standardError
113+
);
114+
115+
if (result.ExitCode != 0 && options.ThrowOnNonZeroExitCode)
116+
{
117+
var input = command.ToString();
118+
throw new CommandException(input, result.ExitCode, result.RunTime, standardOutput, standardError);
119+
}
120+
121+
return new CommandResult(command, result, standardOutput, standardError);
122+
}
123+
catch (CommandExecutionException e)
124+
{
125+
_commandLogger.Log(options: options,
126+
inputToLog: command.ToString(),
127+
e.ExitCode,
128+
stopwatch.Elapsed,
129+
standardOutput,
130+
standardError
131+
);
132+
throw;
133+
}
134+
catch (Exception e) when (e is not CommandExecutionException)
135+
{
136+
_commandLogger.Log(options: options,
137+
inputToLog: command.ToString(),
138+
-1,
139+
stopwatch.Elapsed,
140+
standardOutput,
141+
standardError
142+
);
143+
144+
throw;
99145
}
100-
101-
return result;
102146
}
103147
}
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1-
using CliWrap.Buffered;
2-
31
namespace ModularPipelines.Exceptions;
42

53
public class CommandException : PipelineException
64
{
7-
public CommandException(string input, BufferedCommandResult commandResult) : base(GenerateMessage(input, commandResult))
5+
public CommandException(string input, int exitCode, TimeSpan executionTime, string standardOutput, string standardError) : base(GenerateMessage(input, exitCode, executionTime, standardOutput, standardError))
86
{
9-
CommandResult = commandResult;
7+
ExitCode = exitCode;
8+
ExecutionTime = executionTime;
9+
StandardOutput = standardOutput;
10+
StandardError = standardError;
1011
}
12+
13+
public int ExitCode { get; }
14+
15+
public TimeSpan ExecutionTime { get; }
16+
17+
public string StandardOutput { get; }
1118

12-
public BufferedCommandResult CommandResult { get; }
19+
public string StandardError { get; }
1320

14-
private static string? GenerateMessage(string input, BufferedCommandResult commandResult)
21+
private static string? GenerateMessage(string input, int exitCode, TimeSpan executionTime,
22+
string standardOutput, string standardError)
1523
{
16-
return $"Error: {GetOutput(commandResult)}{Environment.NewLine}Exit Code: {commandResult.ExitCode}{Environment.NewLine}Input: {input}";
24+
return $"Error: {GetOutput(standardOutput, standardError)}{Environment.NewLine}Exit Code: {exitCode}{Environment.NewLine}Input: {input}";
1725
}
1826

19-
private static string GetOutput(BufferedCommandResult commandResult)
27+
private static string GetOutput(string standardOutput, string standardError)
2028
{
21-
return !string.IsNullOrEmpty(commandResult.StandardError) ? commandResult.StandardError : commandResult.StandardOutput;
29+
return !string.IsNullOrEmpty(standardError) ? standardError : standardOutput;
2230
}
2331
}

src/ModularPipelines/Logging/CommandLogger.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using CliWrap.Buffered;
21
using Microsoft.Extensions.Logging;
32
using Microsoft.Extensions.Options;
43
using ModularPipelines.Engine;
@@ -26,7 +25,7 @@ public CommandLogger(IModuleLoggerProvider moduleLoggerProvider,
2625

2726
private ILogger Logger => _moduleLoggerProvider.GetLogger();
2827

29-
public void Log(CommandLineToolOptions options, string? inputToLog, BufferedCommandResult result)
28+
public void Log(CommandLineToolOptions options, string? inputToLog, int? exitCode, TimeSpan? runTime, string standardOutput, string standardError)
3029
{
3130
if (options.CommandLogging == CommandLogging.None)
3231
{
@@ -67,15 +66,15 @@ public void Log(CommandLineToolOptions options, string? inputToLog, BufferedComm
6766
Logger.LogInformation("""
6867
---Exit Code----
6968
{ExitCode}
70-
""", result.ExitCode);
69+
""", exitCode);
7170
}
7271

7372
if (optionsCommandLogging.HasFlag(CommandLogging.Duration))
7473
{
7574
Logger.LogInformation("""
7675
---Duration---
7776
{Duration}
78-
""", result.RunTime.ToDisplayString());
77+
""", runTime?.ToDisplayString());
7978
}
8079

8180
if (ShouldLogOutput(optionsCommandLogging))
@@ -85,17 +84,17 @@ public void Log(CommandLineToolOptions options, string? inputToLog, BufferedComm
8584
Logger.LogInformation("""
8685
---Command Result---
8786
{Output}
88-
""", outputLoggingManipulator(_secretObfuscator.Obfuscate(result.StandardOutput, options)));
87+
""", outputLoggingManipulator(_secretObfuscator.Obfuscate(standardOutput, options)));
8988
}
9089

91-
if (ShouldLogError(optionsCommandLogging, result.ExitCode))
90+
if (ShouldLogError(optionsCommandLogging, exitCode))
9291
{
9392
var outputLoggingManipulator = options.OutputLoggingManipulator ?? (s => s);
9493

9594
Logger.LogInformation("""
9695
---Command Error---
9796
{Error}
98-
""", outputLoggingManipulator(_secretObfuscator.Obfuscate(result.StandardError, options)));
97+
""", outputLoggingManipulator(_secretObfuscator.Obfuscate(standardError, options)));
9998
}
10099
}
101100
}
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
using CliWrap.Buffered;
21
using ModularPipelines.Options;
32

43
namespace ModularPipelines.Logging;
54

65
internal interface ICommandLogger
76
{
8-
void Log(CommandLineToolOptions options, string? inputToLog, BufferedCommandResult commandResult);
7+
void Log(CommandLineToolOptions options,
8+
string? inputToLog,
9+
int? exitCode,
10+
TimeSpan? runTime,
11+
string standardOutput,
12+
string standardError
13+
);
914
}

src/ModularPipelines/Models/CommandResult.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ internal CommandResult(Command command)
2727
EnvironmentVariables = command.EnvironmentVariables;
2828
}
2929

30-
internal CommandResult(Command command, BufferedCommandResult commandResult)
30+
internal CommandResult(Command command, CliWrap.CommandResult commandResult, string standardOutput, string standardError)
3131
{
3232
CommandInput = command.ToString();
3333
WorkingDirectory = command.WorkingDirPath;
3434
EnvironmentVariables = command.EnvironmentVariables;
3535

36-
StandardOutput = commandResult.StandardOutput;
37-
StandardError = commandResult.StandardError;
36+
StandardOutput = standardOutput;
37+
StandardError = standardError;
3838
StartTime = commandResult.StartTime;
3939
EndTime = commandResult.ExitTime;
4040
Duration = commandResult.RunTime;

0 commit comments

Comments
 (0)