From ba361b626ffcab66e0bd94ca007b047128e08167 Mon Sep 17 00:00:00 2001 From: Phil Asmar Date: Tue, 1 Feb 2022 13:07:09 -0500 Subject: [PATCH] feat: capture standard error in server mode session --- .../CommandLineWrapper.cs | 54 ++++++++++++++++++- .../ServerModeSession.cs | 7 ++- .../Utilities/CappedStringBuilder.cs | 48 +++++++++++++++++ .../CappedStringBuilderTests.cs | 41 ++++++++++++++ .../ServerModeSessionTests.cs | 38 ++++++++----- 5 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs create mode 100644 test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs diff --git a/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs b/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs index d5ce6629c..a803804d1 100644 --- a/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs +++ b/src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; +using AWS.Deploy.ServerMode.Client.Utilities; namespace AWS.Deploy.ServerMode.Client { @@ -19,16 +21,21 @@ public CommandLineWrapper(bool diagnosticLoggingEnabled) _diagnosticLoggingEnabled = diagnosticLoggingEnabled; } - public virtual async Task Run(string command, params string[] stdIn) + public virtual async Task Run(string command, params string[] stdIn) { var arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"/c {command}" : $"-c \"{command}\""; + var strOutput = new CappedStringBuilder(100); + var strError = new CappedStringBuilder(50); + var processStartInfo = new ProcessStartInfo { FileName = GetSystemShell(), Arguments = arguments, UseShellExecute = false, // UseShellExecute must be false in allow redirection of StdIn. RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, CreateNoWindow = !_diagnosticLoggingEnabled, // It allows displaying stdout and stderr on the screen. }; @@ -43,9 +50,28 @@ public virtual async Task Run(string command, params string[] stdIn) await process.StandardInput.WriteLineAsync(line).ConfigureAwait(false); } + process.OutputDataReceived += (sender, e) => + { + strOutput.AppendLine(e.Data); + }; + + process.ErrorDataReceived += (sender, e) => + { + strError.AppendLine(e.Data); + }; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(-1); - return await Task.FromResult(process.ExitCode).ConfigureAwait(false); + var result = new RunResult + { + ExitCode = process.ExitCode, + StandardError = strError.ToString(), + StandardOut = strOutput.GetLastLines(5), + }; + + return await Task.FromResult(result).ConfigureAwait(false); } private string GetSystemShell() @@ -70,4 +96,28 @@ private bool TryGetEnvironmentVariable(string variable, out string? value) return !string.IsNullOrEmpty(value); } } + + public class RunResult + { + /// + /// Indicates if this command was run successfully. This checks that + /// is empty. + /// + public bool Success => string.IsNullOrWhiteSpace(StandardError); + + /// + /// Fully read + /// + public string StandardOut { get; set; } = string.Empty; + + /// + /// Fully read + /// + public string StandardError { get; set; } = string.Empty; + + /// + /// Fully read + /// + public int ExitCode { get; set; } + } } diff --git a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs index 84667a9c9..1f220ef9f 100644 --- a/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs +++ b/src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs @@ -164,9 +164,12 @@ public async Task Start(CancellationToken cancellationToken) // For -100 errors, we want to check all the ports in the configured port range // If the error code other than -100, this is an unexpected exit code. - if (startServerTask.Result != TCP_PORT_ERROR) + if (startServerTask.Result.ExitCode != TCP_PORT_ERROR) { - throw new InternalServerModeException($"\"{command}\" failed for unknown reason."); + throw new InternalServerModeException( + string.IsNullOrEmpty(startServerTask.Result.StandardError) ? + $"\"{command}\" failed for unknown reason." : + startServerTask.Result.StandardError); } } diff --git a/src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs b/src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs new file mode 100644 index 000000000..954757744 --- /dev/null +++ b/src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AWS.Deploy.ServerMode.Client.Utilities +{ + public class CappedStringBuilder + { + public int LineLimit { get; } + public int LineCount { + get + { + return _lines?.Count ?? 0; + } + } + + private readonly Queue _lines; + + public CappedStringBuilder(int lineLimit) + { + _lines = new Queue(lineLimit); + LineLimit = lineLimit; + } + + public void AppendLine(string value) + { + if (LineCount >= LineLimit) + { + _lines.Dequeue(); + } + + _lines.Enqueue(value); + } + + public string GetLastLines(int lineCount) + { + return _lines.Reverse().Take(lineCount).Reverse().Aggregate((x, y) => x + Environment.NewLine + y); + } + + public override string ToString() + { + return _lines.Aggregate((x, y) => x + Environment.NewLine + y); + } + } +} diff --git a/test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs b/test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs new file mode 100644 index 000000000..7fc1e34d8 --- /dev/null +++ b/test/AWS.Deploy.ServerMode.Client.UnitTests/CappedStringBuilderTests.cs @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using AWS.Deploy.ServerMode.Client.Utilities; +using Xunit; + +namespace AWS.Deploy.ServerMode.Client.UnitTests +{ + public class CappedStringBuilderTests + { + private readonly CappedStringBuilder _cappedStringBuilder; + + public CappedStringBuilderTests() + { + _cappedStringBuilder = new CappedStringBuilder(5); + } + + [Fact] + public void AppendLineTest() + { + _cappedStringBuilder.AppendLine("test1"); + _cappedStringBuilder.AppendLine("test2"); + _cappedStringBuilder.AppendLine("test3"); + + Assert.Equal(3, _cappedStringBuilder.LineCount); + Assert.Equal($"test1{Environment.NewLine}test2{Environment.NewLine}test3", _cappedStringBuilder.ToString()); + } + + [Fact] + public void GetLastLinesTest() + { + _cappedStringBuilder.AppendLine("test1"); + _cappedStringBuilder.AppendLine("test2"); + + Assert.Equal(2, _cappedStringBuilder.LineCount); + Assert.Equal("test2", _cappedStringBuilder.GetLastLines(1)); + Assert.Equal($"test1{Environment.NewLine}test2", _cappedStringBuilder.GetLastLines(2)); + } + } +} diff --git a/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs b/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs index 5e2317a3b..d9444c71d 100644 --- a/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs +++ b/test/AWS.Deploy.ServerMode.Client.UnitTests/ServerModeSessionTests.cs @@ -27,7 +27,8 @@ public ServerModeSessionTests() public async Task Start() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); // Act @@ -44,7 +45,8 @@ public async Task Start() public async Task Start_PortUnavailable() { // Arrange - MockCommandLineWrapperRun(-100); + var runResult = new RunResult { ExitCode = -100 }; + MockCommandLineWrapperRun(runResult); MockHttpGet(HttpStatusCode.NotFound, TimeSpan.FromSeconds(5)); // Act & Assert @@ -58,7 +60,8 @@ await Assert.ThrowsAsync(async () => public async Task Start_HttpGetThrows() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGetThrows(); // Act & Assert @@ -72,7 +75,8 @@ await Assert.ThrowsAsync(async () => public async Task Start_HttpGetForbidden() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.Forbidden); // Act & Assert @@ -96,7 +100,8 @@ public async Task IsAlive_BaseUrlNotInitialized() public async Task IsAlive_GetAsyncThrows() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -113,7 +118,8 @@ public async Task IsAlive_GetAsyncThrows() public async Task IsAlive_HttpResponseSuccess() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -128,7 +134,8 @@ public async Task IsAlive_HttpResponseSuccess() public async Task IsAlive_HttpResponseFailure() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -145,7 +152,8 @@ public async Task IsAlive_HttpResponseFailure() public async Task TryGetRestAPIClient() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -161,7 +169,8 @@ public async Task TryGetRestAPIClient() public void TryGetRestAPIClient_WithoutStart() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); // Act @@ -176,7 +185,8 @@ public void TryGetRestAPIClient_WithoutStart() public async Task TryGetDeploymentCommunicationClient() { // Arrange - MockCommandLineWrapperRun(0, TimeSpan.FromSeconds(100)); + var runResult = new RunResult { ExitCode = 0 }; + MockCommandLineWrapperRun(runResult, TimeSpan.FromSeconds(100)); MockHttpGet(HttpStatusCode.OK); await _serverModeSession.Start(CancellationToken.None); @@ -228,15 +238,15 @@ private void MockHttpGetThrows() => ItExpr.IsAny()) .Throws(new Exception()); - private void MockCommandLineWrapperRun(int statusCode) => + private void MockCommandLineWrapperRun(RunResult runResult) => _commandLineWrapper .Setup(wrapper => wrapper.Run(It.IsAny(), It.IsAny())) - .ReturnsAsync(statusCode); + .ReturnsAsync(runResult); - private void MockCommandLineWrapperRun(int statusCode, TimeSpan delay) => + private void MockCommandLineWrapperRun(RunResult runResult, TimeSpan delay) => _commandLineWrapper .Setup(wrapper => wrapper.Run(It.IsAny(), It.IsAny())) - .ReturnsAsync(statusCode, delay); + .ReturnsAsync(runResult, delay); private Task CredentialGenerator() => throw new NotImplementedException(); }