Extract a layer of IIS Async IO handling (#818)
This commit is contained in:
parent
fbf727e073
commit
22a865b832
|
|
@ -9,8 +9,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
internal static class NativeMethods
|
internal static class NativeMethods
|
||||||
{
|
{
|
||||||
private const int HR_NOT_FOUND = unchecked((int)0x80070490);
|
internal const int HR_OK = 0;
|
||||||
private const int HR_OK = 0;
|
internal const int ERROR_NOT_FOUND = unchecked((int)0x80070490);
|
||||||
|
internal const int ERROR_OPERATION_ABORTED = unchecked((int)0x800703E3);
|
||||||
|
|
||||||
private const string KERNEL32 = "kernel32.dll";
|
private const string KERNEL32 = "kernel32.dll";
|
||||||
|
|
||||||
|
|
@ -238,7 +239,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
var hr = http_cancel_io(pInProcessHandler);
|
var hr = http_cancel_io(pInProcessHandler);
|
||||||
// Async operation finished
|
// Async operation finished
|
||||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363792(v=vs.85).aspx
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363792(v=vs.85).aspx
|
||||||
if (hr == HR_NOT_FOUND)
|
if (hr == ERROR_NOT_FOUND)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
// 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.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.IISIntegration
|
|
||||||
{
|
|
||||||
// Primarily copied from https://github.com/aspnet/KestrelHttpServer/blob/dev/src/Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs
|
|
||||||
internal class IISAwaitable : ICriticalNotifyCompletion
|
|
||||||
{
|
|
||||||
private readonly static Action _callbackCompleted = () => { };
|
|
||||||
|
|
||||||
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) =>
|
|
||||||
{
|
|
||||||
var context = (IISHttpContext)GCHandle.FromIntPtr(pvCompletionContext).Target;
|
|
||||||
|
|
||||||
NativeMethods.HttpGetCompletionInfo(pCompletionInfo, out int cbBytes, out int hr);
|
|
||||||
|
|
||||||
context.CompleteReadWebSockets(hr, cbBytes);
|
|
||||||
|
|
||||||
return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING;
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly NativeMethods.PFN_WEBSOCKET_ASYNC_COMPLETION WriteCallback = (IntPtr pHttpContext, IntPtr pCompletionInfo, IntPtr pvCompletionContext) =>
|
|
||||||
{
|
|
||||||
var context = (IISHttpContext)GCHandle.FromIntPtr(pvCompletionContext).Target;
|
|
||||||
|
|
||||||
NativeMethods.HttpGetCompletionInfo(pCompletionInfo, out int cbBytes, out int hr);
|
|
||||||
|
|
||||||
context.CompleteWriteWebSockets(hr, cbBytes);
|
|
||||||
|
|
||||||
return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING;
|
|
||||||
};
|
|
||||||
|
|
||||||
public IISAwaitable GetAwaiter() => this;
|
|
||||||
public bool IsCompleted => _callback == _callbackCompleted;
|
|
||||||
|
|
||||||
public bool HasContinuation => _callback != null && !IsCompleted;
|
|
||||||
|
|
||||||
public int GetResult()
|
|
||||||
{
|
|
||||||
var exception = _exception;
|
|
||||||
var cbBytes = _cbBytes;
|
|
||||||
var hr = _hr;
|
|
||||||
|
|
||||||
// Reset the awaitable state
|
|
||||||
_exception = null;
|
|
||||||
_cbBytes = 0;
|
|
||||||
_callback = null;
|
|
||||||
_hr = 0;
|
|
||||||
|
|
||||||
if (exception != null)
|
|
||||||
{
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnCompleted(Action continuation)
|
|
||||||
{
|
|
||||||
// There should never be a race between IsCompleted and OnCompleted since both operations
|
|
||||||
// should always be on the libuv thread
|
|
||||||
|
|
||||||
if (_callback == _callbackCompleted ||
|
|
||||||
Interlocked.CompareExchange(ref _callback, continuation, null) == _callbackCompleted)
|
|
||||||
{
|
|
||||||
// Just run it inline
|
|
||||||
Task.Run(continuation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnsafeOnCompleted(Action continuation)
|
|
||||||
{
|
|
||||||
OnCompleted(continuation);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
return Interlocked.Exchange(ref _callback, _callbackCompleted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
@ -250,16 +251,23 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("CoreStrings.UpgradeCannotBeCalledMultipleTimes");
|
throw new InvalidOperationException("CoreStrings.UpgradeCannotBeCalledMultipleTimes");
|
||||||
}
|
}
|
||||||
|
|
||||||
_wasUpgraded = true;
|
_wasUpgraded = true;
|
||||||
|
|
||||||
StatusCode = StatusCodes.Status101SwitchingProtocols;
|
StatusCode = StatusCodes.Status101SwitchingProtocols;
|
||||||
ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols);
|
ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols);
|
||||||
_readWebSocketsOperation = new IISAwaitable();
|
|
||||||
_writeWebSocketsOperation = new IISAwaitable();
|
// If we started reading before calling Upgrade Task should be completed at this point
|
||||||
NativeMethods.HttpEnableWebsockets(_pInProcessHandler);
|
// because read would return 0 syncronosly
|
||||||
|
Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted);
|
||||||
|
|
||||||
|
// Reset reading status to allow restarting with new IO
|
||||||
|
_hasRequestReadingStarted = false;
|
||||||
|
|
||||||
// Upgrade async will cause the stream processing to go into duplex mode
|
// Upgrade async will cause the stream processing to go into duplex mode
|
||||||
await UpgradeAsync();
|
AsyncIO = new WebSocketsAsyncIOEngine(_pInProcessHandler);
|
||||||
|
|
||||||
|
await InitializeResponse(flushHeaders: true);
|
||||||
|
|
||||||
return new DuplexStream(RequestBody, ResponseBody);
|
return new DuplexStream(RequestBody, ResponseBody);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,13 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.HttpSys.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.IISIntegration
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
internal partial class IISHttpContext
|
internal partial class IISHttpContext
|
||||||
{
|
{
|
||||||
private const int HttpDataChunkStackLimit = 128; // 16 bytes per HTTP_DATA_CHUNK
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads data from the Input pipe to the user.
|
/// Reads data from the Input pipe to the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -22,11 +18,14 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal async Task<int> ReadAsync(Memory<byte> memory, CancellationToken cancellationToken)
|
internal async Task<int> ReadAsync(Memory<byte> memory, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
StartProcessingRequestAndResponseBody();
|
if (!_hasRequestReadingStarted)
|
||||||
|
{
|
||||||
|
InitializeRequestIO();
|
||||||
|
}
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await Input.Reader.ReadAsync();
|
var result = await _bodyInputPipe.Reader.ReadAsync();
|
||||||
var readableBuffer = result.Buffer;
|
var readableBuffer = result.Buffer;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -44,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Input.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End);
|
_bodyInputPipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,18 +56,13 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal Task WriteAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken = default(CancellationToken))
|
internal Task WriteAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
|
async Task WriteFirstAsync()
|
||||||
// Want to keep exceptions consistent,
|
|
||||||
if (!_hasResponseStarted)
|
|
||||||
{
|
{
|
||||||
return WriteAsyncAwaited(memory, cancellationToken);
|
await InitializeResponse(flushHeaders: false);
|
||||||
|
await _bodyOutput.WriteAsync(memory, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_stateSync)
|
return !HasResponseStarted ? WriteFirstAsync() : _bodyOutput.WriteAsync(memory, cancellationToken);
|
||||||
{
|
|
||||||
DisableReads();
|
|
||||||
return Output.WriteAsync(memory, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -78,397 +72,99 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
internal Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
if (!_hasResponseStarted)
|
async Task FlushFirstAsync()
|
||||||
{
|
{
|
||||||
return FlushAsyncAwaited(cancellationToken);
|
await InitializeResponse(flushHeaders: true);
|
||||||
}
|
await _bodyOutput.FlushAsync(cancellationToken);
|
||||||
lock (_stateSync)
|
|
||||||
{
|
|
||||||
DisableReads();
|
|
||||||
return Output.FlushAsync(cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return !HasResponseStarted ? FlushFirstAsync() : _bodyOutput.FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartProcessingRequestAndResponseBody()
|
private async Task ReadBody()
|
||||||
{
|
|
||||||
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(ReadOnlyMemory<byte> 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.HttpReadRequestBytes(
|
|
||||||
_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<byte> buffer)
|
|
||||||
{
|
|
||||||
var fCompletionExpected = false;
|
|
||||||
var hr = 0;
|
|
||||||
var nChunks = 0;
|
|
||||||
|
|
||||||
// Count the number of chunks in memory.
|
|
||||||
if (buffer.IsSingleSegment)
|
|
||||||
{
|
|
||||||
nChunks = 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var memory in buffer)
|
|
||||||
{
|
|
||||||
nChunks++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nChunks == 1)
|
|
||||||
{
|
|
||||||
// If there is only a single chunk, use fixed to get a pointer to the buffer
|
|
||||||
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.HttpWriteResponseBytes(_pInProcessHandler, pDataChunks, nChunks, out fCompletionExpected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (nChunks < HttpDataChunkStackLimit)
|
|
||||||
{
|
|
||||||
// To avoid stackoverflows, we will only stackalloc if the write size is less than the StackChunkLimit
|
|
||||||
// The stack size is IIS is by default 128/256 KB, so we are generous with this threshold.
|
|
||||||
var pDataChunks = stackalloc HttpApiTypes.HTTP_DATA_CHUNK[nChunks];
|
|
||||||
hr = WriteSequenceToIIS(nChunks, buffer, pDataChunks, out fCompletionExpected);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Otherwise allocate the chunks on the heap.
|
|
||||||
var chunks = new HttpApiTypes.HTTP_DATA_CHUNK[nChunks];
|
|
||||||
fixed (HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks = chunks)
|
|
||||||
{
|
|
||||||
hr = WriteSequenceToIIS(nChunks, buffer, pDataChunks, out fCompletionExpected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fCompletionExpected)
|
|
||||||
{
|
|
||||||
_operation.Complete(hr, 0);
|
|
||||||
}
|
|
||||||
return _operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe int WriteSequenceToIIS(int nChunks, ReadOnlySequence<byte> buffer, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, out bool fCompletionExpected)
|
|
||||||
{
|
|
||||||
var currentChunk = 0;
|
|
||||||
var hr = 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.Pin();
|
|
||||||
|
|
||||||
chunk.DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
chunk.fromMemory.BufferLength = (uint)b.Length;
|
|
||||||
chunk.fromMemory.pBuffer = (IntPtr)handle.Pointer;
|
|
||||||
|
|
||||||
currentChunk++;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr = NativeMethods.HttpWriteResponseBytes(_pInProcessHandler, pDataChunks, nChunks, out fCompletionExpected);
|
|
||||||
|
|
||||||
// Free the handles
|
|
||||||
foreach (var handle in handles)
|
|
||||||
{
|
|
||||||
handle.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return hr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe IISAwaitable FlushToIISAsync()
|
|
||||||
{
|
|
||||||
// Calls flush
|
|
||||||
var hr = 0;
|
|
||||||
hr = NativeMethods.HttpFlushResponseBytes(_pInProcessHandler, out var fCompletionExpected);
|
|
||||||
if (!fCompletionExpected)
|
|
||||||
{
|
|
||||||
_operation.Complete(hr, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The Reading and Writing task.</returns>
|
|
||||||
private async Task ReadAndWriteLoopAsync()
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// First we check if there is anything to write from the Output pipe
|
var memory = _bodyInputPipe.Writer.GetMemory();
|
||||||
// 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
|
var read = await AsyncIO.ReadAsync(memory);
|
||||||
{
|
|
||||||
if (!buffer.IsEmpty)
|
// End of body
|
||||||
{
|
if (read == 0)
|
||||||
// Write to IIS buffers
|
{
|
||||||
// Guaranteed to write the entire buffer to IIS
|
break;
|
||||||
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
|
// Read was not canceled because of incoming write or IO stopping
|
||||||
// two seperate loops. These will still be using the same Input and Output pipes here.
|
if (read != -1)
|
||||||
if (_upgradeTcs?.TrySetResult(null) == true)
|
|
||||||
{
|
{
|
||||||
// _wasUpgraded will be set at this point, exit the loop and we will check if we upgraded or not
|
_bodyInputPipe.Writer.Advance(read);
|
||||||
// when going to next read/write type.
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we handle the read.
|
var result = await _bodyInputPipe.Writer.FlushAsync();
|
||||||
var memory = Input.Writer.GetMemory();
|
|
||||||
_inputHandle = memory.Pin();
|
|
||||||
|
|
||||||
try
|
if (result.IsCompleted || result.IsCanceled)
|
||||||
{
|
|
||||||
// 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete the input writer as we are done reading the request body.
|
|
||||||
Input.Writer.Complete();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Input.Writer.Complete(ex);
|
_bodyInputPipe.Writer.Complete(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_bodyInputPipe.Writer.Complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
await WriteLoopAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private async Task WriteBody(bool flush = false)
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
private async Task WriteLoopAsync()
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Reading is done, so we will await all reads from the output pipe
|
var result = await _bodyOutput.Reader.ReadAsync();
|
||||||
var readResult = await Output.Reader.ReadAsync();
|
|
||||||
|
|
||||||
// Get data from pipe
|
var buffer = result.Buffer;
|
||||||
var buffer = readResult.Buffer;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!buffer.IsEmpty)
|
if (!buffer.IsEmpty)
|
||||||
{
|
{
|
||||||
// Write to IIS buffers
|
await AsyncIO.WriteAsync(buffer);
|
||||||
// Guaranteed to write the entire buffer to IIS
|
|
||||||
await WriteToIISAsync(buffer);
|
|
||||||
}
|
}
|
||||||
else if (readResult.IsCompleted)
|
|
||||||
|
// if request is done no need to flush, http.sys would do it for us
|
||||||
|
if (result.IsCompleted)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
flush = flush | result.IsCanceled;
|
||||||
|
|
||||||
|
if (flush)
|
||||||
{
|
{
|
||||||
// Flush of zero bytes will
|
await AsyncIO.FlushAsync();
|
||||||
await FlushToIISAsync();
|
flush = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Always Advance the data pointer to the end of the buffer.
|
_bodyOutput.Reader.AdvanceTo(buffer.End);
|
||||||
Output.Reader.AdvanceTo(buffer.End);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the output pipe as we are done reading from it.
|
|
||||||
Output.Reader.Complete();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Output.Reader.Complete(ex);
|
_bodyOutput.Reader.Complete(ex);
|
||||||
}
|
}
|
||||||
}
|
finally
|
||||||
|
|
||||||
// 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;
|
_bodyOutput.Reader.Complete();
|
||||||
// Calls IHttpContext->CancelIo(), which will cause the OnAsyncCompletion handler to fire.
|
|
||||||
NativeMethods.HttpTryCancelIO(_pInProcessHandler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
// 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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the websocket portion of the <see cref="IISHttpContext"/>
|
|
||||||
/// </summary>
|
|
||||||
internal partial class IISHttpContext
|
|
||||||
{
|
|
||||||
private bool _wasUpgraded; // Used for detecting repeated upgrades in IISHttpContext
|
|
||||||
|
|
||||||
private IISAwaitable _readWebSocketsOperation;
|
|
||||||
private IISAwaitable _writeWebSocketsOperation;
|
|
||||||
private TaskCompletionSource<object> _upgradeTcs;
|
|
||||||
|
|
||||||
private Task StartBidirectionalStream()
|
|
||||||
{
|
|
||||||
// IIS allows for websocket support and duplex channels only on Win8 and above
|
|
||||||
// This allows us to have two tasks for reading the request and writing the response
|
|
||||||
var readWebsocketTask = ReadWebSockets();
|
|
||||||
var writeWebsocketTask = WriteWebSockets();
|
|
||||||
return Task.WhenAll(readWebsocketTask, writeWebsocketTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpgradeAsync()
|
|
||||||
{
|
|
||||||
if (_upgradeTcs == null)
|
|
||||||
{
|
|
||||||
_upgradeTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
// Flush any contents of the OutputPipe before upgrading to websockets.
|
|
||||||
await FlushAsync();
|
|
||||||
await _upgradeTcs.Task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe IISAwaitable ReadWebSocketsFromIISAsync(int length)
|
|
||||||
{
|
|
||||||
var hr = 0;
|
|
||||||
int dwReceivedBytes;
|
|
||||||
bool fCompletionExpected;
|
|
||||||
|
|
||||||
// For websocket calls, we can directly provide a callback function to be called once the websocket operation completes.
|
|
||||||
hr = NativeMethods.HttpWebsocketsReadBytes(
|
|
||||||
_pInProcessHandler,
|
|
||||||
(byte*)_inputHandle.Pointer,
|
|
||||||
length,
|
|
||||||
IISAwaitable.ReadCallback,
|
|
||||||
(IntPtr)_thisHandle,
|
|
||||||
out dwReceivedBytes,
|
|
||||||
out fCompletionExpected);
|
|
||||||
if (!fCompletionExpected)
|
|
||||||
{
|
|
||||||
CompleteReadWebSockets(hr, dwReceivedBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _readWebSocketsOperation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe IISAwaitable WriteWebSocketsFromIISAsync(ReadOnlySequence<byte> 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.HttpWebsocketsWriteBytes(_pInProcessHandler, pDataChunks, nChunks, IISAwaitable.WriteCallback, (IntPtr)_thisHandle, 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.Pin();
|
|
||||||
|
|
||||||
chunk.DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
chunk.fromMemory.BufferLength = (uint)b.Length;
|
|
||||||
chunk.fromMemory.pBuffer = (IntPtr)handle.Pointer;
|
|
||||||
|
|
||||||
currentChunk++;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr = NativeMethods.HttpWebsocketsWriteBytes(_pInProcessHandler, pDataChunks, nChunks, IISAwaitable.WriteCallback, (IntPtr)_thisHandle, out fCompletionExpected);
|
|
||||||
|
|
||||||
foreach (var handle in handles)
|
|
||||||
{
|
|
||||||
handle.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fCompletionExpected)
|
|
||||||
{
|
|
||||||
CompleteWriteWebSockets(hr, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _writeWebSocketsOperation;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CompleteWriteWebSockets(int hr, int cbBytes)
|
|
||||||
{
|
|
||||||
_writeWebSocketsOperation.Complete(hr, cbBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CompleteReadWebSockets(int hr, int cbBytes)
|
|
||||||
{
|
|
||||||
_readWebSocketsOperation.Complete(hr, cbBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReadWebSockets()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var memory = Input.Writer.GetMemory();
|
|
||||||
_inputHandle = memory.Pin();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int read = 0;
|
|
||||||
read = await ReadWebSocketsFromIISAsync(memory.Length);
|
|
||||||
|
|
||||||
if (read == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Input.Writer.Advance(read);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_inputHandle.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await Input.Writer.FlushAsync();
|
|
||||||
|
|
||||||
if (result.IsCompleted || result.IsCanceled)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Input.Writer.Complete();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Input.Writer.Complete(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task WriteWebSockets()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var result = await Output.Reader.ReadAsync();
|
|
||||||
|
|
||||||
var buffer = result.Buffer;
|
|
||||||
var consumed = buffer.End;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!buffer.IsEmpty)
|
|
||||||
{
|
|
||||||
await WriteWebSocketsFromIISAsync(buffer);
|
|
||||||
}
|
|
||||||
else if (result.IsCompleted)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Output.Reader.AdvanceTo(consumed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Output.Reader.Complete();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Output.Reader.Complete(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.HttpSys.Internal;
|
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.IISIntegration
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
|
|
@ -33,15 +34,13 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
|
||||||
private readonly IISOptions _options;
|
private readonly IISOptions _options;
|
||||||
|
|
||||||
private bool _reading; // To know whether we are currently in a read operation.
|
|
||||||
private volatile bool _hasResponseStarted;
|
private volatile bool _hasResponseStarted;
|
||||||
|
private volatile bool _hasRequestReadingStarted;
|
||||||
|
|
||||||
private int _statusCode;
|
private int _statusCode;
|
||||||
private string _reasonPhrase;
|
private string _reasonPhrase;
|
||||||
private readonly object _onStartingSync = new object();
|
private readonly object _onStartingSync = new object();
|
||||||
private readonly object _onCompletedSync = new object();
|
private readonly object _onCompletedSync = new object();
|
||||||
private readonly object _stateSync = new object();
|
|
||||||
protected readonly object _createReadWriteBodySync = new object();
|
|
||||||
|
|
||||||
protected Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
|
protected Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
|
||||||
protected Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted;
|
protected Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted;
|
||||||
|
|
@ -51,16 +50,20 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
private readonly IISHttpServer _server;
|
private readonly IISHttpServer _server;
|
||||||
|
|
||||||
private GCHandle _thisHandle;
|
private GCHandle _thisHandle;
|
||||||
private MemoryHandle _inputHandle;
|
protected Task _readBodyTask;
|
||||||
private IISAwaitable _operation = new IISAwaitable();
|
protected Task _writeBodyTask;
|
||||||
protected Task _processBodiesTask;
|
|
||||||
|
|
||||||
|
private bool _wasUpgraded;
|
||||||
protected int _requestAborted;
|
protected int _requestAborted;
|
||||||
|
|
||||||
|
protected Pipe _bodyInputPipe;
|
||||||
|
protected OutputProducer _bodyOutput;
|
||||||
|
|
||||||
private const string NtlmString = "NTLM";
|
private const string NtlmString = "NTLM";
|
||||||
private const string NegotiateString = "Negotiate";
|
private const string NegotiateString = "Negotiate";
|
||||||
private const string BasicString = "Basic";
|
private const string BasicString = "Basic";
|
||||||
|
|
||||||
|
|
||||||
internal unsafe IISHttpContext(MemoryPool<byte> memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server)
|
internal unsafe IISHttpContext(MemoryPool<byte> memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server)
|
||||||
: base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler))
|
: base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler))
|
||||||
{
|
{
|
||||||
|
|
@ -89,8 +92,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
internal WindowsPrincipal WindowsUser { get; set; }
|
internal WindowsPrincipal WindowsUser { get; set; }
|
||||||
public Stream RequestBody { get; set; }
|
public Stream RequestBody { get; set; }
|
||||||
public Stream ResponseBody { get; set; }
|
public Stream ResponseBody { get; set; }
|
||||||
public Pipe Input { get; set; }
|
|
||||||
public OutputProducer Output { get; set; }
|
protected IAsyncIOEngine AsyncIO { get; set; }
|
||||||
|
|
||||||
public IHeaderDictionary RequestHeaders { get; set; }
|
public IHeaderDictionary RequestHeaders { get; set; }
|
||||||
public IHeaderDictionary ResponseHeaders { get; set; }
|
public IHeaderDictionary ResponseHeaders { get; set; }
|
||||||
|
|
@ -153,7 +156,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
RequestBody = new IISHttpRequestBody(this);
|
RequestBody = new IISHttpRequestBody(this);
|
||||||
ResponseBody = new IISHttpResponseBody(this);
|
ResponseBody = new IISHttpResponseBody(this);
|
||||||
|
|
||||||
Input = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize));
|
|
||||||
var pipe = new Pipe(
|
var pipe = new Pipe(
|
||||||
new PipeOptions(
|
new PipeOptions(
|
||||||
_memoryPool,
|
_memoryPool,
|
||||||
|
|
@ -161,7 +164,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
pauseWriterThreshold: PauseWriterThreshold,
|
pauseWriterThreshold: PauseWriterThreshold,
|
||||||
resumeWriterThreshold: ResumeWriterTheshold,
|
resumeWriterThreshold: ResumeWriterTheshold,
|
||||||
minimumSegmentSize: MinAllocBufferSize));
|
minimumSegmentSize: MinAllocBufferSize));
|
||||||
Output = new OutputProducer(pipe);
|
_bodyOutput = new OutputProducer(pipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int StatusCode
|
public int StatusCode
|
||||||
|
|
@ -169,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
get { return _statusCode; }
|
get { return _statusCode; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_hasResponseStarted)
|
if (HasResponseStarted)
|
||||||
{
|
{
|
||||||
ThrowResponseAlreadyStartedException(nameof(StatusCode));
|
ThrowResponseAlreadyStartedException(nameof(StatusCode));
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +185,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
get { return _reasonPhrase; }
|
get { return _reasonPhrase; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_hasResponseStarted)
|
if (HasResponseStarted)
|
||||||
{
|
{
|
||||||
ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
|
ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
|
||||||
}
|
}
|
||||||
|
|
@ -190,12 +193,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IISHttpServer Server
|
internal IISHttpServer Server => _server;
|
||||||
{
|
|
||||||
get { return _server; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializeResponseAwaited()
|
private async Task InitializeResponse(bool flushHeaders)
|
||||||
{
|
{
|
||||||
await FireOnStarting();
|
await FireOnStarting();
|
||||||
|
|
||||||
|
|
@ -204,7 +204,46 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
ThrowResponseAbortedException();
|
ThrowResponseAbortedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await ProduceStart(appCompleted: false);
|
await ProduceStart(flushHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProduceStart(bool flushHeaders)
|
||||||
|
{
|
||||||
|
Debug.Assert(_hasResponseStarted == false);
|
||||||
|
|
||||||
|
_hasResponseStarted = true;
|
||||||
|
|
||||||
|
SetResponseHeaders();
|
||||||
|
|
||||||
|
EnsureIOInitialized();
|
||||||
|
|
||||||
|
if (flushHeaders)
|
||||||
|
{
|
||||||
|
await AsyncIO.FlushAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeBodyTask = WriteBody(!flushHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeRequestIO()
|
||||||
|
{
|
||||||
|
Debug.Assert(!_hasRequestReadingStarted);
|
||||||
|
|
||||||
|
_hasRequestReadingStarted = true;
|
||||||
|
|
||||||
|
EnsureIOInitialized();
|
||||||
|
|
||||||
|
_bodyInputPipe = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize));
|
||||||
|
_readBodyTask = ReadBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureIOInitialized()
|
||||||
|
{
|
||||||
|
// If at this point request was not upgraded just start a normal IO engine
|
||||||
|
if (AsyncIO == null)
|
||||||
|
{
|
||||||
|
AsyncIO = new AsyncIOEngine(_pInProcessHandler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ThrowResponseAbortedException()
|
private void ThrowResponseAbortedException()
|
||||||
|
|
@ -212,38 +251,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
throw new ObjectDisposedException("Unhandled application exception", _applicationException);
|
throw new ObjectDisposedException("Unhandled application exception", _applicationException);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProduceStart(bool appCompleted)
|
|
||||||
{
|
|
||||||
if (_hasResponseStarted)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasResponseStarted = true;
|
|
||||||
|
|
||||||
SendResponseHeaders(appCompleted);
|
|
||||||
|
|
||||||
// On first flush for websockets, we need to flush the headers such that
|
|
||||||
// IIS will know that an upgrade occured.
|
|
||||||
// If we don't have anything on the Output pipe, the TryRead in ReadAndWriteLoopAsync
|
|
||||||
// will fail and we will signal the upgradeTcs that we are upgrading. However, we still
|
|
||||||
// didn't flush. To fix this, we flush 0 bytes right after writing the headers.
|
|
||||||
Task flushTask;
|
|
||||||
lock (_stateSync)
|
|
||||||
{
|
|
||||||
DisableReads();
|
|
||||||
flushTask = Output.FlushAsync();
|
|
||||||
}
|
|
||||||
await flushTask;
|
|
||||||
|
|
||||||
StartProcessingRequestAndResponseBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Task ProduceEnd()
|
protected Task ProduceEnd()
|
||||||
{
|
{
|
||||||
if (_applicationException != null)
|
if (_applicationException != null)
|
||||||
{
|
{
|
||||||
if (_hasResponseStarted)
|
if (HasResponseStarted)
|
||||||
{
|
{
|
||||||
// We can no longer change the response, so we simply close the connection.
|
// We can no longer change the response, so we simply close the connection.
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
@ -258,7 +270,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_hasResponseStarted)
|
if (!HasResponseStarted)
|
||||||
{
|
{
|
||||||
return ProduceEndAwaited();
|
return ProduceEndAwaited();
|
||||||
}
|
}
|
||||||
|
|
@ -275,27 +287,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
|
||||||
private async Task ProduceEndAwaited()
|
private async Task ProduceEndAwaited()
|
||||||
{
|
{
|
||||||
if (_hasResponseStarted)
|
await ProduceStart(flushHeaders: true);
|
||||||
{
|
await _bodyOutput.FlushAsync(default);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasResponseStarted = true;
|
|
||||||
|
|
||||||
SendResponseHeaders(appCompleted: true);
|
|
||||||
StartProcessingRequestAndResponseBody();
|
|
||||||
|
|
||||||
Task flushAsync;
|
|
||||||
|
|
||||||
lock (_stateSync)
|
|
||||||
{
|
|
||||||
DisableReads();
|
|
||||||
flushAsync = Output.FlushAsync();
|
|
||||||
}
|
|
||||||
await flushAsync;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void SendResponseHeaders(bool appCompleted)
|
public unsafe void SetResponseHeaders()
|
||||||
{
|
{
|
||||||
// Verifies we have sent the statuscode before writing a header
|
// Verifies we have sent the statuscode before writing a header
|
||||||
var reasonPhrase = string.IsNullOrEmpty(ReasonPhrase) ? ReasonPhrases.GetReasonPhrase(StatusCode) : ReasonPhrase;
|
var reasonPhrase = string.IsNullOrEmpty(ReasonPhrase) ? ReasonPhrases.GetReasonPhrase(StatusCode) : ReasonPhrase;
|
||||||
|
|
@ -348,7 +344,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
lock (_onStartingSync)
|
lock (_onStartingSync)
|
||||||
{
|
{
|
||||||
if (_hasResponseStarted)
|
if (HasResponseStarted)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Response already started");
|
throw new InvalidOperationException("Response already started");
|
||||||
}
|
}
|
||||||
|
|
@ -439,8 +435,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
|
||||||
public void PostCompletion(NativeMethods.REQUEST_NOTIFICATION_STATUS requestNotificationStatus)
|
public void PostCompletion(NativeMethods.REQUEST_NOTIFICATION_STATUS requestNotificationStatus)
|
||||||
{
|
{
|
||||||
Debug.Assert(!_operation.HasContinuation, "Pending async operation!");
|
|
||||||
|
|
||||||
NativeMethods.HttpSetCompletionStatus(_pInProcessHandler, requestNotificationStatus);
|
NativeMethods.HttpSetCompletionStatus(_pInProcessHandler, requestNotificationStatus);
|
||||||
NativeMethods.HttpPostCompletion(_pInProcessHandler, 0);
|
NativeMethods.HttpPostCompletion(_pInProcessHandler, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -450,18 +444,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
NativeMethods.HttpIndicateCompletion(_pInProcessHandler, notificationStatus);
|
NativeMethods.HttpIndicateCompletion(_pInProcessHandler, notificationStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void OnAsyncCompletion(int hr, int cbBytes)
|
internal void OnAsyncCompletion(int hr, int bytes)
|
||||||
{
|
{
|
||||||
// Must acquire the _stateSync here as anytime we call complete, we need to hold the lock
|
AsyncIO.NotifyCompletion(hr, bytes);
|
||||||
// to avoid races with cancellation.
|
|
||||||
Action continuation;
|
|
||||||
lock (_stateSync)
|
|
||||||
{
|
|
||||||
_reading = false;
|
|
||||||
continuation = _operation.GetCompletion(hr, cbBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool disposedValue = false; // To detect redundant calls
|
private bool disposedValue = false; // To detect redundant calls
|
||||||
|
|
|
||||||
|
|
@ -80,18 +80,24 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// The app is finished and there should be nobody writing to the response pipe
|
// Complete response writer and request reader pipe sides
|
||||||
Output.Dispose();
|
_bodyOutput.Dispose();
|
||||||
|
_bodyInputPipe?.Reader.Complete();
|
||||||
|
|
||||||
// The app is finished and there should be nobody reading from the request pipe
|
// Allow writes to drain
|
||||||
Input.Reader.Complete();
|
if (_writeBodyTask != null)
|
||||||
|
|
||||||
Task processBodiesTask;
|
|
||||||
lock (_createReadWriteBodySync)
|
|
||||||
{
|
{
|
||||||
processBodiesTask = _processBodiesTask;
|
await _writeBodyTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancell all remaining IO, thre might be reads pending if not entire request body was sent
|
||||||
|
// by client
|
||||||
|
AsyncIO.Dispose();
|
||||||
|
|
||||||
|
if (_readBodyTask != null)
|
||||||
|
{
|
||||||
|
await _readBodyTask;
|
||||||
}
|
}
|
||||||
await processBodiesTask;
|
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override unsafe Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var memory = new Memory<byte>(buffer, offset, count);
|
var memory = new Memory<byte>(buffer, offset, count);
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe override void Write(byte[] buffer, int offset, int count)
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.IISIntegration
|
|
||||||
{
|
|
||||||
internal static class IISServerConstants
|
|
||||||
{
|
|
||||||
internal const int HResultCancelIO = -2147023901;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class AsyncIOEngine
|
||||||
|
{
|
||||||
|
internal class AsyncFlushOperation : AsyncIOOperation
|
||||||
|
{
|
||||||
|
private readonly AsyncIOEngine _engine;
|
||||||
|
|
||||||
|
private IntPtr _requestHandler;
|
||||||
|
|
||||||
|
public AsyncFlushOperation(AsyncIOEngine engine)
|
||||||
|
{
|
||||||
|
_engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(IntPtr requestHandler)
|
||||||
|
{
|
||||||
|
_requestHandler = requestHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool InvokeOperation(out int hr, out int bytes)
|
||||||
|
{
|
||||||
|
bytes = 0;
|
||||||
|
hr = NativeMethods.HttpFlushResponseBytes(_requestHandler, out var fCompletionExpected);
|
||||||
|
|
||||||
|
return !fCompletionExpected;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetOperation()
|
||||||
|
{
|
||||||
|
base.ResetOperation();
|
||||||
|
|
||||||
|
_requestHandler = default;
|
||||||
|
_engine.ReturnOperation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class AsyncIOEngine
|
||||||
|
{
|
||||||
|
internal class AsyncReadOperation : AsyncIOOperation
|
||||||
|
{
|
||||||
|
private readonly AsyncIOEngine _engine;
|
||||||
|
|
||||||
|
private MemoryHandle _inputHandle;
|
||||||
|
|
||||||
|
private IntPtr _requestHandler;
|
||||||
|
|
||||||
|
private Memory<byte> _memory;
|
||||||
|
|
||||||
|
public AsyncReadOperation(AsyncIOEngine engine)
|
||||||
|
{
|
||||||
|
_engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(IntPtr requestHandler, Memory<byte> memory)
|
||||||
|
{
|
||||||
|
_requestHandler = requestHandler;
|
||||||
|
_memory = memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe bool InvokeOperation(out int hr, out int bytes)
|
||||||
|
{
|
||||||
|
_inputHandle = _memory.Pin();
|
||||||
|
hr = NativeMethods.HttpReadRequestBytes(
|
||||||
|
_requestHandler,
|
||||||
|
(byte*)_inputHandle.Pointer,
|
||||||
|
_memory.Length,
|
||||||
|
out bytes,
|
||||||
|
out bool completionExpected);
|
||||||
|
|
||||||
|
return !completionExpected;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetOperation()
|
||||||
|
{
|
||||||
|
base.ResetOperation();
|
||||||
|
|
||||||
|
_memory = default;
|
||||||
|
_inputHandle.Dispose();
|
||||||
|
_inputHandle = default;
|
||||||
|
_requestHandler = default;
|
||||||
|
|
||||||
|
_engine.ReturnOperation(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void FreeOperationResources(int hr, int bytes)
|
||||||
|
{
|
||||||
|
_inputHandle.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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 Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class AsyncIOEngine
|
||||||
|
{
|
||||||
|
private class AsyncWriteOperation : AsyncWriteOperationBase
|
||||||
|
{
|
||||||
|
private readonly AsyncIOEngine _engine;
|
||||||
|
|
||||||
|
public AsyncWriteOperation(AsyncIOEngine engine)
|
||||||
|
{
|
||||||
|
_engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe int WriteChunks(IntPtr requestHandler, int chunkCount, HttpApiTypes.HTTP_DATA_CHUNK* dataChunks,
|
||||||
|
out bool completionExpected)
|
||||||
|
{
|
||||||
|
return NativeMethods.HttpWriteResponseBytes(requestHandler, dataChunks, chunkCount, out completionExpected);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetOperation()
|
||||||
|
{
|
||||||
|
base.ResetOperation();
|
||||||
|
|
||||||
|
_engine.ReturnOperation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class AsyncIOEngine : IAsyncIOEngine
|
||||||
|
{
|
||||||
|
private readonly IntPtr _handler;
|
||||||
|
|
||||||
|
private bool _stopped;
|
||||||
|
|
||||||
|
private AsyncIOOperation _nextOperation;
|
||||||
|
private AsyncIOOperation _runningOperation;
|
||||||
|
|
||||||
|
private AsyncReadOperation _cachedAsyncReadOperation;
|
||||||
|
private AsyncWriteOperation _cachedAsyncWriteOperation;
|
||||||
|
private AsyncFlushOperation _cachedAsyncFlushOperation;
|
||||||
|
|
||||||
|
public AsyncIOEngine(IntPtr handler)
|
||||||
|
{
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<int> ReadAsync(Memory<byte> memory)
|
||||||
|
{
|
||||||
|
var read = GetReadOperation();
|
||||||
|
read.Initialize(_handler, memory);
|
||||||
|
Run(read);
|
||||||
|
return new ValueTask<int>(read, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<int> WriteAsync(ReadOnlySequence<byte> data)
|
||||||
|
{
|
||||||
|
var write = GetWriteOperation();
|
||||||
|
write.Initialize(_handler, data);
|
||||||
|
Run(write);
|
||||||
|
return new ValueTask<int>(write, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run(AsyncIOOperation ioOperation)
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (_stopped)
|
||||||
|
{
|
||||||
|
throw new IOException("IO stopped", NativeMethods.ERROR_OPERATION_ABORTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_runningOperation != null)
|
||||||
|
{
|
||||||
|
if (_nextOperation == null)
|
||||||
|
{
|
||||||
|
_nextOperation = ioOperation;
|
||||||
|
|
||||||
|
// If there is an active read cancel it
|
||||||
|
if (_runningOperation is AsyncReadOperation)
|
||||||
|
{
|
||||||
|
NativeMethods.HttpTryCancelIO(_handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Only one queued operation is allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// we are just starting operation so there would be no
|
||||||
|
// continuation registered
|
||||||
|
var completed = ioOperation.Invoke() != null;
|
||||||
|
|
||||||
|
// operation went async
|
||||||
|
if (!completed)
|
||||||
|
{
|
||||||
|
_runningOperation = ioOperation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ValueTask FlushAsync()
|
||||||
|
{
|
||||||
|
var flush = GetFlushOperation();
|
||||||
|
flush.Initialize(_handler);
|
||||||
|
Run(flush);
|
||||||
|
return new ValueTask(flush, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyCompletion(int hr, int bytes)
|
||||||
|
{
|
||||||
|
AsyncIOOperation.AsyncContinuation continuation;
|
||||||
|
AsyncIOOperation.AsyncContinuation? nextContinuation = null;
|
||||||
|
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
Debug.Assert(_runningOperation != null);
|
||||||
|
|
||||||
|
continuation = _runningOperation.Complete(hr, bytes);
|
||||||
|
|
||||||
|
var next = _nextOperation;
|
||||||
|
_nextOperation = null;
|
||||||
|
_runningOperation = null;
|
||||||
|
|
||||||
|
if (next != null)
|
||||||
|
{
|
||||||
|
if (_stopped)
|
||||||
|
{
|
||||||
|
// Abort next operation if IO is stopped
|
||||||
|
nextContinuation = next.Complete(NativeMethods.ERROR_OPERATION_ABORTED, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nextContinuation = next.Invoke();
|
||||||
|
|
||||||
|
// operation went async
|
||||||
|
if (nextContinuation == null)
|
||||||
|
{
|
||||||
|
_runningOperation = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.Invoke();
|
||||||
|
nextContinuation?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
_stopped = true;
|
||||||
|
NativeMethods.HttpTryCancelIO(_handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncReadOperation GetReadOperation() =>
|
||||||
|
Interlocked.Exchange(ref _cachedAsyncReadOperation, null) ??
|
||||||
|
new AsyncReadOperation(this);
|
||||||
|
|
||||||
|
private AsyncWriteOperation GetWriteOperation() =>
|
||||||
|
Interlocked.Exchange(ref _cachedAsyncWriteOperation, null) ??
|
||||||
|
new AsyncWriteOperation(this);
|
||||||
|
|
||||||
|
private AsyncFlushOperation GetFlushOperation() =>
|
||||||
|
Interlocked.Exchange(ref _cachedAsyncFlushOperation, null) ??
|
||||||
|
new AsyncFlushOperation(this);
|
||||||
|
|
||||||
|
private void ReturnOperation(AsyncReadOperation operation)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _cachedAsyncReadOperation, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReturnOperation(AsyncWriteOperation operation)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _cachedAsyncWriteOperation, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReturnOperation(AsyncFlushOperation operation)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _cachedAsyncFlushOperation, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks.Sources;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal abstract class AsyncIOOperation: IValueTaskSource<int>, IValueTaskSource
|
||||||
|
{
|
||||||
|
private static readonly Action<object> CallbackCompleted = _ => { Debug.Assert(false, "Should not be invoked"); };
|
||||||
|
|
||||||
|
private Action<object> _continuation;
|
||||||
|
private object _state;
|
||||||
|
private int _result;
|
||||||
|
|
||||||
|
private Exception _exception;
|
||||||
|
|
||||||
|
public ValueTaskSourceStatus GetStatus(short token)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(Volatile.Read(ref _continuation), null))
|
||||||
|
{
|
||||||
|
return ValueTaskSourceStatus.Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _exception != null ? ValueTaskSourceStatus.Succeeded : ValueTaskSourceStatus.Faulted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
|
||||||
|
{
|
||||||
|
if (_state != null)
|
||||||
|
{
|
||||||
|
ThrowMultipleContinuations();
|
||||||
|
}
|
||||||
|
|
||||||
|
_state = state;
|
||||||
|
|
||||||
|
var previousContinuation = Interlocked.CompareExchange(ref _continuation, continuation, null);
|
||||||
|
|
||||||
|
if (previousContinuation != null)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(previousContinuation, CallbackCompleted))
|
||||||
|
{
|
||||||
|
ThrowMultipleContinuations();
|
||||||
|
}
|
||||||
|
|
||||||
|
new AsyncContinuation(continuation, state).Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ThrowMultipleContinuations()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Multiple awaiters are not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void IValueTaskSource.GetResult(short token)
|
||||||
|
{
|
||||||
|
var exception = _exception;
|
||||||
|
|
||||||
|
ResetOperation();
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetResult(short token)
|
||||||
|
{
|
||||||
|
var exception = _exception;
|
||||||
|
var result = _result;
|
||||||
|
|
||||||
|
ResetOperation();
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncContinuation? Invoke()
|
||||||
|
{
|
||||||
|
if (InvokeOperation(out var hr, out var bytes))
|
||||||
|
{
|
||||||
|
return Complete(hr, bytes);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract bool InvokeOperation(out int hr, out int bytes);
|
||||||
|
|
||||||
|
public AsyncContinuation Complete(int hr, int bytes)
|
||||||
|
{
|
||||||
|
if (hr != NativeMethods.ERROR_OPERATION_ABORTED)
|
||||||
|
{
|
||||||
|
_result = bytes;
|
||||||
|
if (hr != NativeMethods.HR_OK)
|
||||||
|
{
|
||||||
|
_exception = new IOException("IO exception occurred", hr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_result = -1;
|
||||||
|
_exception = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncContinuation asyncContinuation = default;
|
||||||
|
var continuation = Interlocked.CompareExchange(ref _continuation, CallbackCompleted, null);
|
||||||
|
if (continuation != null)
|
||||||
|
{
|
||||||
|
asyncContinuation = new AsyncContinuation(continuation, _state);
|
||||||
|
}
|
||||||
|
|
||||||
|
FreeOperationResources(hr, bytes);
|
||||||
|
|
||||||
|
return asyncContinuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void FreeOperationResources(int hr, int bytes) { }
|
||||||
|
|
||||||
|
protected virtual void ResetOperation()
|
||||||
|
{
|
||||||
|
_exception = null;
|
||||||
|
_result = int.MinValue;
|
||||||
|
_state = null;
|
||||||
|
_continuation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct AsyncContinuation
|
||||||
|
{
|
||||||
|
public Action<object> Continuation { get; }
|
||||||
|
public object State { get; }
|
||||||
|
|
||||||
|
public AsyncContinuation(Action<object> continuation, object state)
|
||||||
|
{
|
||||||
|
Continuation = continuation;
|
||||||
|
State = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invoke()
|
||||||
|
{
|
||||||
|
if (Continuation != null)
|
||||||
|
{
|
||||||
|
// TODO: use generic overload when code moved to be netcoreapp only
|
||||||
|
var continuation = Continuation;
|
||||||
|
var state = State;
|
||||||
|
ThreadPool.QueueUserWorkItem(_ => continuation(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// 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 Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal abstract class AsyncWriteOperationBase : AsyncIOOperation
|
||||||
|
{
|
||||||
|
private const int HttpDataChunkStackLimit = 128; // 16 bytes per HTTP_DATA_CHUNK
|
||||||
|
|
||||||
|
private IntPtr _requestHandler;
|
||||||
|
private ReadOnlySequence<byte> _buffer;
|
||||||
|
private MemoryHandle[] _handles;
|
||||||
|
|
||||||
|
public void Initialize(IntPtr requestHandler, ReadOnlySequence<byte> buffer)
|
||||||
|
{
|
||||||
|
_requestHandler = requestHandler;
|
||||||
|
_buffer = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe bool InvokeOperation(out int hr, out int bytes)
|
||||||
|
{
|
||||||
|
if (_buffer.Length > int.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Writes larger then {int.MaxValue} are not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool completionExpected;
|
||||||
|
var chunkCount = GetChunkCount();
|
||||||
|
|
||||||
|
var bufferLength = (int)_buffer.Length;
|
||||||
|
|
||||||
|
if (chunkCount < HttpDataChunkStackLimit)
|
||||||
|
{
|
||||||
|
// To avoid stackoverflows, we will only stackalloc if the write size is less than the StackChunkLimit
|
||||||
|
// The stack size is IIS is by default 128/256 KB, so we are generous with this threshold.
|
||||||
|
var chunks = stackalloc HttpApiTypes.HTTP_DATA_CHUNK[chunkCount];
|
||||||
|
hr = WriteSequence(chunkCount, _buffer, chunks, out completionExpected);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Otherwise allocate the chunks on the heap.
|
||||||
|
var chunks = new HttpApiTypes.HTTP_DATA_CHUNK[chunkCount];
|
||||||
|
fixed (HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks = chunks)
|
||||||
|
{
|
||||||
|
hr = WriteSequence(chunkCount, _buffer, pDataChunks, out completionExpected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes = bufferLength;
|
||||||
|
return !completionExpected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void FreeOperationResources(int hr, int bytes)
|
||||||
|
{
|
||||||
|
// Free the handles
|
||||||
|
foreach (var handle in _handles)
|
||||||
|
{
|
||||||
|
handle.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetOperation()
|
||||||
|
{
|
||||||
|
base.ResetOperation();
|
||||||
|
|
||||||
|
_requestHandler = default;
|
||||||
|
_buffer = default;
|
||||||
|
_handles.AsSpan().Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetChunkCount()
|
||||||
|
{
|
||||||
|
if (_buffer.IsSingleSegment)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
foreach (var _ in _buffer)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe int WriteSequence(int nChunks, ReadOnlySequence<byte> buffer, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, out bool fCompletionExpected)
|
||||||
|
{
|
||||||
|
var currentChunk = 0;
|
||||||
|
|
||||||
|
if (_handles == null || _handles.Length < nChunks)
|
||||||
|
{
|
||||||
|
_handles = new MemoryHandle[nChunks];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var readOnlyMemory in buffer)
|
||||||
|
{
|
||||||
|
ref var handle = ref _handles[currentChunk];
|
||||||
|
ref var chunk = ref pDataChunks[currentChunk];
|
||||||
|
handle = readOnlyMemory.Pin();
|
||||||
|
|
||||||
|
chunk.DataChunkType = HttpApiTypes.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
||||||
|
chunk.fromMemory.BufferLength = (uint)readOnlyMemory.Length;
|
||||||
|
chunk.fromMemory.pBuffer = (IntPtr)handle.Pointer;
|
||||||
|
|
||||||
|
currentChunk++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WriteChunks(_requestHandler, nChunks, pDataChunks, out fCompletionExpected);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract unsafe int WriteChunks(IntPtr requestHandler, int chunkCount, HttpApiTypes.HTTP_DATA_CHUNK* dataChunks, out bool completionExpected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal interface IAsyncIOEngine: IDisposable
|
||||||
|
{
|
||||||
|
ValueTask<int> ReadAsync(Memory<byte> memory);
|
||||||
|
ValueTask<int> WriteAsync(ReadOnlySequence<byte> data);
|
||||||
|
ValueTask FlushAsync();
|
||||||
|
void NotifyCompletion(int hr, int bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class WebSocketsAsyncIOEngine
|
||||||
|
{
|
||||||
|
internal class AsyncInitializeOperation : AsyncIOOperation
|
||||||
|
{
|
||||||
|
private readonly WebSocketsAsyncIOEngine _engine;
|
||||||
|
|
||||||
|
private IntPtr _requestHandler;
|
||||||
|
|
||||||
|
public AsyncInitializeOperation(WebSocketsAsyncIOEngine engine)
|
||||||
|
{
|
||||||
|
_engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(IntPtr requestHandler)
|
||||||
|
{
|
||||||
|
_requestHandler = requestHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool InvokeOperation(out int hr, out int bytes)
|
||||||
|
{
|
||||||
|
hr = NativeMethods.HttpFlushResponseBytes(_requestHandler, out var completionExpected);
|
||||||
|
bytes = 0;
|
||||||
|
return !completionExpected;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetOperation()
|
||||||
|
{
|
||||||
|
base.ResetOperation();
|
||||||
|
|
||||||
|
_requestHandler = default;
|
||||||
|
_engine.ReturnOperation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class WebSocketsAsyncIOEngine
|
||||||
|
{
|
||||||
|
internal class WebSocketReadOperation : AsyncIOOperation
|
||||||
|
{
|
||||||
|
public static readonly NativeMethods.PFN_WEBSOCKET_ASYNC_COMPLETION ReadCallback = (httpContext, completionInfo, completionContext) =>
|
||||||
|
{
|
||||||
|
var context = (WebSocketReadOperation)GCHandle.FromIntPtr(completionContext).Target;
|
||||||
|
|
||||||
|
NativeMethods.HttpGetCompletionInfo(completionInfo, out var cbBytes, out var hr);
|
||||||
|
|
||||||
|
var continuation = context.Complete(hr, cbBytes);
|
||||||
|
|
||||||
|
continuation.Invoke();
|
||||||
|
|
||||||
|
return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly WebSocketsAsyncIOEngine _engine;
|
||||||
|
private readonly GCHandle _thisHandle;
|
||||||
|
private MemoryHandle _inputHandle;
|
||||||
|
private IntPtr _requestHandler;
|
||||||
|
private Memory<byte> _memory;
|
||||||
|
|
||||||
|
public WebSocketReadOperation(WebSocketsAsyncIOEngine engine)
|
||||||
|
{
|
||||||
|
_engine = engine;
|
||||||
|
_thisHandle = GCHandle.Alloc(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe bool InvokeOperation(out int hr, out int bytes)
|
||||||
|
{
|
||||||
|
_inputHandle = _memory.Pin();
|
||||||
|
|
||||||
|
hr = NativeMethods.HttpWebsocketsReadBytes(
|
||||||
|
_requestHandler,
|
||||||
|
(byte*)_inputHandle.Pointer,
|
||||||
|
_memory.Length,
|
||||||
|
ReadCallback,
|
||||||
|
(IntPtr)_thisHandle,
|
||||||
|
out bytes,
|
||||||
|
out var completionExpected);
|
||||||
|
|
||||||
|
return !completionExpected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(IntPtr requestHandler, Memory<byte> memory)
|
||||||
|
{
|
||||||
|
_requestHandler = requestHandler;
|
||||||
|
_memory = memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void FreeOperationResources(int hr, int bytes)
|
||||||
|
{
|
||||||
|
_inputHandle.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetOperation()
|
||||||
|
{
|
||||||
|
base.ResetOperation();
|
||||||
|
|
||||||
|
_memory = default;
|
||||||
|
_inputHandle.Dispose();
|
||||||
|
_inputHandle = default;
|
||||||
|
_requestHandler = default;
|
||||||
|
|
||||||
|
_engine.ReturnOperation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// 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.Runtime.InteropServices;
|
||||||
|
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class WebSocketsAsyncIOEngine
|
||||||
|
{
|
||||||
|
internal sealed class WebSocketWriteOperation : AsyncWriteOperationBase
|
||||||
|
{
|
||||||
|
|
||||||
|
private static readonly NativeMethods.PFN_WEBSOCKET_ASYNC_COMPLETION WriteCallback = (httpContext, completionInfo, completionContext) =>
|
||||||
|
{
|
||||||
|
var context = (WebSocketWriteOperation)GCHandle.FromIntPtr(completionContext).Target;
|
||||||
|
|
||||||
|
NativeMethods.HttpGetCompletionInfo(completionInfo, out var cbBytes, out var hr);
|
||||||
|
|
||||||
|
var continuation = context.Complete(hr, cbBytes);
|
||||||
|
continuation.Invoke();
|
||||||
|
|
||||||
|
return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING;
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly WebSocketsAsyncIOEngine _engine;
|
||||||
|
private readonly GCHandle _thisHandle;
|
||||||
|
|
||||||
|
public WebSocketWriteOperation(WebSocketsAsyncIOEngine engine)
|
||||||
|
{
|
||||||
|
_engine = engine;
|
||||||
|
_thisHandle = GCHandle.Alloc(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe int WriteChunks(IntPtr requestHandler, int chunkCount, HttpApiTypes.HTTP_DATA_CHUNK* dataChunks, out bool completionExpected)
|
||||||
|
{
|
||||||
|
return NativeMethods.HttpWebsocketsWriteBytes(requestHandler, dataChunks, chunkCount, WriteCallback, (IntPtr)_thisHandle, out completionExpected);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ResetOperation()
|
||||||
|
{
|
||||||
|
base.ResetOperation();
|
||||||
|
|
||||||
|
_engine.ReturnOperation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
{
|
||||||
|
internal partial class WebSocketsAsyncIOEngine: IAsyncIOEngine
|
||||||
|
{
|
||||||
|
private readonly IntPtr _handler;
|
||||||
|
|
||||||
|
private bool _isInitialized;
|
||||||
|
|
||||||
|
private AsyncInitializeOperation _initializationFlush;
|
||||||
|
|
||||||
|
private WebSocketWriteOperation _cachedWebSocketWriteOperation;
|
||||||
|
|
||||||
|
private WebSocketReadOperation _cachedWebSocketReadOperation;
|
||||||
|
|
||||||
|
private AsyncInitializeOperation _cachedAsyncInitializeOperation;
|
||||||
|
|
||||||
|
public WebSocketsAsyncIOEngine(IntPtr handler)
|
||||||
|
{
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<int> ReadAsync(Memory<byte> memory)
|
||||||
|
{
|
||||||
|
ThrowIfNotInitialized();
|
||||||
|
|
||||||
|
var read = GetReadOperation();
|
||||||
|
read.Initialize(_handler, memory);
|
||||||
|
read.Invoke();
|
||||||
|
return new ValueTask<int>(read, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<int> WriteAsync(ReadOnlySequence<byte> data)
|
||||||
|
{
|
||||||
|
ThrowIfNotInitialized();
|
||||||
|
|
||||||
|
var write = GetWriteOperation();
|
||||||
|
write.Initialize(_handler, data);
|
||||||
|
write.Invoke();
|
||||||
|
return new ValueTask<int>(write, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask FlushAsync()
|
||||||
|
{
|
||||||
|
if (_isInitialized)
|
||||||
|
{
|
||||||
|
return new ValueTask(Task.CompletedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeMethods.HttpEnableWebsockets(_handler);
|
||||||
|
|
||||||
|
var init = GetInitializeOperation();
|
||||||
|
init.Initialize(_handler);
|
||||||
|
|
||||||
|
var continuation = init.Invoke();
|
||||||
|
|
||||||
|
if (continuation != null)
|
||||||
|
{
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_initializationFlush = init;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValueTask(init, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyCompletion(int hr, int bytes)
|
||||||
|
{
|
||||||
|
_isInitialized = true;
|
||||||
|
|
||||||
|
var init = _initializationFlush;
|
||||||
|
if (init == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unexpected completion for WebSocket operation");
|
||||||
|
}
|
||||||
|
|
||||||
|
var continuation = init.Complete(hr, bytes);
|
||||||
|
|
||||||
|
_initializationFlush = null;
|
||||||
|
|
||||||
|
continuation.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfNotInitialized()
|
||||||
|
{
|
||||||
|
if (!_isInitialized)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Socket IO not initialized yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
NativeMethods.HttpTryCancelIO(_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebSocketReadOperation GetReadOperation() =>
|
||||||
|
Interlocked.Exchange(ref _cachedWebSocketReadOperation, null) ??
|
||||||
|
new WebSocketReadOperation(this);
|
||||||
|
|
||||||
|
private WebSocketWriteOperation GetWriteOperation() =>
|
||||||
|
Interlocked.Exchange(ref _cachedWebSocketWriteOperation, null) ??
|
||||||
|
new WebSocketWriteOperation(this);
|
||||||
|
|
||||||
|
private AsyncInitializeOperation GetInitializeOperation() =>
|
||||||
|
Interlocked.Exchange(ref _cachedAsyncInitializeOperation, null) ??
|
||||||
|
new AsyncInitializeOperation(this);
|
||||||
|
|
||||||
|
private void ReturnOperation(AsyncInitializeOperation operation) =>
|
||||||
|
Volatile.Write(ref _cachedAsyncInitializeOperation, operation);
|
||||||
|
|
||||||
|
private void ReturnOperation(WebSocketWriteOperation operation) =>
|
||||||
|
Volatile.Write(ref _cachedWebSocketWriteOperation, operation);
|
||||||
|
|
||||||
|
private void ReturnOperation(WebSocketReadOperation operation) =>
|
||||||
|
Volatile.Write(ref _cachedWebSocketReadOperation, operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
internal class OutputProducer
|
internal class OutputProducer
|
||||||
{
|
{
|
||||||
private static readonly ArraySegment<byte> _emptyData = new ArraySegment<byte>(new byte[0]);
|
|
||||||
|
|
||||||
// This locks access to to all of the below fields
|
// This locks access to to all of the below fields
|
||||||
private readonly object _contextLock = new object();
|
private readonly object _contextLock = new object();
|
||||||
|
|
||||||
|
|
@ -36,9 +34,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
|
|
||||||
public PipeReader Reader => _pipe.Reader;
|
public PipeReader Reader => _pipe.Reader;
|
||||||
|
|
||||||
public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
public Task FlushAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return WriteAsync(_emptyData, cancellationToken);
|
_pipe.Reader.CancelPendingRead();
|
||||||
|
// Await backpressure
|
||||||
|
return FlushAsync(_pipe.Writer, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
@ -71,9 +71,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WriteAsync(
|
public Task WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
|
||||||
ReadOnlyMemory<byte> buffer,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
lock (_contextLock)
|
lock (_contextLock)
|
||||||
{
|
{
|
||||||
|
|
@ -88,8 +86,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
return FlushAsync(_pipe.Writer, cancellationToken);
|
return FlushAsync(_pipe.Writer, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task FlushAsync(PipeWriter pipeWriter,
|
private Task FlushAsync(PipeWriter pipeWriter, CancellationToken cancellationToken)
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var awaitable = pipeWriter.FlushAsync(cancellationToken);
|
var awaitable = pipeWriter.FlushAsync(cancellationToken);
|
||||||
if (awaitable.IsCompleted)
|
if (awaitable.IsCompleted)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
||||||
|
<PackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="$(SystemNetWebSocketsWebSocketProtocolPackageVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -70,55 +68,129 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[ConditionalFact]
|
||||||
public void ReadAndWriteSlowConnection()
|
public async Task ReadSetHeaderWrite()
|
||||||
{
|
{
|
||||||
var ipHostEntry = Dns.GetHostEntry(_fixture.Client.BaseAddress.Host);
|
var body = "Body text";
|
||||||
|
var content = new StringContent(body);
|
||||||
|
var response = await _fixture.Client.PostAsync("SetHeaderFromBody", content);
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
Assert.Equal(body, response.Headers.GetValues("BodyAsString").Single());
|
||||||
|
Assert.Equal(body, responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public async Task ReadAndWriteSlowConnection()
|
||||||
|
{
|
||||||
|
using (var connection = _fixture.CreateTestConnection())
|
||||||
{
|
{
|
||||||
foreach (var hostEntry in ipHostEntry.AddressList)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
socket.Connect(hostEntry, _fixture.Client.BaseAddress.Port);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// Exceptions can be thrown based on ipv6 support
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.True(socket.Connected);
|
|
||||||
|
|
||||||
var testString = "hello world";
|
var testString = "hello world";
|
||||||
var request = $"POST /ReadAndWriteSlowConnection HTTP/1.0\r\n" +
|
var request = $"POST /ReadAndWriteSlowConnection HTTP/1.0\r\n" +
|
||||||
$"Content-Length: {testString.Length}\r\n" +
|
$"Content-Length: {testString.Length}\r\n" +
|
||||||
"Host: " + "localhost\r\n" +
|
"Host: " + "localhost\r\n" +
|
||||||
"\r\n";
|
"\r\n" + testString;
|
||||||
var bytes = 0;
|
|
||||||
var requestStringBytes = Encoding.ASCII.GetBytes(request);
|
|
||||||
var testStringBytes = Encoding.ASCII.GetBytes(testString);
|
|
||||||
|
|
||||||
while ((bytes += socket.Send(requestStringBytes, bytes, 1, SocketFlags.None)) < requestStringBytes.Length)
|
foreach (var c in request)
|
||||||
{
|
{
|
||||||
|
await connection.Send(c.ToString());
|
||||||
|
await Task.Delay(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes = 0;
|
await connection.Receive(
|
||||||
while ((bytes += socket.Send(testStringBytes, bytes, 1, SocketFlags.None)) < testStringBytes.Length)
|
"HTTP/1.1 200 OK",
|
||||||
|
"");
|
||||||
|
await connection.ReceiveHeaders();
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
Thread.Sleep(100);
|
foreach (var c in testString)
|
||||||
|
{
|
||||||
|
await connection.Receive(c.ToString());
|
||||||
|
}
|
||||||
|
await Task.Delay(10);
|
||||||
|
}
|
||||||
|
await connection.WaitForConnectionClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public async Task ReadAndWriteInterleaved()
|
||||||
|
{
|
||||||
|
using (var connection = _fixture.CreateTestConnection())
|
||||||
|
{
|
||||||
|
var requestLength = 0;
|
||||||
|
var messages = new List<string>();
|
||||||
|
for (var i = 1; i < 100; i++)
|
||||||
|
{
|
||||||
|
var message = i + Environment.NewLine;
|
||||||
|
requestLength += message.Length;
|
||||||
|
messages.Add(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringBuilder = new StringBuilder();
|
await connection.Send(
|
||||||
var buffer = new byte[4096];
|
"POST /ReadAndWriteEchoLines HTTP/1.0",
|
||||||
int size;
|
$"Content-Length: {requestLength}",
|
||||||
while ((size = socket.Receive(buffer, buffer.Length, SocketFlags.None)) != 0)
|
"Host: localhost",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
await connection.Receive(
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
"");
|
||||||
|
await connection.ReceiveHeaders();
|
||||||
|
|
||||||
|
foreach (var message in messages)
|
||||||
{
|
{
|
||||||
stringBuilder.Append(Encoding.ASCII.GetString(buffer, 0, size));
|
await connection.Send(message);
|
||||||
|
await connection.Receive(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.Contains(new StringBuilder().Insert(0, "hello world", 100).ToString(), stringBuilder.ToString());
|
await connection.Send("\r\n");
|
||||||
|
await connection.WaitForConnectionClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public async Task ConsumePartialBody()
|
||||||
|
{
|
||||||
|
using (var connection = _fixture.CreateTestConnection())
|
||||||
|
{
|
||||||
|
var message = "Hello";
|
||||||
|
await connection.Send(
|
||||||
|
"POST /ReadPartialBody HTTP/1.1",
|
||||||
|
$"Content-Length: {100}",
|
||||||
|
"Host: localhost",
|
||||||
|
"Connection: close",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
await connection.Send(message);
|
||||||
|
|
||||||
|
await connection.Receive(
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
"");
|
||||||
|
|
||||||
|
// This test can return both content length or chunked response
|
||||||
|
// depending on if appfunc managed to complete before write was
|
||||||
|
// issued
|
||||||
|
var headers = await connection.ReceiveHeaders();
|
||||||
|
if (headers.Contains("Content-Length: 5"))
|
||||||
|
{
|
||||||
|
await connection.Receive("Hello");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await connection.Receive(
|
||||||
|
"5",
|
||||||
|
message,
|
||||||
|
"");
|
||||||
|
await connection.Receive(
|
||||||
|
"0",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.WaitForConnectionClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
|
{
|
||||||
|
[Collection(IISTestSiteCollection.Name)]
|
||||||
|
public class WebSocketsTests
|
||||||
|
{
|
||||||
|
private readonly string _webSocketUri;
|
||||||
|
|
||||||
|
public WebSocketsTests(IISTestSiteFixture fixture)
|
||||||
|
{
|
||||||
|
_webSocketUri = fixture.BaseUri.Replace("http:", "ws:");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public async Task OnStartedCalledForWebSocket()
|
||||||
|
{
|
||||||
|
var cws = new ClientWebSocket();
|
||||||
|
await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketLifetimeEvents"), default);
|
||||||
|
|
||||||
|
await ReceiveMessage(cws, "OnStarting");
|
||||||
|
await ReceiveMessage(cws, "Upgraded");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public async Task WebReadBeforeUpgrade()
|
||||||
|
{
|
||||||
|
var cws = new ClientWebSocket();
|
||||||
|
await cws.ConnectAsync(new Uri(_webSocketUri + "WebReadBeforeUpgrade"), default);
|
||||||
|
|
||||||
|
await ReceiveMessage(cws, "Yay");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalFact]
|
||||||
|
public async Task CanSendAndReceieveData()
|
||||||
|
{
|
||||||
|
var cws = new ClientWebSocket();
|
||||||
|
await cws.ConnectAsync(new Uri(_webSocketUri + "WebSocketEcho"), default);
|
||||||
|
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
{
|
||||||
|
var mesage = i.ToString();
|
||||||
|
await SendMessage(cws, mesage);
|
||||||
|
await ReceiveMessage(cws, mesage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendMessage(ClientWebSocket webSocket, string message)
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync(new ArraySegment<byte>(Encoding.ASCII.GetBytes(message)), WebSocketMessageType.Text, true, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveMessage(ClientWebSocket webSocket, string expectedMessage)
|
||||||
|
{
|
||||||
|
var received = new byte[expectedMessage.Length];
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
WebSocketReceiveResult result;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(received, offset, received.Length - offset), default);
|
||||||
|
offset += result.Count;
|
||||||
|
} while (!result.EndOfMessage);
|
||||||
|
|
||||||
|
Assert.Equal(expectedMessage, Encoding.ASCII.GetString(received));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
public CancellationToken ShutdownToken { get; }
|
public CancellationToken ShutdownToken { get; }
|
||||||
public DeploymentResult DeploymentResult { get; }
|
public DeploymentResult DeploymentResult { get; }
|
||||||
|
|
||||||
|
public TestConnection CreateTestConnection()
|
||||||
|
{
|
||||||
|
return new TestConnection(Client.BaseAddress.Port);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_deployer.Dispose();
|
_deployer.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Summary description for TestConnection
|
||||||
|
/// </summary>
|
||||||
|
public class TestConnection : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
private readonly bool _ownsSocket;
|
||||||
|
private readonly Socket _socket;
|
||||||
|
private readonly NetworkStream _stream;
|
||||||
|
private readonly StreamReader _reader;
|
||||||
|
|
||||||
|
public TestConnection(int port)
|
||||||
|
: this(port, AddressFamily.InterNetwork)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestConnection(int port, AddressFamily addressFamily)
|
||||||
|
: this(CreateConnectedLoopbackSocket(port, addressFamily), ownsSocket: true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestConnection(Socket socket)
|
||||||
|
: this(socket, ownsSocket: false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestConnection(Socket socket, bool ownsSocket)
|
||||||
|
{
|
||||||
|
_ownsSocket = ownsSocket;
|
||||||
|
_socket = socket;
|
||||||
|
_stream = new NetworkStream(_socket, ownsSocket: false);
|
||||||
|
_reader = new StreamReader(_stream, Encoding.ASCII);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Socket Socket => _socket;
|
||||||
|
|
||||||
|
public StreamReader Reader => _reader;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_stream.Dispose();
|
||||||
|
|
||||||
|
if (_ownsSocket)
|
||||||
|
{
|
||||||
|
_socket.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Send(params string[] lines)
|
||||||
|
{
|
||||||
|
var text = string.Join("\r\n", lines);
|
||||||
|
var writer = new StreamWriter(_stream, Encoding.GetEncoding("iso-8859-1"));
|
||||||
|
for (var index = 0; index < text.Length; index++)
|
||||||
|
{
|
||||||
|
var ch = text[index];
|
||||||
|
writer.Write(ch);
|
||||||
|
await writer.FlushAsync().ConfigureAwait(false);
|
||||||
|
// Re-add delay to help find socket input consumption bugs more consistently
|
||||||
|
//await Task.Delay(TimeSpan.FromMilliseconds(5));
|
||||||
|
}
|
||||||
|
await writer.FlushAsync().ConfigureAwait(false);
|
||||||
|
await _stream.FlushAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Receive(params string[] lines)
|
||||||
|
{
|
||||||
|
var expected = string.Join("\r\n", lines);
|
||||||
|
var actual = new char[expected.Length];
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (offset < expected.Length)
|
||||||
|
{
|
||||||
|
var data = new byte[expected.Length];
|
||||||
|
var task = _reader.ReadAsync(actual, offset, actual.Length - offset);
|
||||||
|
if (!Debugger.IsAttached)
|
||||||
|
{
|
||||||
|
task = task.TimeoutAfter(Timeout);
|
||||||
|
}
|
||||||
|
var count = await task.ConfigureAwait(false);
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TimeoutException ex) when (offset != 0)
|
||||||
|
{
|
||||||
|
throw new TimeoutException($"Did not receive a complete response within {Timeout}.{Environment.NewLine}{Environment.NewLine}" +
|
||||||
|
$"Expected:{Environment.NewLine}{expected}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
|
$"Actual:{Environment.NewLine}{new string(actual, 0, offset)}{Environment.NewLine}",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(expected, new string(actual, 0, offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReceiveStartsWith(string prefix, int maxLineLength = 1024)
|
||||||
|
{
|
||||||
|
var actual = new char[maxLineLength];
|
||||||
|
var offset = 0;
|
||||||
|
|
||||||
|
while (offset < maxLineLength)
|
||||||
|
{
|
||||||
|
// Read one char at a time so we don't read past the end of the line.
|
||||||
|
var task = _reader.ReadAsync(actual, offset, 1);
|
||||||
|
if (!Debugger.IsAttached)
|
||||||
|
{
|
||||||
|
Assert.True(task.Wait(4000), "timeout");
|
||||||
|
}
|
||||||
|
var count = await task.ConfigureAwait(false);
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(count == 1);
|
||||||
|
offset++;
|
||||||
|
|
||||||
|
if (actual[offset - 1] == '\n')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var actualLine = new string(actual, 0, offset);
|
||||||
|
Assert.StartsWith(prefix, actualLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string[]> ReceiveHeaders(params string[] lines)
|
||||||
|
{
|
||||||
|
List<string> headers = new List<string>();
|
||||||
|
string line;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
line = await _reader.ReadLineAsync();
|
||||||
|
headers.Add(line);
|
||||||
|
} while (line != "");
|
||||||
|
|
||||||
|
foreach (var s in lines)
|
||||||
|
{
|
||||||
|
Assert.Contains(s, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WaitForConnectionClose()
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<object>();
|
||||||
|
var eventArgs = new SocketAsyncEventArgs();
|
||||||
|
eventArgs.SetBuffer(new byte[128], 0, 128);
|
||||||
|
eventArgs.Completed += ReceiveAsyncCompleted;
|
||||||
|
eventArgs.UserToken = tcs;
|
||||||
|
|
||||||
|
if (!_socket.ReceiveAsync(eventArgs))
|
||||||
|
{
|
||||||
|
ReceiveAsyncCompleted(this, eventArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReceiveAsyncCompleted(object sender, SocketAsyncEventArgs e)
|
||||||
|
{
|
||||||
|
var tcs = (TaskCompletionSource<object>)e.UserToken;
|
||||||
|
if (e.BytesTransferred == 0)
|
||||||
|
{
|
||||||
|
tcs.SetResult(null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tcs.SetException(new IOException(
|
||||||
|
$"Expected connection close, received data instead: \"{_reader.CurrentEncoding.GetString(e.Buffer, 0, e.BytesTransferred)}\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Socket CreateConnectedLoopbackSocket(int port, AddressFamily addressFamily)
|
||||||
|
{
|
||||||
|
if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"TestConnection does not support address family of type {addressFamily}", nameof(addressFamily));
|
||||||
|
}
|
||||||
|
|
||||||
|
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
var address = addressFamily == AddressFamily.InterNetworkV6
|
||||||
|
? IPAddress.IPv6Loopback
|
||||||
|
: IPAddress.Loopback;
|
||||||
|
socket.Connect(new IPEndPoint(address, port));
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,17 @@
|
||||||
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Server.IISIntegration\Microsoft.AspNetCore.Server.IISIntegration.csproj" />
|
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Server.IISIntegration\Microsoft.AspNetCore.Server.IISIntegration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\shared\**\*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="$(MicrosoftAspNetCoreWebUtilitiesPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="$(MicrosoftAspNetCoreWebUtilitiesPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="$(MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsConfigurationJsonPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||||
|
<PackageReference Include="System.Net.WebSockets.WebSocketProtocol" Version="$(SystemNetWebSocketsWebSocketProtocolPackageVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using IISIntegration.FunctionalTests;
|
||||||
|
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;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace IISTestSite
|
||||||
|
{
|
||||||
|
public partial class Startup
|
||||||
|
{
|
||||||
|
|
||||||
|
private void WebsocketRequest(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("test");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WebReadBeforeUpgrade(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Run(async context => {
|
||||||
|
|
||||||
|
var singleByteArray = new byte[1];
|
||||||
|
Assert.Equal(0, await context.Request.Body.ReadAsync(singleByteArray, 0, 1));
|
||||||
|
|
||||||
|
var ws = await Upgrade(context);
|
||||||
|
await SendMessages(ws, "Yay");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WebSocketEcho(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Run(async context =>
|
||||||
|
{
|
||||||
|
var ws = await Upgrade(context);
|
||||||
|
|
||||||
|
var appLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
|
||||||
|
|
||||||
|
await Echo(ws, appLifetime.ApplicationStopping);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WebSocketLifetimeEvents(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Run(async context => {
|
||||||
|
|
||||||
|
var messages = new List<string>();
|
||||||
|
|
||||||
|
context.Response.OnStarting(() => {
|
||||||
|
context.Response.Headers["custom-header"] = "value";
|
||||||
|
messages.Add("OnStarting");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
var ws = await Upgrade(context);
|
||||||
|
messages.Add("Upgraded");
|
||||||
|
|
||||||
|
await SendMessages(ws, messages.ToArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SendMessages(WebSocket webSocket, params string[] messages)
|
||||||
|
{
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync(new ArraySegment<byte>(Encoding.ASCII.GetBytes(message)), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<WebSocket> Upgrade(HttpContext context)
|
||||||
|
{
|
||||||
|
var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();
|
||||||
|
|
||||||
|
// Generate WebSocket response headers
|
||||||
|
string key = context.Request.Headers[Constants.Headers.SecWebSocketKey].ToString();
|
||||||
|
var responseHeaders = HandshakeHelpers.GenerateResponseHeaders(key);
|
||||||
|
foreach (var headerPair in responseHeaders)
|
||||||
|
{
|
||||||
|
context.Response.Headers[headerPair.Key] = headerPair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade the connection
|
||||||
|
Stream opaqueTransport = await upgradeFeature.UpgradeAsync();
|
||||||
|
|
||||||
|
// Get the WebSocket object
|
||||||
|
var ws = WebSocketProtocol.CreateFromStream(opaqueTransport, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromMinutes(2));
|
||||||
|
return ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Echo(WebSocket webSocket, CancellationToken token)
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);
|
||||||
|
bool closeFromServer = false;
|
||||||
|
string closeFromServerCmd = "CloseFromServer";
|
||||||
|
int closeFromServerLength = closeFromServerCmd.Length;
|
||||||
|
|
||||||
|
while (!result.CloseStatus.HasValue && !token.IsCancellationRequested && !closeFromServer)
|
||||||
|
{
|
||||||
|
if (result.Count == closeFromServerLength &&
|
||||||
|
Encoding.ASCII.GetString(buffer).Substring(0, result.Count) == closeFromServerCmd)
|
||||||
|
{
|
||||||
|
// The client sent "CloseFromServer" text message to request the server to close (a test scenario).
|
||||||
|
closeFromServer = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, token);
|
||||||
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.CloseStatus.HasValue)
|
||||||
|
{
|
||||||
|
// Client-initiated close handshake
|
||||||
|
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Server-initiated close handshake due to either of the two conditions:
|
||||||
|
// (1) The applicaton host is performing a graceful shutdown.
|
||||||
|
// (2) The client sent "CloseFromServer" text message to request the server to close (a test scenario).
|
||||||
|
await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, closeFromServerCmd, CancellationToken.None);
|
||||||
|
|
||||||
|
// The server has sent the Close frame.
|
||||||
|
// Stop sending but keep receiving until we get the Close frame from the client.
|
||||||
|
while (!result.CloseStatus.HasValue)
|
||||||
|
{
|
||||||
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,25 +2,30 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IISIntegration.FunctionalTests;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.IIS;
|
using Microsoft.AspNetCore.Server.IIS;
|
||||||
using Microsoft.AspNetCore.Server.IISIntegration;
|
using Microsoft.AspNetCore.Server.IISIntegration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace IISTestSite
|
namespace IISTestSite
|
||||||
{
|
{
|
||||||
public class Startup
|
public partial class Startup
|
||||||
{
|
{
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
|
|
@ -369,8 +374,7 @@ namespace IISTestSite
|
||||||
|
|
||||||
private void ReadAndWriteEcho(IApplicationBuilder app)
|
private void ReadAndWriteEcho(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
app.Run(async context =>
|
app.Run(async context => {
|
||||||
{
|
|
||||||
var readBuffer = new byte[4096];
|
var readBuffer = new byte[4096];
|
||||||
var result = await context.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length);
|
var result = await context.Request.Body.ReadAsync(readBuffer, 0, readBuffer.Length);
|
||||||
while (result != 0)
|
while (result != 0)
|
||||||
|
|
@ -381,6 +385,51 @@ namespace IISTestSite
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReadAndWriteEchoLines(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Run(async context => {
|
||||||
|
//Send headers
|
||||||
|
await context.Response.Body.FlushAsync();
|
||||||
|
|
||||||
|
var reader = new StreamReader(context.Request.Body);
|
||||||
|
while (!reader.EndOfStream)
|
||||||
|
{
|
||||||
|
var line = await reader.ReadLineAsync();
|
||||||
|
if (line == "")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await context.Response.WriteAsync(line + Environment.NewLine);
|
||||||
|
await context.Response.Body.FlushAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadPartialBody(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Run(async context => {
|
||||||
|
var data = new byte[5];
|
||||||
|
var count = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
count += await context.Request.Body.ReadAsync(data, count, data.Length - count);
|
||||||
|
} while (count != data.Length);
|
||||||
|
await context.Response.Body.WriteAsync(data, 0, data.Length);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetHeaderFromBody(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Run(async context => {
|
||||||
|
using (var reader = new StreamReader(context.Request.Body))
|
||||||
|
{
|
||||||
|
var value = await reader.ReadToEndAsync();
|
||||||
|
context.Response.Headers["BodyAsString"] = value;
|
||||||
|
await context.Response.WriteAsync(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void ReadAndWriteEchoTwice(IApplicationBuilder app)
|
private void ReadAndWriteEchoTwice(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
app.Run(async context =>
|
app.Run(async context =>
|
||||||
|
|
@ -416,14 +465,6 @@ namespace IISTestSite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WebsocketRequest(IApplicationBuilder app)
|
|
||||||
{
|
|
||||||
app.Run(async context =>
|
|
||||||
{
|
|
||||||
await context.Response.WriteAsync("test");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadAndWriteCopyToAsync(IApplicationBuilder app)
|
private void ReadAndWriteCopyToAsync(IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
app.Run(async context =>
|
app.Run(async context =>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"nativeDebugging": true,
|
"nativeDebugging": true,
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"IIS_SITE_PATH": "$(MSBuildThisFileDirectory)",
|
"IIS_SITE_PATH": "$(MSBuildThisFileDirectory)",
|
||||||
"ANCM_PATH": "$(TargetDir)$(AncmPath)",
|
"ANCM_PATH": "$(TargetDir)$(AncmV2Path)",
|
||||||
"LAUNCHER_ARGS": "$(TargetPath)",
|
"LAUNCHER_ARGS": "$(TargetPath)",
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"LAUNCHER_PATH": "$(DotNetPath)"
|
"LAUNCHER_PATH": "$(DotNetPath)"
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
"commandLineArgs": "$(IISArguments)",
|
"commandLineArgs": "$(IISArguments)",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"IIS_SITE_PATH": "$(MSBuildThisFileDirectory)",
|
"IIS_SITE_PATH": "$(MSBuildThisFileDirectory)",
|
||||||
"ANCM_PATH": "$(TargetDir)$(AncmPath)",
|
"ANCM_PATH": "$(TargetDir)$(AncmV2Path)",
|
||||||
"LAUNCHER_ARGS": "$(TargetPath)",
|
"LAUNCHER_ARGS": "$(TargetPath)",
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"LAUNCHER_PATH": "$(DotNetPath)"
|
"LAUNCHER_PATH": "$(DotNetPath)"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using IISIntegration.FunctionalTests;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<Import Project="..\..\..\build\testsite.props" />
|
<Import Project="..\..\..\build\testsite.props" />
|
||||||
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Server.IISIntegration\Microsoft.AspNetCore.Server.IISIntegration.csproj" />
|
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Server.IISIntegration\Microsoft.AspNetCore.Server.IISIntegration.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\shared\**\*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace ANCMStressTestApp
|
namespace IISIntegration.FunctionalTests
|
||||||
{
|
{
|
||||||
public static class Constants
|
public static class Constants
|
||||||
{
|
{
|
||||||
|
|
@ -6,9 +6,8 @@ using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace ANCMStressTestApp
|
namespace IISIntegration.FunctionalTests
|
||||||
{
|
{
|
||||||
// Removed all the
|
|
||||||
internal static class HandshakeHelpers
|
internal static class HandshakeHelpers
|
||||||
{
|
{
|
||||||
public static IEnumerable<KeyValuePair<string, string>> GenerateResponseHeaders(string key)
|
public static IEnumerable<KeyValuePair<string, string>> GenerateResponseHeaders(string key)
|
||||||
Loading…
Reference in New Issue