diff --git a/build/dependencies.props b/build/dependencies.props
index cdd013ce5d..8691d498a5 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -3,7 +3,7 @@
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
- 2.1.0-preview2-15721
+ 2.1.0-preview2-157262.1.0-preview2-302202.1.0-preview2-302202.1.0-preview2-30220
diff --git a/korebuild-lock.txt b/korebuild-lock.txt
index e6c7fddffa..bdaa7048b3 100644
--- a/korebuild-lock.txt
+++ b/korebuild-lock.txt
@@ -1,2 +1,2 @@
-version:2.1.0-preview2-15721
-commithash:f9bb4be59e39938ec59a6975257e26099b0d03c1
+version:2.1.0-preview2-15726
+commithash:599e691c41f502ed9e062b1822ce13b673fc916e
diff --git a/samples/NativeIISSample/NativeIISSample.csproj b/samples/NativeIISSample/NativeIISSample.csproj
index d9c2419b8f..ac2d826cff 100644
--- a/samples/NativeIISSample/NativeIISSample.csproj
+++ b/samples/NativeIISSample/NativeIISSample.csproj
@@ -20,4 +20,8 @@
+
+ inprocess
+
+
diff --git a/samples/NativeIISSample/Startup.cs b/samples/NativeIISSample/Startup.cs
index 3fac22e9da..4010839be8 100644
--- a/samples/NativeIISSample/Startup.cs
+++ b/samples/NativeIISSample/Startup.cs
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.IIS;
using Microsoft.AspNetCore.Server.IISIntegration;
diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs
index 7a211d83d8..e922dbd909 100644
--- a/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs
+++ b/src/Microsoft.AspNetCore.Server.IISIntegration/NativeMethods.cs
@@ -90,10 +90,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
internal unsafe static extern int http_websockets_write_bytes(IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, PFN_WEBSOCKET_ASYNC_COMPLETION pfnCompletionCallback, IntPtr pvCompletionContext, out bool fCompletionExpected);
[DllImport(AspNetCoreModuleDll)]
- public unsafe static extern int http_enable_websockets(IntPtr pHttpContext);
+ public unsafe static extern int http_enable_websockets(IntPtr pInProcessHandler);
[DllImport(AspNetCoreModuleDll)]
- public unsafe static extern int http_cancel_io(IntPtr pHttpContext);
+ public unsafe static extern int http_cancel_io(IntPtr pInProcessHandler);
[DllImport(AspNetCoreModuleDll)]
public unsafe static extern int http_response_set_unknown_header(IntPtr pInProcessHandler, byte* pszHeaderName, byte* pszHeaderValue, ushort usHeaderValueLength, bool fReplace);
diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISAwaitable.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISAwaitable.cs
index ef5d29b078..28cc3672d5 100644
--- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISAwaitable.cs
+++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISAwaitable.cs
@@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
@@ -18,8 +17,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private Action _callback;
private Exception _exception;
-
private int _cbBytes;
+ private int _hr;
public static readonly NativeMethods.PFN_WEBSOCKET_ASYNC_COMPLETION ReadCallback = (IntPtr pHttpContext, IntPtr pCompletionInfo, IntPtr pvCompletionContext) =>
{
@@ -52,15 +51,30 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
{
var exception = _exception;
var cbBytes = _cbBytes;
+ var hr = _hr;
// Reset the awaitable state
_exception = null;
_cbBytes = 0;
_callback = null;
+ _hr = 0;
if (exception != null)
{
- throw exception;
+ // If the exception was an aborted read operation,
+ // return -1 to notify NativeReadAsync that the write was cancelled.
+ // E_OPERATIONABORTED == 0x800703e3 == -2147023901
+ // We also don't throw the exception here as this is expected behavior
+ // and can negatively impact perf if we catch an exception for each
+ // cann
+ if (hr != IISServerConstants.HResultCancelIO)
+ {
+ throw exception;
+ }
+ else
+ {
+ cbBytes = -1;
+ }
}
return cbBytes;
@@ -86,12 +100,20 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
public void Complete(int hr, int cbBytes)
{
+ _hr = hr;
+ _exception = Marshal.GetExceptionForHR(hr);
+ _cbBytes = cbBytes;
+ var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
+ continuation?.Invoke();
+ }
+
+ public Action GetCompletion(int hr, int cbBytes)
+ {
+ _hr = hr;
_exception = Marshal.GetExceptionForHR(hr);
_cbBytes = cbBytes;
- var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
-
- continuation?.Invoke();
+ return Interlocked.Exchange(ref _callback, _callbackCompleted);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs
index 345bd6f34e..7f1efba984 100644
--- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs
+++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Threading;
@@ -14,7 +13,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.WebUtilities;
-using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.IISIntegration
{
@@ -291,15 +289,16 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
{
throw new InvalidOperationException("CoreStrings.UpgradeCannotBeCalledMultipleTimes");
}
+ _wasUpgraded = true;
StatusCode = StatusCodes.Status101SwitchingProtocols;
ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols);
- await UpgradeAsync();
- NativeMethods.http_enable_websockets(_pInProcessHandler);
-
- _wasUpgraded = true;
_readWebSocketsOperation = new IISAwaitable();
_writeWebSocketsOperation = new IISAwaitable();
+ NativeMethods.http_enable_websockets(_pInProcessHandler);
+
+ // Upgrade async will cause the stream processing to go into duplex mode
+ await UpgradeAsync();
return new DuplexStream(RequestBody, ResponseBody);
}
diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.ReadWrite.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.ReadWrite.cs
new file mode 100644
index 0000000000..f60a2a848a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.ReadWrite.cs
@@ -0,0 +1,454 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.IISIntegration
+{
+ internal partial class IISHttpContext
+ {
+ ///
+ /// Reads data from the Input pipe to the user.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ // Start a task which will continuously call ReadFromIISAsync and WriteToIISAsync
+ StartProcessingRequestAndResponseBody();
+
+ while (true)
+ {
+ var result = await Input.Reader.ReadAsync();
+ var readableBuffer = result.Buffer;
+ try
+ {
+ if (!readableBuffer.IsEmpty)
+ {
+ var actual = Math.Min(readableBuffer.Length, count);
+ readableBuffer = readableBuffer.Slice(0, actual);
+ readableBuffer.CopyTo(buffer);
+ return (int)actual;
+ }
+ else if (result.IsCompleted)
+ {
+ return 0;
+ }
+ }
+ finally
+ {
+ Input.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End);
+ }
+ }
+ }
+
+ ///
+ /// Writes data to the output pipe.
+ ///
+ ///
+ ///
+ ///
+ public Task WriteAsync(ArraySegment data, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!_hasResponseStarted)
+ {
+ return WriteAsyncAwaited(data, cancellationToken);
+ }
+
+ lock (_stateSync)
+ {
+ DisableReads();
+ return Output.WriteAsync(data, cancellationToken: cancellationToken);
+ }
+ }
+
+ ///
+ /// Flushes the data in the output pipe
+ ///
+ ///
+ ///
+ public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (!_hasResponseStarted)
+ {
+ return FlushAsyncAwaited(cancellationToken);
+ }
+ lock (_stateSync)
+ {
+ DisableReads();
+ return Output.FlushAsync(cancellationToken);
+ }
+ }
+
+ public void StartProcessingRequestAndResponseBody()
+ {
+ if (_processBodiesTask == null)
+ {
+ lock (_createReadWriteBodySync)
+ {
+ if (_processBodiesTask == null)
+ {
+ _processBodiesTask = ConsumeAsync();
+ }
+ }
+ }
+ }
+
+ private async Task FlushAsyncAwaited(CancellationToken cancellationToken)
+ {
+ await InitializeResponseAwaited();
+
+ Task flushTask;
+ lock (_stateSync)
+ {
+ DisableReads();
+
+ // Want to guarantee that data has been written to the pipe before releasing the lock.
+ flushTask = Output.FlushAsync(cancellationToken: cancellationToken);
+ }
+ await flushTask;
+ }
+
+ private async Task WriteAsyncAwaited(ArraySegment data, CancellationToken cancellationToken)
+ {
+ // WriteAsyncAwaited is only called for the first write to the body.
+ // Ensure headers are flushed if Write(Chunked)Async isn't called.
+ await InitializeResponseAwaited();
+
+ Task writeTask;
+ lock (_stateSync)
+ {
+ DisableReads();
+
+ // Want to guarantee that data has been written to the pipe before releasing the lock.
+ writeTask = Output.WriteAsync(data, cancellationToken: cancellationToken);
+ }
+ await writeTask;
+ }
+
+ // ConsumeAsync is called when either the first read or first write is done.
+ // There are two modes for reading and writing to the request/response bodies without upgrade.
+ // 1. Await all reads and try to read from the Output pipe
+ // 2. Done reading and await all writes.
+ // If the request is upgraded, we will start bidirectional streams for the input and output.
+ private async Task ConsumeAsync()
+ {
+ await ReadAndWriteLoopAsync();
+
+ // The ReadAndWriteLoop can return due to being upgraded. Check if _wasUpgraded is true to determine
+ // whether we go to a bidirectional stream or only write.
+ if (_wasUpgraded)
+ {
+ await StartBidirectionalStream();
+ }
+ }
+
+ private unsafe IISAwaitable ReadFromIISAsync(int length)
+ {
+ Action completion = null;
+ lock (_stateSync)
+ {
+ // We don't want to read if there is data available in the output pipe
+ // Therefore, we mark the current operation as cancelled to allow for the read
+ // to be requeued.
+ if (Output.Reader.TryRead(out var result))
+ {
+ // If the buffer is empty, it is considered a write of zero.
+ // we still want to cancel and allow the write to occur.
+ completion = _operation.GetCompletion(hr: IISServerConstants.HResultCancelIO, cbBytes: 0);
+ Output.Reader.AdvanceTo(result.Buffer.Start);
+ }
+ else
+ {
+ var hr = NativeMethods.http_read_request_bytes(
+ _pInProcessHandler,
+ (byte*)_inputHandle.Pointer,
+ length,
+ out var dwReceivedBytes,
+ out bool fCompletionExpected);
+ // if we complete the read synchronously, there is no need to set the reading flag
+ // as there is no cancelable operation.
+ if (!fCompletionExpected)
+ {
+ completion = _operation.GetCompletion(hr, dwReceivedBytes);
+ }
+ else
+ {
+ _reading = true;
+ }
+ }
+ }
+
+ // Invoke the completion outside of the lock if the reead finished synchronously.
+ completion?.Invoke();
+
+ return _operation;
+ }
+
+ private unsafe IISAwaitable WriteToIISAsync(ReadOnlySequence buffer)
+ {
+ var fCompletionExpected = false;
+ var hr = 0;
+ var nChunks = 0;
+
+ if (buffer.IsSingleSegment)
+ {
+ nChunks = 1;
+ }
+ else
+ {
+ foreach (var memory in buffer)
+ {
+ nChunks++;
+ }
+ }
+
+ if (buffer.IsSingleSegment)
+ {
+ var pDataChunks = stackalloc HttpApiTypes.HTTP_DATA_CHUNK[1];
+
+ fixed (byte* pBuffer = &MemoryMarshal.GetReference(buffer.First.Span))
+ {
+ ref var chunk = ref pDataChunks[0];
+
+ chunk.DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
+ chunk.fromMemory.pBuffer = (IntPtr)pBuffer;
+ chunk.fromMemory.BufferLength = (uint)buffer.Length;
+ hr = NativeMethods.http_write_response_bytes(_pInProcessHandler, pDataChunks, nChunks, out fCompletionExpected);
+ }
+ }
+ else
+ {
+ // REVIEW: Do we need to guard against this getting too big? It seems unlikely that we'd have more than say 10 chunks in real life
+ var pDataChunks = stackalloc HttpApiTypes.HTTP_DATA_CHUNK[nChunks];
+ var currentChunk = 0;
+
+ // REVIEW: We don't really need this list since the memory is already pinned with the default pool,
+ // but shouldn't assume the pool implementation right now. Unfortunately, this causes a heap allocation...
+ var handles = new MemoryHandle[nChunks];
+
+ foreach (var b in buffer)
+ {
+ ref var handle = ref handles[currentChunk];
+ ref var chunk = ref pDataChunks[currentChunk];
+
+ handle = b.Retain(true);
+
+ chunk.DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
+ chunk.fromMemory.BufferLength = (uint)b.Length;
+ chunk.fromMemory.pBuffer = (IntPtr)handle.Pointer;
+
+ currentChunk++;
+ }
+
+ hr = NativeMethods.http_write_response_bytes(_pInProcessHandler, pDataChunks, nChunks, out fCompletionExpected);
+ // Free the handles
+ foreach (var handle in handles)
+ {
+ handle.Dispose();
+ }
+ }
+
+ if (!fCompletionExpected)
+ {
+ _operation.Complete(hr, 0);
+ }
+ return _operation;
+ }
+
+ private unsafe IISAwaitable FlushToIISAsync()
+ {
+ // Calls flush
+ var hr = 0;
+ hr = NativeMethods.http_flush_response_bytes(_pInProcessHandler, out var fCompletionExpected);
+ if (!fCompletionExpected)
+ {
+ _operation.Complete(hr, 0);
+ }
+
+ return _operation;
+ }
+
+ ///
+ /// Main function for control flow with IIS.
+ /// Uses two Pipes (Input and Output) between application calls to Read/Write/FlushAsync
+ /// Control Flow:
+ /// Try to see if there is data written by the application code (using TryRead)
+ /// and write it to IIS.
+ /// Check if the connection has been upgraded and call StartBidirectionalStreams
+ /// if it has.
+ /// Await reading from IIS, which will be cancelled if application code calls Write/FlushAsync.
+ ///
+ /// The Reading and Writing task.
+ public async Task ReadAndWriteLoopAsync()
+ {
+ try
+ {
+ while (true)
+ {
+ // First we check if there is anything to write from the Output pipe
+ // If there is, we call WriteToIISAsync
+ // Check if Output pipe has anything to write to IIS.
+ if (Output.Reader.TryRead(out var readResult))
+ {
+ var buffer = readResult.Buffer;
+
+ try
+ {
+ if (!buffer.IsEmpty)
+ {
+ // Write to IIS buffers
+ // Guaranteed to write the entire buffer to IIS
+ await WriteToIISAsync(buffer);
+ }
+ else if (readResult.IsCompleted)
+ {
+ break;
+ }
+ else
+ {
+ // Flush of zero bytes
+ await FlushToIISAsync();
+ }
+ }
+ finally
+ {
+ // Always Advance the data pointer to the end of the buffer.
+ Output.Reader.AdvanceTo(buffer.End);
+ }
+ }
+
+ // Check if there was an upgrade. If there is, we will replace the request and response bodies with
+ // two seperate loops. These will still be using the same Input and Output pipes here.
+ if (_upgradeTcs?.TrySetResult(null) == true)
+ {
+ // _wasUpgraded will be set at this point, exit the loop and we will check if we upgraded or not
+ // when going to next read/write type.
+ return;
+ }
+
+ // Now we handle the read.
+ var memory = Input.Writer.GetMemory();
+ _inputHandle = memory.Retain(true);
+
+ try
+ {
+ // Lock around invoking ReadFromIISAsync as we don't want to call CancelIo
+ // when calling read
+ var read = await ReadFromIISAsync(memory.Length);
+
+ // read value of 0 == done reading
+ // read value of -1 == read cancelled, still allowed to read but we
+ // need a write to occur first.
+ if (read == 0)
+ {
+ break;
+ }
+ else if (read == -1)
+ {
+ continue;
+ }
+ Input.Writer.Advance(read);
+ }
+ finally
+ {
+ // Always commit any changes to the Input pipe
+ _inputHandle.Dispose();
+ }
+
+ // Flush the read data for the Input Pipe writer
+ var flushResult = await Input.Writer.FlushAsync();
+
+ // If the pipe was closed, we are done reading,
+ if (flushResult.IsCompleted || flushResult.IsCanceled)
+ {
+ break;
+ }
+ }
+
+ // Complete the input writer as we are done reading the request body.
+ Input.Writer.Complete();
+ }
+ catch (Exception ex)
+ {
+ Input.Writer.Complete(ex);
+ }
+
+ await WriteLoopAsync();
+ }
+
+ ///
+ /// Secondary function for control flow with IIS. This is only called once we are done
+ /// reading the request body. We now await reading from the Output pipe.
+ ///
+ ///
+ private async Task WriteLoopAsync()
+ {
+ try
+ {
+ while (true)
+ {
+ // Reading is done, so we will await all reads from the output pipe
+ var readResult = await Output.Reader.ReadAsync();
+
+ // Get data from pipe
+ var buffer = readResult.Buffer;
+
+ try
+ {
+ if (!buffer.IsEmpty)
+ {
+ // Write to IIS buffers
+ // Guaranteed to write the entire buffer to IIS
+ await WriteToIISAsync(buffer);
+ }
+ else if (readResult.IsCompleted)
+ {
+ break;
+ }
+ else
+ {
+ // Flush of zero bytes will
+ await FlushToIISAsync();
+ }
+ }
+ finally
+ {
+ // Always Advance the data pointer to the end of the buffer.
+ Output.Reader.AdvanceTo(buffer.End);
+ }
+ }
+
+ // Close the output pipe as we are done reading from it.
+ Output.Reader.Complete();
+ }
+ catch (Exception ex)
+ {
+ Output.Reader.Complete(ex);
+ }
+ }
+
+ // Always called from within a lock
+ private void DisableReads()
+ {
+ // To avoid concurrent reading and writing, if we have a pending read,
+ // we must cancel it.
+ // _reading will always be false if we upgrade to websockets, so we don't need to check wasUpgrade
+ // Also, we set _reading to false after cancelling to detect redundant calls
+ if (_reading)
+ {
+ _reading = false;
+ // Calls IHttpContext->CancelIo(), which will cause the OnAsyncCompletion handler to fire.
+ NativeMethods.http_cancel_io(_pInProcessHandler);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.Websockets.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.Websockets.cs
new file mode 100644
index 0000000000..fa84c3ce11
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.Websockets.cs
@@ -0,0 +1,225 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.HttpSys.Internal;
+
+namespace Microsoft.AspNetCore.Server.IISIntegration
+{
+ ///
+ /// Represents the websocket portion of the
+ ///
+ internal partial class IISHttpContext
+ {
+ private bool _wasUpgraded; // Used for detecting repeated upgrades in IISHttpContext
+
+ private IISAwaitable _readWebSocketsOperation;
+ private IISAwaitable _writeWebSocketsOperation;
+ private TaskCompletionSource