Skip to content

Commit

Permalink
feat: capture standard error in server mode session
Browse files Browse the repository at this point in the history
  • Loading branch information
philasmar committed Feb 3, 2022
1 parent 69dd0f9 commit ba361b6
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 18 deletions.
54 changes: 52 additions & 2 deletions src/AWS.Deploy.ServerMode.Client/CommandLineWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -19,16 +21,21 @@ public CommandLineWrapper(bool diagnosticLoggingEnabled)
_diagnosticLoggingEnabled = diagnosticLoggingEnabled;
}

public virtual async Task<int> Run(string command, params string[] stdIn)
public virtual async Task<RunResult> 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.
};

Expand All @@ -43,9 +50,28 @@ public virtual async Task<int> 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()
Expand All @@ -70,4 +96,28 @@ private bool TryGetEnvironmentVariable(string variable, out string? value)
return !string.IsNullOrEmpty(value);
}
}

public class RunResult
{
/// <summary>
/// Indicates if this command was run successfully. This checks that
/// <see cref="StandardError"/> is empty.
/// </summary>
public bool Success => string.IsNullOrWhiteSpace(StandardError);

/// <summary>
/// Fully read <see cref="Process.StandardOutput"/>
/// </summary>
public string StandardOut { get; set; } = string.Empty;

/// <summary>
/// Fully read <see cref="Process.StandardError"/>
/// </summary>
public string StandardError { get; set; } = string.Empty;

/// <summary>
/// Fully read <see cref="Process.ExitCode"/>
/// </summary>
public int ExitCode { get; set; }
}
}
7 changes: 5 additions & 2 deletions src/AWS.Deploy.ServerMode.Client/ServerModeSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
48 changes: 48 additions & 0 deletions src/AWS.Deploy.ServerMode.Client/Utilities/CappedStringBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<string> _lines;

public CappedStringBuilder(int lineLimit)
{
_lines = new Queue<string>(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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -58,7 +60,8 @@ await Assert.ThrowsAsync<PortUnavailableException>(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
Expand All @@ -72,7 +75,8 @@ await Assert.ThrowsAsync<InternalServerModeException>(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
Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -228,15 +238,15 @@ private void MockHttpGetThrows() =>
ItExpr.IsAny<CancellationToken>())
.Throws(new Exception());

private void MockCommandLineWrapperRun(int statusCode) =>
private void MockCommandLineWrapperRun(RunResult runResult) =>
_commandLineWrapper
.Setup(wrapper => wrapper.Run(It.IsAny<string>(), It.IsAny<string[]>()))
.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<string>(), It.IsAny<string[]>()))
.ReturnsAsync(statusCode, delay);
.ReturnsAsync(runResult, delay);

private Task<AWSCredentials> CredentialGenerator() => throw new NotImplementedException();
}
Expand Down

0 comments on commit ba361b6

Please sign in to comment.