333 lines
13 KiB
C#
333 lines
13 KiB
C#
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Threading;
|
|
|
|
using Best.HTTP.Caching;
|
|
using Best.HTTP.Hosts.Connections.File;
|
|
using Best.HTTP.Hosts.Connections.HTTP1;
|
|
using Best.HTTP.HostSetting;
|
|
using Best.HTTP.Request.Authentication;
|
|
using Best.HTTP.Request.Timings;
|
|
using Best.HTTP.Shared;
|
|
using Best.HTTP.Shared.PlatformSupport.Memory;
|
|
using Best.HTTP.Shared.Streams;
|
|
|
|
namespace Best.HTTP.Hosts.Connections.WebGL
|
|
{
|
|
class PeekableIncomingSegmentContentProviderStream : PeekableContentProviderStream
|
|
{
|
|
private int peek_listIdx;
|
|
private int peek_pos;
|
|
|
|
public override void BeginPeek()
|
|
{
|
|
peek_listIdx = 0;
|
|
peek_pos = base.bufferList.Count > 0 ? base.bufferList[0].Offset : 0;
|
|
}
|
|
|
|
public override int PeekByte()
|
|
{
|
|
if (base.bufferList.Count == 0)
|
|
return -1;
|
|
|
|
var segment = base.bufferList[this.peek_listIdx];
|
|
if (peek_pos >= segment.Offset + segment.Count)
|
|
{
|
|
if (base.bufferList.Count <= this.peek_listIdx + 1)
|
|
return -1;
|
|
|
|
segment = base.bufferList[++this.peek_listIdx];
|
|
this.peek_pos = segment.Offset;
|
|
}
|
|
|
|
return segment.Data[this.peek_pos++];
|
|
}
|
|
}
|
|
|
|
|
|
internal sealed class WebGLXHRConnection : ConnectionBase
|
|
{
|
|
public PeekableContentProviderStream ContentProvider { get; private set; }
|
|
|
|
int NativeId;
|
|
PeekableHTTP1Response _response;
|
|
|
|
public WebGLXHRConnection(HostKey hostKey)
|
|
: base(hostKey, false)
|
|
{
|
|
WebGLXHRNativeInterface.XHR_SetLoglevel((byte)HTTPManager.Logger.Level);
|
|
}
|
|
|
|
public override void Shutdown(ShutdownTypes type)
|
|
{
|
|
base.Shutdown(type);
|
|
|
|
WebGLXHRNativeInterface.XHR_Abort(this.NativeId);
|
|
}
|
|
|
|
protected override void ThreadFunc()
|
|
{
|
|
// XmlHttpRequest setup
|
|
|
|
CurrentRequest.Prepare();
|
|
|
|
Credentials credentials = null;// CurrentRequest.Authenticator?.Credentials;
|
|
|
|
this.NativeId = WebGLXHRNativeInterface.XHR_Create(HTTPRequest.MethodNames[(byte)CurrentRequest.MethodType],
|
|
CurrentRequest.CurrentUri.OriginalString,
|
|
credentials?.UserName, credentials?.Password, CurrentRequest.WithCredentials ? 1 : 0);
|
|
WebGLXHRNativeConnectionLayer.Add(NativeId, this);
|
|
|
|
CurrentRequest.EnumerateHeaders((header, values) =>
|
|
{
|
|
if (!header.Equals("Content-Length"))
|
|
for (int i = 0; i < values.Count; ++i)
|
|
WebGLXHRNativeInterface.XHR_SetRequestHeader(NativeId, header, values[i]);
|
|
}, /*callBeforeSendCallback:*/ true);
|
|
|
|
WebGLXHRNativeConnectionLayer.SetupHandlers(NativeId, CurrentRequest);
|
|
|
|
WebGLXHRNativeInterface.XHR_SetTimeout(NativeId, (uint)(CurrentRequest.TimeoutSettings.ConnectTimeout.TotalMilliseconds + CurrentRequest.TimeoutSettings.Timeout.TotalMilliseconds));
|
|
|
|
Stream upStream = CurrentRequest.UploadSettings.UploadStream;
|
|
byte[] body = null;
|
|
int length = 0;
|
|
bool releaseBodyBuffer = false;
|
|
|
|
if (upStream != null)
|
|
{
|
|
var internalBuffer = BufferPool.Get(upStream.Length > 0 ? upStream.Length : CurrentRequest.UploadSettings.UploadChunkSize, true);
|
|
using (BufferPoolMemoryStream ms = new BufferPoolMemoryStream(internalBuffer, 0, internalBuffer.Length, true, true, false, true))
|
|
{
|
|
var buffer = BufferPool.Get(CurrentRequest.UploadSettings.UploadChunkSize, true);
|
|
int readCount = -1;
|
|
while ((readCount = upStream.Read(buffer, 0, buffer.Length)) > 0)
|
|
ms.Write(buffer, 0, readCount);
|
|
|
|
BufferPool.Release(buffer);
|
|
|
|
length = (int)ms.Position;
|
|
body = ms.GetBuffer();
|
|
|
|
releaseBodyBuffer = true;
|
|
}
|
|
}
|
|
|
|
if (this._response == null)
|
|
this.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.CurrentRequest, false, null);
|
|
|
|
this.ContentProvider = new PeekableIncomingSegmentContentProviderStream();
|
|
|
|
WebGLXHRNativeInterface.XHR_Send(NativeId, body, length);
|
|
|
|
if (releaseBodyBuffer)
|
|
BufferPool.Release(body);
|
|
|
|
this.CurrentRequest.TimeoutSettings.QueuedAt = DateTime.MinValue;
|
|
this.CurrentRequest.TimeoutSettings.ProcessingStarted = DateTime.UtcNow;
|
|
this.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
|
}
|
|
|
|
#region Callback Implementations
|
|
|
|
private void OnCancellationRequested(HTTPRequest req)
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnCancellationRequested()", this.Context);
|
|
|
|
Interlocked.Exchange(ref this._response, null);
|
|
req.OnCancellationRequested -= OnCancellationRequested;
|
|
|
|
WebGLXHRNativeInterface.XHR_Abort(this.NativeId);
|
|
}
|
|
|
|
internal void OnBuffer(BufferSegment buffer)
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnBuffer({buffer})", this.Context);
|
|
|
|
try
|
|
{
|
|
if (this.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
|
|
throw new TimeoutException();
|
|
|
|
if (this.CurrentRequest.IsCancellationRequested)
|
|
throw new Exception("Cancellation requested!");
|
|
|
|
this.ContentProvider?.Write(buffer);
|
|
this._response?.ProcessPeekable(this.ContentProvider);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
BufferPool.Release(buffer);
|
|
|
|
if (this.ShutdownType == ShutdownTypes.Immediate)
|
|
return;
|
|
|
|
FinishedProcessing(e);
|
|
}
|
|
|
|
// After an exception, this._response will be null!
|
|
if (this._response != null && this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
|
FinishedProcessing(null);
|
|
}
|
|
|
|
internal void OnError(string error)
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnError({error})", this.Context);
|
|
|
|
FinishedProcessing(new Exception(error));
|
|
}
|
|
|
|
internal void OnResponse(BufferSegment payload)
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnResponse({payload})", this.Context);
|
|
|
|
this._response?.DownStream?.EmergencyIncreaseMaxBuffered();
|
|
OnBuffer(payload);
|
|
}
|
|
|
|
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;
|
|
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{nameof(FinishedProcessing)}({resp}, {ex})", this.Context);
|
|
|
|
// Unset the consumer, we no longer expect another OnContent call until further notice.
|
|
this.ContentProvider?.Unbind();
|
|
this.ContentProvider?.Dispose();
|
|
this.ContentProvider = null;
|
|
|
|
var req = this.CurrentRequest;
|
|
|
|
req.OnCancellationRequested -= OnCancellationRequested;
|
|
|
|
bool resendRequest = false;
|
|
HTTPRequestStates requestState = HTTPRequestStates.Finished;
|
|
HTTPConnectionStates connectionState = HTTPConnectionStates.Recycle;
|
|
Exception error = ex;
|
|
|
|
if (error != null)
|
|
{
|
|
// Timeout is a non-retryable error
|
|
if (ex is TimeoutException)
|
|
{
|
|
error = null;
|
|
requestState = HTTPRequestStates.TimedOut;
|
|
}
|
|
else if (ex is HTTPCacheAcquireLockException)
|
|
{
|
|
error = null;
|
|
resendRequest = true;
|
|
}
|
|
else
|
|
{
|
|
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries)
|
|
{
|
|
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.
|
|
|
|
KeepAliveHeader keepAlive = null;
|
|
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref keepAlive, this.Context);
|
|
connectionState = HTTPConnectionStates.Recycle;
|
|
|
|
if (error != null)
|
|
requestState = HTTPRequestStates.Error;
|
|
else if (!resendRequest && resp.IsUpgraded)
|
|
requestState = HTTPRequestStates.Processing;
|
|
}
|
|
|
|
req.Timing.StartNext(TimingEventNames.Queued);
|
|
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{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, req);
|
|
}
|
|
else if (resendRequest && requestState == HTTPRequestStates.Finished)
|
|
{
|
|
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, connectionState));
|
|
}
|
|
else
|
|
{
|
|
// Otherwise set the request's then the connection's state
|
|
ConnectionHelper.EnqueueEvents(this, connectionState, req, requestState, error);
|
|
}
|
|
}
|
|
|
|
internal void OnDownloadProgress(int down, int total) => RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.CurrentRequest, RequestEvents.DownloadProgress, down, total));
|
|
internal void OnUploadProgress(int up, int total) => RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.CurrentRequest, RequestEvents.UploadProgress, up, total));
|
|
|
|
internal void OnTimeout()
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnTimeout", this.Context);
|
|
|
|
CurrentRequest.Response = null;
|
|
CurrentRequest.State = HTTPRequestStates.TimedOut;
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
|
}
|
|
|
|
internal void OnAborted()
|
|
{
|
|
if (HTTPManager.Logger.IsDiagnostic)
|
|
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnAborted", this.Context);
|
|
|
|
CurrentRequest.Response = null;
|
|
CurrentRequest.State = HTTPRequestStates.Aborted;
|
|
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
|
|
if (disposing)
|
|
{
|
|
WebGLXHRNativeConnectionLayer.Remove(NativeId);
|
|
WebGLXHRNativeInterface.XHR_Release(NativeId);
|
|
|
|
this.ContentProvider?.Dispose();
|
|
this.ContentProvider = null;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
|
|
#endif
|