using BestHTTP.Extensions; using BestHTTP.Logger; using BestHTTP.PlatformSupport.Memory; using BestHTTP.Timings; using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace BestHTTP.Core { public enum RequestEvents { Upgraded, DownloadProgress, UploadProgress, StreamingData, StateChange, Resend, Headers, TimingData } public #if CSHARP_7_OR_LATER readonly #endif struct RequestEventInfo { public readonly HTTPRequest SourceRequest; public readonly RequestEvents Event; public readonly HTTPRequestStates State; public readonly long Progress; public readonly long ProgressLength; public readonly byte[] Data; public readonly int DataLength; // Timing Data public readonly string Name; public readonly DateTime Time; public readonly TimeSpan Duration; // Headers public readonly Dictionary> Headers; public RequestEventInfo(HTTPRequest request, RequestEvents @event) { this.SourceRequest = request; this.Event = @event; this.State = HTTPRequestStates.Initial; this.Progress = this.ProgressLength = 0; this.Data = null; this.DataLength = 0; // TimingData this.Name = null; this.Time = DateTime.MinValue; this.Duration = TimeSpan.Zero; // Headers this.Headers = null; } public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState) { this.SourceRequest = request; this.Event = RequestEvents.StateChange; this.State = newState; this.Progress = this.ProgressLength = 0; this.Data = null; this.DataLength = 0; // TimingData this.Name = null; this.Time = DateTime.MinValue; this.Duration = TimeSpan.Zero; // Headers this.Headers = null; } public RequestEventInfo(HTTPRequest request, RequestEvents @event, long progress, long progressLength) { this.SourceRequest = request; this.Event = @event; this.State = HTTPRequestStates.Initial; this.Progress = progress; this.ProgressLength = progressLength; this.Data = null; this.DataLength = 0; // TimingData this.Name = null; this.Time = DateTime.MinValue; this.Duration = TimeSpan.Zero; // Headers this.Headers = null; } public RequestEventInfo(HTTPRequest request, byte[] data, int dataLength) { this.SourceRequest = request; this.Event = RequestEvents.StreamingData; this.State = HTTPRequestStates.Initial; this.Progress = this.ProgressLength = 0; this.Data = data; this.DataLength = dataLength; // TimingData this.Name = null; this.Time = DateTime.MinValue; this.Duration = TimeSpan.Zero; // Headers this.Headers = null; } public RequestEventInfo(HTTPRequest request, string name, DateTime time) { this.SourceRequest = request; this.Event = RequestEvents.TimingData; this.State = HTTPRequestStates.Initial; this.Progress = this.ProgressLength = 0; this.Data = null; this.DataLength = 0; // TimingData this.Name = name; this.Time = time; this.Duration = TimeSpan.Zero; // Headers this.Headers = null; } public RequestEventInfo(HTTPRequest request, string name, TimeSpan duration) { this.SourceRequest = request; this.Event = RequestEvents.TimingData; this.State = HTTPRequestStates.Initial; this.Progress = this.ProgressLength = 0; this.Data = null; this.DataLength = 0; // TimingData this.Name = name; this.Time = DateTime.Now; this.Duration = duration; // Headers this.Headers = null; } public RequestEventInfo(HTTPRequest request, Dictionary> headers) { this.SourceRequest = request; this.Event = RequestEvents.Headers; this.State = HTTPRequestStates.Initial; this.Progress = this.ProgressLength = 0; this.Data = null; this.DataLength = 0; // TimingData this.Name = null; this.Time = DateTime.MinValue; this.Duration = TimeSpan.Zero; // Headers this.Headers = headers; } public override string ToString() { switch (this.Event) { case RequestEvents.Upgraded: return string.Format("[RequestEventInfo SourceRequest: {0}, Event: Upgraded]", this.SourceRequest.CurrentUri); case RequestEvents.DownloadProgress: return string.Format("[RequestEventInfo SourceRequest: {0}, Event: DownloadProgress, Progress: {1}, ProgressLength: {2}]", this.SourceRequest.CurrentUri, this.Progress, this.ProgressLength); case RequestEvents.UploadProgress: return string.Format("[RequestEventInfo SourceRequest: {0}, Event: UploadProgress, Progress: {1}, ProgressLength: {2}]", this.SourceRequest.CurrentUri, this.Progress, this.ProgressLength); case RequestEvents.StreamingData: return string.Format("[RequestEventInfo SourceRequest: {0}, Event: StreamingData, DataLength: {1}]", this.SourceRequest.CurrentUri, this.DataLength); case RequestEvents.StateChange: return string.Format("[RequestEventInfo SourceRequest: {0}, Event: StateChange, State: {1}]", this.SourceRequest.CurrentUri, this.State); case RequestEvents.Resend: return string.Format("[RequestEventInfo SourceRequest: {0}, Event: Resend]", this.SourceRequest.CurrentUri); case RequestEvents.Headers: return string.Format("[RequestEventInfo SourceRequest: {0}, Event: Headers]", this.SourceRequest.CurrentUri); case RequestEvents.TimingData: if (this.Duration == TimeSpan.Zero) return string.Format("[RequestEventInfo SourceRequest: {0}, Event: TimingData, Name: {1}, Time: {2}]", this.SourceRequest.CurrentUri, this.Name, this.Time); else return string.Format("[RequestEventInfo SourceRequest: {0}, Event: TimingData, Name: {1}, Time: {2}, Duration: {3}]", this.SourceRequest.CurrentUri, this.Name, this.Time, this.Duration); default: throw new NotImplementedException(this.Event.ToString()); } } } class ProgressFlattener { struct FlattenedProgress { public HTTPRequest request; public OnProgressDelegate onProgress; public long progress; public long length; } private FlattenedProgress[] progresses; private bool hasProgress; public void InsertOrUpdate(RequestEventInfo info, OnProgressDelegate onProgress) { if (progresses == null) progresses = new FlattenedProgress[1]; hasProgress = true; var newProgresss = new FlattenedProgress { request = info.SourceRequest, progress = info.Progress, length = info.ProgressLength, onProgress = onProgress }; int firstEmptyIdx = -1; for (int i = 0; i < progresses.Length; i++) { var progress = progresses[i]; if (object.ReferenceEquals(progress.request, info.SourceRequest)) { progresses[i] = newProgresss; return; } if (firstEmptyIdx == -1 && progress.request == null) firstEmptyIdx = i; } if (firstEmptyIdx == -1) { Array.Resize(ref progresses, progresses.Length + 1); progresses[progresses.Length - 1] = newProgresss; } else progresses[firstEmptyIdx] = newProgresss; } public void DispatchProgressCallbacks() { if (progresses == null || !hasProgress) return; for (int i = 0; i < progresses.Length; ++i) { var @event = progresses[i]; var source = @event.request; if (source != null && @event.onProgress != null) { try { @event.onProgress(source, @event.progress, @event.length); } catch (Exception ex) { HTTPManager.Logger.Exception("ProgressFlattener", "DispatchProgressCallbacks", ex, source.Context); } } } Array.Clear(progresses, 0, progresses.Length); hasProgress = false; } } public static class RequestEventHelper { private static ConcurrentQueue requestEventQueue = new ConcurrentQueue(); #pragma warning disable 0649 public static Action OnEvent; #pragma warning restore // Low frame rate and hight download/upload speed can add more download/upload progress events to dispatch in one frame. // This can add higher CPU usage as it might cause updating the UI/do other things unnecessary in the same frame. // To avoid this, instead of calling the events directly, we store the last event's data and call download/upload callbacks only once per frame. private static ProgressFlattener downloadProgress; private static ProgressFlattener uploadProgress; public static void EnqueueRequestEvent(RequestEventInfo @event) { if (HTTPManager.Logger.Level == Loglevels.All) HTTPManager.Logger.Information("RequestEventHelper", "Enqueue request event: " + @event.ToString(), @event.SourceRequest.Context); requestEventQueue.Enqueue(@event); } internal static void Clear() { requestEventQueue.Clear(); } internal static void ProcessQueue() { RequestEventInfo requestEvent; while (requestEventQueue.TryDequeue(out requestEvent)) { HTTPRequest source = requestEvent.SourceRequest; if (HTTPManager.Logger.Level == Loglevels.All) HTTPManager.Logger.Information("RequestEventHelper", "Processing request event: " + requestEvent.ToString(), source.Context); if (OnEvent != null) { try { OnEvent(requestEvent); } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "ProcessQueue", ex, source.Context); } } switch (requestEvent.Event) { case RequestEvents.StreamingData: { var response = source.Response; if (response != null) System.Threading.Interlocked.Decrement(ref response.UnprocessedFragments); bool reuseBuffer = true; try { if (source.UseStreaming) reuseBuffer = source.OnStreamingData(source, response, requestEvent.Data, requestEvent.DataLength); } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.StreamingData", ex, source.Context); } if (reuseBuffer) BufferPool.Release(requestEvent.Data); break; } case RequestEvents.DownloadProgress: try { if (source.OnDownloadProgress != null) { if (downloadProgress == null) downloadProgress = new ProgressFlattener(); downloadProgress.InsertOrUpdate(requestEvent, source.OnDownloadProgress); } } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.DownloadProgress", ex, source.Context); } break; case RequestEvents.UploadProgress: try { if (source.OnUploadProgress != null) { if (uploadProgress == null) uploadProgress = new ProgressFlattener(); uploadProgress.InsertOrUpdate(requestEvent, source.OnUploadProgress); } } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.UploadProgress", ex, source.Context); } break; #if !UNITY_WEBGL || UNITY_EDITOR case RequestEvents.Upgraded: try { if (source.OnUpgraded != null) source.OnUpgraded(source, source.Response); } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.Upgraded", ex, source.Context); } IProtocol protocol = source.Response as IProtocol; if (protocol != null) ProtocolEventHelper.AddProtocol(protocol); break; #endif case RequestEvents.Resend: source.State = HTTPRequestStates.Initial; var host = HostManager.GetHost(source.CurrentUri.Host); host.Send(source); break; case RequestEvents.Headers: { try { var response = source.Response; if (source.OnHeadersReceived != null && response != null) source.OnHeadersReceived(source, response, requestEvent.Headers); } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.Headers", ex, source.Context); } break; } case RequestEvents.StateChange: try { RequestEventHelper.HandleRequestStateChange(requestEvent); } catch(Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange", ex, source.Context); } break; case RequestEvents.TimingData: source.Timing.AddEvent(requestEvent.Name, requestEvent.Time, requestEvent.Duration); break; } } uploadProgress?.DispatchProgressCallbacks(); downloadProgress?.DispatchProgressCallbacks(); } private static bool AbortRequestWhenTimedOut(DateTime now, object context) { HTTPRequest request = context as HTTPRequest; if (request.State >= HTTPRequestStates.Finished) return false; // don't repeat // Protocols will shut down themselves if (request.Response is IProtocol) return false; if (request.IsTimedOut) { HTTPManager.Logger.Information("RequestEventHelper", "AbortRequestWhenTimedOut - Request timed out. CurrentUri: " + request.CurrentUri.ToString(), request.Context); request.Abort(); return false; // don't repeat } return true; // repeat } internal static void HandleRequestStateChange(RequestEventInfo @event) { HTTPRequest source = @event.SourceRequest; // Because there's a race condition between setting the request's State in its Abort() function running on Unity's main thread // and the HTTP1/HTTP2 handlers running on an another one. // Because of these race conditions cases violating expectations can be: // 1.) State is finished but the response null // 2.) State is (Connection)TimedOut and the response non-null // We have to make sure that no callbacks are called twice and in the request must be in a consistent state! // State | Request // --------- +--------- // 1 Null // Finished | Skip // Timeout/Abort | Deliver // // 2 Non-Null // Finished | Deliver // Timeout/Abort | Skip switch (@event.State) { case HTTPRequestStates.Queued: source.QueuedAt = DateTime.UtcNow; if ((!source.UseStreaming && source.UploadStream == null) || source.EnableTimoutForStreaming) BestHTTP.Extensions.Timer.Add(new TimerData(TimeSpan.FromSeconds(1), @event.SourceRequest, AbortRequestWhenTimedOut)); break; case HTTPRequestStates.ConnectionTimedOut: case HTTPRequestStates.TimedOut: case HTTPRequestStates.Error: case HTTPRequestStates.Aborted: source.Response = null; goto case HTTPRequestStates.Finished; case HTTPRequestStates.Finished: #if !BESTHTTP_DISABLE_CACHING // Here we will try to load content for a failed load. Failed load is a request with ConnectionTimedOut, TimedOut or Error state. // A request with Finished state but response with status code >= 500 also something that we will try to load from the cache. // We have to set what we going to try to load here too (other place is inside IsCachedEntityExpiresInTheFuture) as we don't want to load a cached content for // a request that just finished without any problem! try { bool tryLoad = !source.DisableCache && source.State != HTTPRequestStates.Aborted && (source.State != HTTPRequestStates.Finished || source.Response == null || source.Response.StatusCode >= 500); if (tryLoad && Caching.HTTPCacheService.IsCachedEntityExpiresInTheFuture(source)) { HTTPManager.Logger.Information("RequestEventHelper", "IsCachedEntityExpiresInTheFuture check returned true! CurrentUri: " + source.CurrentUri.ToString(), source.Context); PlatformSupport.Threading.ThreadedRunner.RunShortLiving((req) => { // Disable any other cache activity. req.DisableCache = true; var originalState = req.State; if (Connections.ConnectionHelper.TryLoadAllFromCache("RequestEventHelper", req, req.Context)) { if (req.State != HTTPRequestStates.Finished) req.State = HTTPRequestStates.Finished; else RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, HTTPRequestStates.Finished)); } else { HTTPManager.Logger.Information("RequestEventHelper", "TryLoadAllFromCache failed to load! CurrentUri: " + req.CurrentUri.ToString(), source.Context); // If for some reason it couldn't load we place back the request to the queue. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, originalState)); } }, source); break; } } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", string.Format("HandleRequestStateChange - Cache probe - CurrentUri: \"{0}\" State: {1} StatusCode: {2}", source.CurrentUri, source.State, source.Response != null ? source.Response.StatusCode : 0), ex, source.Context); } #endif // Dispatch any collected download/upload progress, otherwise they would _after_ the callback! uploadProgress?.DispatchProgressCallbacks(); downloadProgress?.DispatchProgressCallbacks(); source.Timing.AddEvent(TimingEventNames.Queued_For_Disptach, DateTime.Now, TimeSpan.Zero); source.Timing.AddEvent(TimingEventNames.Finished, DateTime.Now, DateTime.Now - source.Timing.Start); if (source.Callback != null) { try { source.Callback(source, source.Response); source.Timing.AddEvent(TimingEventNames.Callback, DateTime.Now, TimeSpan.Zero); if (HTTPManager.Logger.Level <= Loglevels.Information) HTTPManager.Logger.Information("RequestEventHelper", "Finishing request. Timings: " + source.Timing.ToString(), source.Context); } catch (Exception ex) { HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange " + @event.State, ex, source.Context); } } source.Dispose(); HostManager.GetHost(source.CurrentUri.Host) .GetHostDefinition(HostDefinition.GetKeyForRequest(source)) .TryToSendQueuedRequests(); break; } } } }