606 lines
26 KiB
C#
606 lines
26 KiB
C#
#if !UNITY_WEBGL || UNITY_EDITOR
|
|
using Best.HTTP.Request.Timings;
|
|
using Best.HTTP.Request.Upload;
|
|
using Best.HTTP.Response;
|
|
using Best.HTTP.Shared;
|
|
using Best.HTTP.Shared.Extensions;
|
|
using Best.HTTP.Shared.Logger;
|
|
using Best.HTTP.Shared.PlatformSupport.Memory;
|
|
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
|
using Best.HTTP.Shared.PlatformSupport.Threading;
|
|
using Best.HTTP.Shared.Streams;
|
|
|
|
using System;
|
|
using System.Threading;
|
|
|
|
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
|
|
|
namespace Best.HTTP.Hosts.Connections.HTTP1
|
|
{
|
|
public sealed class HTTP1ContentConsumer : IHTTPRequestHandler, IContentConsumer, IDownloadContentBufferAvailable, IThreadSignaler
|
|
{
|
|
public ShutdownTypes ShutdownType { get; private set; }
|
|
|
|
public KeepAliveHeader KeepAlive { get { return this._keepAlive; } }
|
|
private KeepAliveHeader _keepAlive;
|
|
|
|
public bool CanProcessMultiple { get { return false; } }
|
|
|
|
/// <summary>
|
|
/// Number of assigned requests to process.
|
|
/// </summary>
|
|
public int AssignedRequests => this.conn.CurrentRequest == null ? 0 : 1;
|
|
|
|
/// <summary>
|
|
/// Maximum number of assignable requests
|
|
/// </summary>
|
|
public int MaxAssignedRequests => 1;
|
|
|
|
public LoggingContext Context { get; private set; }
|
|
|
|
public PeekableContentProviderStream ContentProvider { get; private set; }
|
|
|
|
private readonly HTTPOverTCPConnection conn;
|
|
private PeekableHTTP1Response _response;
|
|
private int _isAlreadyProcessingContent;
|
|
private WriteOnlyBufferedStream _upStream;
|
|
|
|
private int _isSendingContent;
|
|
private int _hasMoreData;
|
|
private bool _isContentSendingFinished;
|
|
private int _acceptContentSignals;
|
|
|
|
// Initialize the progress report variables
|
|
private long _uploaded = 0;
|
|
|
|
public HTTP1ContentConsumer(HTTPOverTCPConnection conn)
|
|
{
|
|
this.Context = new LoggingContext(this);
|
|
this.conn = conn;
|
|
}
|
|
|
|
public void RunHandler()
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "Started processing request", this.Context);
|
|
|
|
//ThreadedRunner.SetThreadName("Best.HTTP1 Write");
|
|
|
|
try
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
|
|
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(now))
|
|
throw new TimeoutException();
|
|
|
|
if (this.conn.CurrentRequest.IsCancellationRequested)
|
|
throw new Exception("Cancellation requested!");
|
|
|
|
// create the response before we would send out the request, because sending out might cause an exception
|
|
// and the response is used for decision making in the FinishedProcessing call.
|
|
if (this._response == null)
|
|
this.conn.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.conn.CurrentRequest, false, this);
|
|
|
|
// Write the request to the stream
|
|
this.conn.CurrentRequest.TimeoutSettings.SetProcessing(now);
|
|
|
|
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Request_Sent);
|
|
|
|
SendOutTo(this.conn.CurrentRequest, this.conn.TopStream);
|
|
|
|
this.conn.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (this.ShutdownType == ShutdownTypes.Immediate)
|
|
return;
|
|
|
|
FinishedProcessing(e);
|
|
}
|
|
}
|
|
|
|
private void SendOutTo(HTTPRequest request, System.IO.Stream stream)
|
|
{
|
|
request.Prepare();
|
|
|
|
string requestPathAndQuery =
|
|
request.ProxySettings.HasProxyFor(request.CurrentUri) ?
|
|
request.ProxySettings.Proxy.GetRequestPath(request.CurrentUri) :
|
|
request.CurrentUri.GetRequestPathAndQueryURL();
|
|
|
|
string requestLine = string.Format("{0} {1} HTTP/1.1", HTTPRequest.MethodNames[(byte)request.MethodType], requestPathAndQuery);
|
|
|
|
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
|
HTTPManager.Logger.Information("HTTPRequest", string.Format("Sending request: '{0}'", requestLine), request.Context);
|
|
|
|
// Create a buffer stream that will not close 'stream' when disposed or closed.
|
|
// buffersize should be larger than UploadChunkSize as it might be used for uploading user data and
|
|
// it should have enough room for UploadChunkSize data and additional chunk information.
|
|
using (WriteOnlyBufferedStream bufferStream = new WriteOnlyBufferedStream(stream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context))
|
|
{
|
|
var requestLineBytes = requestLine.GetASCIIBytes();
|
|
bufferStream.WriteBufferSegment(requestLineBytes);
|
|
bufferStream.WriteArray(EOL);
|
|
|
|
BufferPool.Release(requestLineBytes);
|
|
|
|
// Write headers to the buffer
|
|
request.EnumerateHeaders((header, values) =>
|
|
{
|
|
if (string.IsNullOrEmpty(header) || values == null)
|
|
return;
|
|
|
|
//var headerName = string.Concat(header, ": ").GetASCIIBytes();
|
|
var headerName = header.GetASCIIBytes();
|
|
|
|
for (int i = 0; i < values.Count; ++i)
|
|
{
|
|
if (string.IsNullOrEmpty(values[i]))
|
|
{
|
|
HTTPManager.Logger.Warning("HTTPRequest", string.Format("Null/empty value for header: {0}", header), request.Context);
|
|
continue;
|
|
}
|
|
|
|
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
|
HTTPManager.Logger.Verbose("HTTPRequest", $"Header - '{header}': '{values[i]}'", request.Context);
|
|
|
|
var valueBytes = values[i].GetASCIIBytes();
|
|
|
|
bufferStream.WriteBufferSegment(headerName);
|
|
bufferStream.WriteArray(HeaderValueSeparator);
|
|
bufferStream.WriteBufferSegment(valueBytes);
|
|
bufferStream.WriteArray(EOL);
|
|
|
|
BufferPool.Release(valueBytes);
|
|
}
|
|
|
|
BufferPool.Release(headerName);
|
|
}, /*callBeforeSendCallback:*/ true);
|
|
|
|
bufferStream.WriteArray(EOL);
|
|
|
|
// Send body content
|
|
if (this.conn.CurrentRequest.UploadSettings.UploadStream is Request.Upload.UploadStreamBase upStream)
|
|
upStream.BeforeSendBody(this.conn.CurrentRequest, this);
|
|
|
|
Interlocked.Exchange(ref this._isSendingContent, 1);
|
|
Interlocked.Exchange(ref this._acceptContentSignals, 1);
|
|
|
|
SendContent(bufferStream);
|
|
} // bufferStream.Dispose
|
|
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Information("HTTPRequest", "Sent out '" + requestLine + "'", this.Context);
|
|
}
|
|
|
|
void SendContent(WriteOnlyBufferedStream streamToSend)
|
|
{
|
|
Interlocked.Exchange(ref this._hasMoreData, 0);
|
|
|
|
var request = this.conn.CurrentRequest;
|
|
System.IO.Stream uploadStream = request.UploadSettings.UploadStream;
|
|
|
|
try
|
|
{
|
|
if (uploadStream == null)
|
|
{
|
|
this._isContentSendingFinished = true;
|
|
}
|
|
else
|
|
{
|
|
if (streamToSend == null)
|
|
{
|
|
if (this._upStream == null)
|
|
this._upStream = new WriteOnlyBufferedStream(this.conn.TopStream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context);
|
|
streamToSend = this._upStream;
|
|
}
|
|
long uploadLength = uploadStream.Length;
|
|
bool isChunked = uploadLength == BodyLengths.UnknownWithChunkedTransferEncoding;
|
|
|
|
// Upload buffer. First we will read the data into this buffer from the UploadStream, then write this buffer to our outStream
|
|
byte[] buffer = BufferPool.Get(this.conn.CurrentRequest.UploadSettings.UploadChunkSize, true);
|
|
using var _ = new AutoReleaseBuffer(buffer);
|
|
|
|
// How many bytes was read from the UploadStream
|
|
int count = uploadStream.Read(buffer, 0, buffer.Length);
|
|
while (count > 0)
|
|
{
|
|
if (isChunked)
|
|
{
|
|
var countBytes = count.ToString("X").GetASCIIBytes();
|
|
streamToSend.WriteBufferSegment(countBytes);
|
|
streamToSend.WriteArray(EOL);
|
|
|
|
BufferPool.Release(countBytes);
|
|
}
|
|
|
|
// write out the buffer to the wire
|
|
streamToSend.Write(buffer, 0, count);
|
|
|
|
// chunk trailing EOL
|
|
if (uploadLength < 0)
|
|
streamToSend.WriteArray(EOL);
|
|
|
|
// update how many bytes are uploaded
|
|
_uploaded += count;
|
|
|
|
if (this.conn.CurrentRequest.UploadSettings.OnUploadProgress != null)
|
|
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.conn.CurrentRequest, RequestEvents.UploadProgress, _uploaded, uploadLength));
|
|
|
|
if (this.conn.CurrentRequest.IsCancellationRequested)
|
|
return;
|
|
|
|
count = uploadStream.Read(buffer, 0, buffer.Length);
|
|
}
|
|
|
|
this._isContentSendingFinished = count == UploadReadConstants.Completed;
|
|
|
|
// All data from the stream are sent, write the 'end' chunk if necessary
|
|
if (isChunked && this._isContentSendingFinished)
|
|
{
|
|
ReadOnlySpan<byte> bytes = stackalloc byte[] { (byte)'0' };
|
|
|
|
streamToSend.Write(bytes);
|
|
streamToSend.WriteArray(EOL);
|
|
streamToSend.WriteArray(EOL);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Make sure all remaining data will be on the wire
|
|
streamToSend?.Flush();
|
|
|
|
if (this._isContentSendingFinished)
|
|
{
|
|
this._upStream?.Dispose();
|
|
this._upStream = null;
|
|
if (this.conn.CurrentRequest.UploadSettings.DisposeStream)
|
|
uploadStream?.Dispose();
|
|
|
|
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
|
|
|
|
Interlocked.Exchange(ref this._isSendingContent, 0);
|
|
}
|
|
else
|
|
{
|
|
Interlocked.Exchange(ref this._isSendingContent, 0);
|
|
|
|
if (this._hasMoreData > 0)
|
|
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
|
|
}
|
|
}
|
|
}
|
|
|
|
void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
|
|
{
|
|
//HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);
|
|
|
|
// TODO: Do NOT call OnContent on the Unity main thread
|
|
if (this._response != null)
|
|
OnContent();
|
|
}
|
|
|
|
public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
|
|
|
|
public void UnsetBinding() => this.ContentProvider = null;
|
|
|
|
public void OnContent()
|
|
{
|
|
if (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) != 0)
|
|
return;
|
|
|
|
try
|
|
{
|
|
//HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnContent({peekable?.Length}, {this._response?.ReadState})", this.Context, this.conn.CurrentRequest.Context);
|
|
try
|
|
{
|
|
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
|
|
throw new TimeoutException();
|
|
|
|
if (this.conn.CurrentRequest.IsCancellationRequested)
|
|
throw new Exception("Cancellation requested!");
|
|
|
|
this._response.ProcessPeekable(this.ContentProvider);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (this.ShutdownType == ShutdownTypes.Immediate)
|
|
return;
|
|
|
|
FinishedProcessing(e);
|
|
}
|
|
|
|
// After an exception, this._response will be null!
|
|
if (this._response != null)
|
|
{
|
|
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
|
FinishedProcessing(null);
|
|
else if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.WaitForContentSent)
|
|
{
|
|
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
|
|
}
|
|
}
|
|
|
|
public void OnConnectionClosed()
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);
|
|
|
|
if (this._response != null &&
|
|
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
|
|
this._response.DeliveryMode == PeekableHTTP1Response.ContentDeliveryMode.RawUnknownLength &&
|
|
"close".Equals(this._response.GetFirstHeaderValue("connection"), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
FinishedProcessing(null);
|
|
}
|
|
else if (this.ContentProvider != null && this.ContentProvider.Length > 0 &&
|
|
this._response != null &&
|
|
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
|
|
this._response.DownStream != null)
|
|
{
|
|
// Let the stream comsume any buffered data first, and handle closure when the buffer depletes.
|
|
// TODO: This require however that the PeekableResponse ping this HTTP1ContentConsumer when buffer space is available in the down-stream.
|
|
// Or force-add all remaining data to the stream and see whether we finished downloading or not.
|
|
// Problems:
|
|
// 1.) OnContent might be already called and a call to it would be dropped. We could spin up a new thread waiting for its finish, then call it again.
|
|
//throw new NotImplementedException();
|
|
ThreadedRunner.RunShortLiving(() =>
|
|
{
|
|
SpinWait spinWait = new SpinWait();
|
|
|
|
while (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) == 1)
|
|
spinWait.SpinOnce();
|
|
|
|
try
|
|
{
|
|
try
|
|
{
|
|
this._response.DownStream.EmergencyIncreaseMaxBuffered();
|
|
this._response.ProcessPeekable(this.ContentProvider);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (this.ShutdownType == ShutdownTypes.Immediate)
|
|
return;
|
|
|
|
FinishedProcessing(e);
|
|
}
|
|
finally
|
|
{
|
|
// After an exception, this._response will be null!
|
|
if (this._response != null)
|
|
{
|
|
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
|
FinishedProcessing(null);
|
|
else
|
|
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
|
|
}
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// If the consumer still have a request: error it and close the connection
|
|
if (this.conn.CurrentRequest != null && this._response != null)
|
|
{
|
|
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
|
}
|
|
else // If no current request: close the connection
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, HTTPConnectionStates.Closed));
|
|
}
|
|
|
|
public void OnError(Exception e)
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);
|
|
|
|
if (this.ShutdownType == ShutdownTypes.Immediate)
|
|
return;
|
|
|
|
FinishedProcessing(e);
|
|
}
|
|
|
|
private void OnCancellationRequested(HTTPRequest req)
|
|
{
|
|
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "OnCancellationRequested()", this.Context);
|
|
|
|
Interlocked.Exchange(ref this._response, null);
|
|
req.OnCancellationRequested -= OnCancellationRequested;
|
|
this.conn?.Streamer?.Dispose();
|
|
}
|
|
|
|
void FinishedProcessing(Exception ex)
|
|
{
|
|
// Warning: FinishedProcessing might be called from different threads in parallel:
|
|
// - send thread triggered by a write failure
|
|
// - read thread oncontent/OnError/OnConnectionClosed
|
|
|
|
var resp = Interlocked.Exchange(ref this._response, null);
|
|
if (resp == null)
|
|
return;
|
|
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)}({resp.ReadState}, {ex})", this.Context);
|
|
|
|
// Unset the consumer, we no longer expect another OnContent call until further notice.
|
|
//if (conn.TopStream is IPeekableContentProvider provider && provider?.Consumer == this)
|
|
// provider.Consumer = null;
|
|
this.ContentProvider.UnbindIf(this);
|
|
|
|
var req = this.conn.CurrentRequest;
|
|
|
|
req.OnCancellationRequested -= OnCancellationRequested;
|
|
|
|
bool resendRequest = false;
|
|
HTTPRequestStates requestState = HTTPRequestStates.Finished;
|
|
HTTPConnectionStates connectionState = ex != null ? HTTPConnectionStates.Closed : HTTPConnectionStates.Recycle;
|
|
|
|
// We could finish the request, ignore the error.
|
|
if (resp.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
|
ex = null;
|
|
|
|
Exception error = ex;
|
|
|
|
if (error != null)
|
|
{
|
|
// Timeout is a non-retryable error
|
|
if (ex is TimeoutException)
|
|
{
|
|
error = null;
|
|
requestState = HTTPRequestStates.TimedOut;
|
|
}
|
|
else
|
|
{
|
|
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries && !HTTPManager.IsQuitting)
|
|
{
|
|
req.RetrySettings.Retries++;
|
|
error = null;
|
|
resendRequest = true;
|
|
}
|
|
else
|
|
{
|
|
requestState = HTTPRequestStates.Error;
|
|
}
|
|
}
|
|
|
|
// Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
|
|
connectionState = HTTPConnectionStates.Closed;
|
|
|
|
resp.Dispose();
|
|
}
|
|
else
|
|
{
|
|
// After HandleResponse connectionState can have the following values:
|
|
// - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
|
|
// - Closed: server sent an connection: close header.
|
|
// - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
|
|
// In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
|
|
|
|
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref this._keepAlive, this.Context);
|
|
|
|
if (error != null)
|
|
requestState = HTTPRequestStates.Error;
|
|
else if (!resendRequest && resp.IsUpgraded)
|
|
requestState = HTTPRequestStates.Processing;
|
|
}
|
|
|
|
req.Timing.StartNext(TimingEventNames.Queued);
|
|
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
|
|
|
|
// If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
|
|
if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
|
|
{
|
|
ConnectionHelper.ResendRequestAndCloseConnection(this.conn, req);
|
|
}
|
|
else if (resendRequest && requestState == HTTPRequestStates.Finished)
|
|
{
|
|
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
|
|
|
|
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, connectionState));
|
|
}
|
|
else
|
|
{
|
|
if (resp == null || !resp.IsUpgraded)
|
|
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
|
|
|
|
// Otherwise set the request's then the connection's state
|
|
ConnectionHelper.EnqueueEvents(this.conn, connectionState, req, requestState, error);
|
|
}
|
|
}
|
|
|
|
public void Process(HTTPRequest request)
|
|
{
|
|
(conn.TopStream as IPeekableContentProvider).SetTwoWayBinding(this);
|
|
|
|
// https://github.com/Benedicht/BestHTTP-Issues/issues/179
|
|
// Toughts:
|
|
// - Many requests, especially if they are uploading slowly, can occupy all background threads.
|
|
// Use short-living thread when:
|
|
// - It's a GET request
|
|
// - It's not an upgrade request
|
|
|
|
bool isRequestWithoutBody = request.MethodType == HTTPMethods.Get ||
|
|
request.MethodType == HTTPMethods.Head ||
|
|
request.MethodType == HTTPMethods.Delete ||
|
|
request.MethodType == HTTPMethods.Options;
|
|
bool isUpgrade = request.HasHeader("upgrade");
|
|
|
|
var useShortLivingThread = HTTPManager.PerHostSettings.Get(request.CurrentHostKey).HTTP1ConnectionSettings.ForceUseThreadPool ||
|
|
(isRequestWithoutBody && !isUpgrade);
|
|
|
|
if (useShortLivingThread)
|
|
ThreadedRunner.RunShortLiving(RunHandler);
|
|
else
|
|
ThreadedRunner.RunLongLiving(RunHandler);
|
|
}
|
|
|
|
public void Shutdown(ShutdownTypes type)
|
|
{
|
|
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), string.Format($"Shutdown({type})"), this.Context);
|
|
this.ShutdownType = type;
|
|
}
|
|
|
|
void IThreadSignaler.SignalThread(SignalHandlerTypes signalType)
|
|
{
|
|
Interlocked.Exchange(ref this._hasMoreData, 1);
|
|
|
|
if (this._acceptContentSignals == 1 && Interlocked.CompareExchange(ref this._isSendingContent, 1, 0) == 0)
|
|
{
|
|
switch (signalType)
|
|
{
|
|
case SignalHandlerTypes.Signal:
|
|
ThreadedRunner.RunShortLiving(SignalThreadHandler);
|
|
break;
|
|
|
|
case SignalHandlerTypes.InlineIfPossible:
|
|
SignalThreadHandler();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SignalThreadHandler()
|
|
{
|
|
try
|
|
{
|
|
SendContent(null);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (this.ShutdownType == ShutdownTypes.Immediate)
|
|
return;
|
|
|
|
FinishedProcessing(e);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private void Dispose(bool disposing)
|
|
{
|
|
this._upStream?.Dispose();
|
|
this._upStream = null;
|
|
}
|
|
}
|
|
}
|
|
#endif
|