From 1d7cc8b153f572cbf233dd3ea07ca78ec8690009 Mon Sep 17 00:00:00 2001 From: Aaron Costley Date: Thu, 27 Dec 2018 23:36:16 -0600 Subject: [PATCH] fix(jsii-dotnet-runtime): Fix EPIPE on Windows. * The JSII runtime for DotNet applications did not properly dispose of the IO streams in use for communication between the dotnet and jsii processes. This has been fixed, which solves the EPIPE error. * During this invesigation, it was found that the DotNet runtime did not redirect stderr, which disabled the use of JSII_DEBUG. Fixed. * If the node process is closed unexpectedly, the application will now throw an exception with the contents of STDERR. * Added Unit Test for node process closing unexpectedly. * Added gitignore rule for "dist" folders, which are created by pack. Fixes #341 --- .gitignore | 3 +- .../ComplianceTests.cs | 2 +- packages/jsii-dotnet-runtime/.gitignore | 1 + .../Amazon.JSII.Runtime.UnitTests.csproj | 2 +- .../Client/RuntimeTests.cs | 41 +++++++++++++++++++ .../src/Amazon.JSII.Runtime/JsiiException.cs | 8 +++- .../Services/INodeProcess.cs | 8 ++-- .../Services/NodeProcess.cs | 18 +++++--- .../Amazon.JSII.Runtime/Services/Runtime.cs | 24 ++++++++++- 9 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Client/RuntimeTests.cs diff --git a/.gitignore b/.gitignore index 12d027be13..67f7451e58 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ .BUILD_COMPLETED lerna-debug.log .DS_Store -.idea \ No newline at end of file +.idea +dist \ No newline at end of file diff --git a/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs b/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs index 203f98160a..a464ae836e 100644 --- a/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs +++ b/packages/jsii-dotnet-runtime-test/test/Amazon.JSII.Runtime.IntegrationTests/ComplianceTests.cs @@ -861,7 +861,7 @@ public void NullShouldBeTreatedAsUndefined() obj.ChangeMeToUndefined = null; obj.VerifyPropertyIsUndefined(); } - + [Fact(DisplayName = Prefix + nameof(JsiiAgent))] public void JsiiAgent() { diff --git a/packages/jsii-dotnet-runtime/.gitignore b/packages/jsii-dotnet-runtime/.gitignore index 720c810bbb..4ea90d1e30 100644 --- a/packages/jsii-dotnet-runtime/.gitignore +++ b/packages/jsii-dotnet-runtime/.gitignore @@ -23,3 +23,4 @@ coverage/ bin/ cli/ obj/ +*.DotSettings.user diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Amazon.JSII.Runtime.UnitTests.csproj b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Amazon.JSII.Runtime.UnitTests.csproj index 314f5e1055..f910b9187a 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Amazon.JSII.Runtime.UnitTests.csproj +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Amazon.JSII.Runtime.UnitTests.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.0 diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Client/RuntimeTests.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Client/RuntimeTests.cs new file mode 100644 index 0000000000..30382ec3ea --- /dev/null +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime.UnitTests/Client/RuntimeTests.cs @@ -0,0 +1,41 @@ +using System.IO; +using Amazon.JSII.Runtime.Services; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Amazon.JSII.Runtime.UnitTests.Client +{ + public class RuntimeTests + { + private const string Prefix = "Runtime."; + + private INodeProcess _nodeProcessMock; + private TextReader _standardOutputMock; + private TextReader _standardErrorMock; + + private IRuntime _sut; + + public RuntimeTests() + { + _nodeProcessMock = Substitute.For(); + _standardOutputMock = Substitute.For(); + _standardErrorMock = Substitute.For(); + + _nodeProcessMock.StandardOutput.Returns(_standardOutputMock); + _nodeProcessMock.StandardError.Returns(_standardErrorMock); + + _sut = new Services.Runtime(_nodeProcessMock); + } + + [Fact(DisplayName = Prefix + nameof(ThrowsJsiiExceptionWhenResponseNotReceived))] + public void ThrowsJsiiExceptionWhenResponseNotReceived() + { + _nodeProcessMock.StandardOutput.ReadLine().ReturnsNull(); + _nodeProcessMock.StandardError.ReadToEnd().Returns("This is a test."); + + var ex = Assert.Throws(() => _sut.ReadResponse()); + Assert.Equal("Child process exited unexpectedly: This is a test.", ex.Message); + } + } +} \ No newline at end of file diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/JsiiException.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/JsiiException.cs index 996770b66d..d7390dbd3a 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/JsiiException.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/JsiiException.cs @@ -1,5 +1,5 @@ -using Amazon.JSII.JsonModel.Api.Response; -using System; +using System; +using Amazon.JSII.JsonModel.Api.Response; namespace Amazon.JSII.Runtime { @@ -7,6 +7,10 @@ public class JsiiException : Exception { public ErrorResponse ErrorResponse { get; } + public JsiiException(string message) : base(message) + { + } + public JsiiException(string message, Exception innerException) : base(message, innerException) { diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/INodeProcess.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/INodeProcess.cs index 635837dc5a..7da3671bdc 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/INodeProcess.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/INodeProcess.cs @@ -5,8 +5,10 @@ namespace Amazon.JSII.Runtime.Services { public interface INodeProcess : IDisposable { - StreamWriter StandardInput { get; } + TextWriter StandardInput { get; } - StreamReader StandardOutput { get; } + TextReader StandardOutput { get; } + + TextReader StandardError { get; } } -} +} \ No newline at end of file diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/NodeProcess.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/NodeProcess.cs index d3f0cd22f3..3d715a0f82 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/NodeProcess.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/NodeProcess.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Logging; -using System; +using System; using System.Diagnostics; using System.IO; +using Microsoft.Extensions.Logging; namespace Amazon.JSII.Runtime.Services { @@ -23,10 +23,11 @@ public NodeProcess(IJsiiRuntimeProvider jsiiRuntimeProvider, ILoggerFactory logg Arguments = "--max-old-space-size=4096 " + jsiiRuntimeProvider.JsiiRuntimePath, RedirectStandardInput = true, RedirectStandardOutput = true, + RedirectStandardError = true } }; - _process.StartInfo.EnvironmentVariables.Add("JSII_AGENT", "DotNet/" + Environment.Version.ToString()); + _process.StartInfo.EnvironmentVariables.Add("JSII_AGENT", "DotNet/" + Environment.Version); _logger.LogDebug("Starting jsii runtime..."); _logger.LogDebug($"{_process.StartInfo.FileName} {_process.StartInfo.Arguments}"); @@ -34,13 +35,18 @@ public NodeProcess(IJsiiRuntimeProvider jsiiRuntimeProvider, ILoggerFactory logg _process.Start(); } - public StreamWriter StandardInput => _process.StandardInput; + public TextWriter StandardInput => _process.StandardInput; - public StreamReader StandardOutput => _process.StandardOutput; + public TextReader StandardOutput => _process.StandardOutput; + + public TextReader StandardError => _process.StandardError; void IDisposable.Dispose() { + StandardInput.Dispose(); + StandardOutput.Dispose(); + StandardError.Dispose(); _process.Dispose(); } } -} +} \ No newline at end of file diff --git a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/Runtime.cs b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/Runtime.cs index 5f1cc7ceac..bbb4550df6 100644 --- a/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/Runtime.cs +++ b/packages/jsii-dotnet-runtime/src/Amazon.JSII.Runtime/Services/Runtime.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Amazon.JSII.Runtime.Services { @@ -9,11 +10,22 @@ public class Runtime : IRuntime public Runtime(INodeProcess nodeProcess) { _nodeProcess = nodeProcess ?? throw new ArgumentNullException(nameof(nodeProcess)); + if (Environment.GetEnvironmentVariable("JSII_DEBUG") != null) + { + Task.Run(() => RedirectStandardError()); + } } public string ReadResponse() { - return _nodeProcess.StandardOutput.ReadLine(); + var response = _nodeProcess.StandardOutput.ReadLine(); + if (string.IsNullOrEmpty(response)) + { + var errorMessage = _nodeProcess.StandardError.ReadToEnd(); + throw new JsiiException("Child process exited unexpectedly: " + errorMessage); + } + + return response; } public void WriteRequest(string request) @@ -31,5 +43,13 @@ public void WriteRequest(string request) _nodeProcess.StandardInput.WriteLine(request); _nodeProcess.StandardInput.Flush(); } + + private void RedirectStandardError() + { + while (true) + { + Console.WriteLine(_nodeProcess.StandardError.ReadLine()); + } + } } -} +} \ No newline at end of file