#region License /* Copyright (c) 2010-2014 Danko Kozar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion License using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Xml.Serialization; using eDriven.Core.Events; using eDriven.Core.Managers; using eDriven.Core.Serialization; using UnityEngine; using Event=eDriven.Core.Events.Event; using Object=UnityEngine.Object; namespace eDriven.Networking.Rpc { /// /// A class that handles HTTP requests/responses /// It can queue requests and send them one-by-one or in any other fashion specified by ConcurencyMode /// After the responses are returned, it processes them (fires responders) in fashion specified by ProcessingMode /// For instence, you could make the connector to fire all the responders, but only after all responses are returned /// public class HttpConnector : EventDispatcher, IUnique, ICloneable { #region Static #if DEBUG public new static bool DebugMode; #endif /// /// This event fires when number of finished requests equals total number of requests /// public static string ALL_PROCESSED = "allProcessed"; /// /// This event fires when any request is in timeout /// public const string TIMEOUT = "timeout"; /// /// Displays the overal progress /// //public static readonly HttpServiceProgressAggregator ProgressAggregator = new HttpServiceProgressAggregator(); #endregion #region Properties #region Implementation of IUnique /// /// The identifier of this HttpConnector /// [XmlAttribute("Id")] public string Id { get; set; } #endregion /// /// The URL of the server-side /// If defined, one can use Send() method without parameters /// [XmlAttribute("Url")] public string Url { get; set; } /// /// Randomizes the URL /// If set to true, the URL called by this connector gets the "&=something" suffix /// It assures that each requests gets the non-cached version of data /// [XmlAttribute("CacheBuster")] public bool CacheBuster; /// /// Should calls be written to log /// [DefaultValue(false)] public bool LogCalls; /// /// Overal fault handler for the service /// [XmlIgnore] public FaultHandler FaultHandler; ///// ///// Should the response time be measured and set as Duration parameter of token? ///// //[XmlAttribute] //[DefaultValue(true)] //public bool DoMeasureTime = true; /// /// How to handle multiple calls to the same service. /// The default concurrency value is multiple. /// [XmlAttribute] [DefaultValue(ConcurencyMode.Multiple)] public ConcurencyMode ConcurencyMode = ConcurencyMode.Multiple; // NOTE: Null exception error happen IN EDITOR when using Multiple concurency and more than one simultaneous call!!! // Use Queued mode and MaxConcurrentRequests=1 when in editor! /// /// How to handle multiple responses /// [XmlAttribute] [DefaultValue(ProcessingMode.Async)] public ProcessingMode ProcessingMode = ProcessingMode.Async; /// /// Request timeout in seconds /// [XmlAttribute] [DefaultValue(30f)] public float Timeout = 30f; /// /// Request timeout in seconds /// [XmlAttribute] [DefaultValue(false)] public bool ResetTimeoutOnProgress = true; /// /// Should system automatically dispose WWW resources (but after processing) /// [XmlAttribute] [DefaultValue(true)] protected bool AutoDisposeResources; /// /// You can peek to responses through this collection /// Available on ALL_PROCESSED event /// [XmlIgnore] public List Responses { get { return _forRemoval; } } /// /// A flag indicating that this connector is processing /// [XmlIgnore] public bool IsWorking { get; private set; } /// /// Max number of concurrent requests /// In FIFO and FILO mode /// Meaning it doesn't have to be processed one-by-one (the default) /// but two-by-two, three-by-three etc. /// [XmlAttribute] [DefaultValue(1)] public int MaxConcurrentRequests = 1; /// /// Response mode /// Token or WWW (the default is token) /// /// If ResponseMode.Token, the argument passed to result handler is AsyncToken, /// else if ResponseMode.WWW, the argument passed to result handler is current WWW /// public ResponseMode ResponseMode = ResponseMode.Token; #endregion #region Members /// /// Queued WWW requests /// When Send() is called, request is added to queue /// private readonly List _queued; /// /// Active list /// private readonly List _active; /// /// Finished list /// private readonly List _finished; /// /// List for synced modes /// private readonly List _synced; /// /// Requests ready for disposal /// private readonly List _forRemoval; /// /// Dictionary containing request to token relations /// //private readonly Dictionary _tokens; private int _previouslyActive; #endregion #region Default fault handler private static void DefaultFaultHandler(object data) { Debug.Log("Communication error: " + data); } #endregion #region Constructor /// /// Constructor /// public HttpConnector() { _queued = new List(); _active = new List(); _finished = new List(); _synced = new List(); _forRemoval = new List(); //ProgressAggregator.RegisterService(this); } #endregion #region Send method (main) /// /// Sends a request /// This method is always called when calling any of Send overrides /// /// /// public AsyncToken Send(WebRequest request) { if (string.IsNullOrEmpty(request.Url)) throw new Exception("URL not set"); if (CacheBuster) request.CacheBuster = true; #region Handling relative ULRs string url = request.Url; if (url.StartsWith("~")) { url = Url + request.Url.TrimStart('~'); } request.Url = url; #endregion #if DEBUG if (DebugMode || LogCalls) Debug.Log(string.Format(@"* {0} {1}", this, request)); #endif // create token AsyncToken token = new AsyncToken {Request = request, Timeout = Timeout}; // add it to queue _queued.Add(token); if (!SystemManager.Instance.UpdateSignal.HasSlot(UpdateSlot)) { #if DEBUG if (DebugMode) { Debug.Log("Connecting to UpdateSignal"); } #endif SystemManager.Instance.UpdateSignal.Connect(UpdateSlot); } IsWorking = true; DispatchStatusEvent(); // return it to caller return token; } #endregion #region Send overrides /// /// Sends a request /// /// /// /// public AsyncToken Send(WebRequest request, params Responder[] responders) { AsyncToken token = Send(request); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// /// public AsyncToken Send(WebRequest request, params ResultHandler[] resultHandlers) { List responders = new List(); foreach (ResultHandler resultHandler in resultHandlers) { responders.Add(new Responder(resultHandler)); } return Send(request, responders.ToArray()); } /// /// Sends a request /// /// /// public AsyncToken Send() { if (string.IsNullOrEmpty(Url)) throw new Exception("Url not defined"); return Send(new WebRequest(Url)); } /// /// Sends a request /// /// /// /// public AsyncToken Send(params Responder[] responders) { if (string.IsNullOrEmpty(Url)) throw new Exception("Url not defined"); AsyncToken token = Send(new WebRequest(Url)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// public AsyncToken Send(string url) { return Send(new WebRequest(url)); } /// /// Sends a request /// /// /// /// public AsyncToken Send(string url, params Responder[] responders) { AsyncToken token = Send(new WebRequest(url)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// /// public AsyncToken Send(string url, params ResultHandler[] resultHandlers) { List responders = new List(); foreach (ResultHandler resultHandler in resultHandlers) { responders.Add(new Responder(resultHandler)); } return Send(url, responders.ToArray()); } /// /// Sends a request /// /// /// /// public AsyncToken Send(string url, byte[] postData) { return Send(new WebRequest(url, postData)); } /// /// Sends a request /// /// /// /// /// public AsyncToken Send(string url, byte[] postData, params Responder[] responders) { AsyncToken token = Send(new WebRequest(url, postData)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// /// /// public AsyncToken Send(string url, byte[] postData, Hashtable headers) { return Send(new WebRequest(url, postData, headers)); } /// /// Sends a request /// /// /// /// /// /// public AsyncToken Send(string url, byte[] postData, Hashtable headers, params Responder[] responders) { AsyncToken token = Send(new WebRequest(url, postData, headers)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// /// public AsyncToken Send(string url, WWWForm form) { return Send(new WebRequest(url, form)); } /// /// Sends a request /// /// /// /// /// public AsyncToken Send(string url, WWWForm form, params Responder[] responders) { AsyncToken token = Send(new WebRequest(url, form)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// public AsyncToken Send(byte[] postData) { return Send(new WebRequest(Url, postData)); } /// /// Sends a request /// /// /// /// public AsyncToken Send(byte[] postData, params Responder[] responders) { AsyncToken token = Send(new WebRequest(Url, postData)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// /// public AsyncToken Send(byte[] postData, Hashtable headers) { return Send(new WebRequest(Url, postData, headers)); } /// /// Sends a request /// /// /// /// /// public AsyncToken Send(byte[] postData, Hashtable headers, params Responder[] responders) { AsyncToken token = Send(new WebRequest(Url, postData, headers)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// public AsyncToken Send(WWWForm form) { return Send(new WebRequest(Url, form)); } /// /// Sends a request /// /// /// /// public AsyncToken Send(WWWForm form, params Responder[] responders) { AsyncToken token = Send(new WebRequest(Url, form)); HandleResponders(responders, token); return token; } /// /// Sends a request /// /// /// /// /// public AsyncToken Send(WWWForm form, Hashtable headers, params Responder[] responders) { AsyncToken token = Send(new WebRequest(Url, form, headers)); HandleResponders(responders, token); return token; } #endregion #region Cancel /// /// Cancels the request /// /// public void Cancel(AsyncToken token) { // todo: execute responders! (we need this to remove progress bars etc.) token.Dispose(); _queued.Remove(token); _active.Remove(token); _finished.Remove(token); _synced.Remove(token); _forRemoval.Remove(token); DispatchStatusEvent(); } /// /// Cansels all requests /// public void CancelAll() { Dispose(); DispatchStatusEvent(); } #endregion #region Update /// /// Update slot /// /// private void UpdateSlot(params object[] parameters) { ProcessQueued(); ProcessActive(); ProcessFinished(); if (_queued.Count == 0 && _active.Count == 0 && _finished.Count == 0) SystemManager.Instance.UpdateSignal.Disconnect(UpdateSlot); } /// /// Processes queued requests /// private void ProcessQueued() { // transfer disposed tokens _queued.ForEach(delegate(AsyncToken token) { if (token.HasBeenDisposed) { ProcessResponders(token); //_finished.Add(token); } }); _queued.RemoveAll(delegate(AsyncToken token) { return token.HasBeenDisposed; }); if (_queued.Count > 0) { int diff = MaxConcurrentRequests - _active.Count; switch (ConcurencyMode) { case ConcurencyMode.SingleLast: // get the last one _active.Clear(); // clear active and do not process any current responses QueuedToActive(_queued[_queued.Count - 1]); _queued.Clear(); break; case ConcurencyMode.SingleFirst: if (_active.Count > 0) // there are active requests already _queued.Clear(); else if (_queued.Count > 1) // no active, but more than one queued { QueuedToActive(_queued[0]); _queued.Clear(); } break; case ConcurencyMode.FifoQueued: //if (_active.Count == 0) // if no active requests, add the first queued //{ // QueuedToActive(_queued[0]); //} if (diff > 0) // if no active requests, add the first queued { QueuedToActive(_queued.GetRange(0, Math.Min(diff, _queued.Count))); } break; case ConcurencyMode.FiloQueued: //if (_active.Count == 0) // if no active requests, add the last queued //{ // QueuedToActive(_queued[_queued.Count - 1]); //} if (diff > 0) // if no active requests, add the first queued { QueuedToActive(_queued[_queued.Count - Math.Min(diff, _queued.Count)]); } break; case ConcurencyMode.Multiple: //default: // transfer ALL queued to active QueuedToActive(_queued); break; } } } private void QueuedToActive(AsyncToken token) { #if DEBUG if (DebugMode) Debug.Log(GetInfo()); #endif // create WWW GenerateWww(token); _active.Add(token); // in Sync and SyncAll modes add it to synced list also if (ProcessingMode.Async != ProcessingMode) _synced.Add(token); // remove from queued _queued.Remove(token); #if DEBUG if (DebugMode || LogCalls) { Debug.Log(string.Format(@"{0} queued --1--> {1} active", _queued.Count, _active.Count)); } #endif DispatchStatusEvent(); } private void QueuedToActive(List tokens) { #if DEBUG if (DebugMode) Debug.Log(GetInfo()); #endif #pragma warning disable 168 int count = tokens.Count; #pragma warning restore 168 // temp ICollection tempTokens = new List(); tokens.ForEach(delegate (AsyncToken request) { GenerateWww(request); tempTokens.Add(request); }); _active.AddRange(tempTokens); // in Sync and SyncAll modes add it to synced list also if (ProcessingMode.Async != ProcessingMode) _synced.AddRange(tempTokens); // clear temp tempTokens.Clear(); _queued.RemoveAll(delegate(AsyncToken token) { return tokens.Contains(token); }); #if DEBUG if (DebugMode || LogCalls) { Debug.Log(string.Format(@"{0} queued --{1}-->{2} active", _queued.Count, count, _active.Count)); } #endif DispatchStatusEvent(); } private void ProcessActive() { _previouslyActive = _active.Count; if (_previouslyActive > 0) { #if DEBUG if (DebugMode){ Debug.Log(GetInfo()); Debug.Log(string.Format("# Processing {0} active", _previouslyActive)); } #endif #pragma warning disable 219 int transferCount = 0; #pragma warning restore 219 /** * Look for requests that are done * */ _active.ForEach(delegate(AsyncToken token) { //if (token.HasBeenDisposed) //{ // _finished.Add(token); // transferCount++; // return; //} if (ResetTimeoutOnProgress && !token.HasBeenDisposed) token.CheckProgress(); double totalMilliseconds = DateTime.Now.Subtract(token.StartTime).TotalMilliseconds; bool isTimeout = totalMilliseconds > token.Timeout*1000; if (isTimeout){ Debug.Log(string.Format("Timeout ({0} sec)", token.Timeout)); token.IsTimeout = true; } if (token.HasBeenDisposed || token.Response.isDone || isTimeout) // NOTE: token.HasBeenDisposed has to be first check, because if it is, then token.Response is null { //if (DebugMode){ // Debug.Log("ProcessActive: token.Response.isDone: " + token.Response.isDone); //} token.Duration = totalMilliseconds; /** * If request is done (even if in error), add it to finished requests and raise event * */ _finished.Add(token); transferCount++; } }); // remove all finished from _active list _active.RemoveAll(delegate(AsyncToken token) { return _finished.Contains(token); }); if (_active.Count < _previouslyActive) // e.g. we moved some requests to finished { #if DEBUG if (DebugMode || LogCalls) { Debug.Log(string.Format(@"{0} active --{1}--> {2} finished", _active.Count, transferCount, _finished.Count)); //Debug.Log(string.Format(@"active --{0}--> finished", transferCount)); } #endif DispatchStatusEvent(); } } } private void DispatchStatusEvent() { if (HasEventListener(HttpServiceProgressEvent.PROGRESS_CHANGE)) { HttpServiceProgressEvent e = new HttpServiceProgressEvent(HttpServiceProgressEvent.PROGRESS_CHANGE) { Active = _active.Count, Finished = _finished.Count, Total = _queued.Count + _active.Count + _finished.Count }; DispatchEvent(e); } } private void ProcessFinished() { _finished.RemoveAll(delegate(AsyncToken token) { return token.HasBeenDisposed; }); if (_finished.Count > 0) { /** * Check if there are no more active requests, * and the last active request wes removed from _active collection on this update * */ bool noMoreActiveRequests = _previouslyActive > 0 && _queued.Count == 0 && _active.Count == 0; // are all requests done (no more active requests) // reset _previouslyActive _previouslyActive = 0; switch (ProcessingMode) { /** * ProcessingMode.Async * Proccess requests as they are finished * */ case ProcessingMode.Async: // process all finished immediatelly DoProcessAsync(noMoreActiveRequests); break; /** * ProcessingMode.Sync * We process results only if previous results in the list are processed * Synced list holds active + finished members * If the index in synced list and the index in finished list are the same, we should proccess the response * */ case ProcessingMode.Sync: DoProcessSync(noMoreActiveRequests); break; /** * ProcessingMode.SyncAll * Proccess responses only if there are no active requests anymore * */ case ProcessingMode.SyncAll: if (noMoreActiveRequests) DoProcessSync(true); break; } } } private void DoProcessSync(bool doDispatchAllProcessedEvent) { #if DEBUG if (DebugMode) { //Debug.Log(GetInfo()); Debug.Log(string.Format("# Processing {0} finished [synced mode])", _finished.Count)); } #endif //HandleTimeouts(); //DispatchStatusEvent(); //_synced.RemoveAll(delegate(AsyncToken token) // remove timeouts // { // return !token.Response.isDone; // }); _forRemoval.Clear(); foreach (AsyncToken token in _synced) { // checks if indexes are the same if (_finished.Contains(token)) { _forRemoval.Add(token); } else { goto next; // break out of the loop as soon as non-finished www found } } //Debug.Log("_forRemoval: " + _forRemoval.Count); next: // label // remove all that are not synced anymore _synced.RemoveAll(delegate(AsyncToken token) { return _forRemoval.Contains(token); }); _finished.RemoveAll(delegate(AsyncToken token) { return _forRemoval.Contains(token); }); _forRemoval.ForEach(delegate(AsyncToken token) { // process WWW try { //if (DebugMode) // Debug.Log("DoProcessSync: token.Response.isDone: " + token.Response.isDone); ProcessResponders(token); } //catch (JsonServiceException ex) //{ // // do not re-throw // // this request should be removed // Debug.Log(ex); //} catch (ArgumentNullException ex) { Debug.Log(ex); } }); if (doDispatchAllProcessedEvent) { _synced.Clear(); _finished.Clear(); #if DEBUG if (DebugMode ) { Debug.Log("All finished"); //Debug.Log(string.Format(@"active --{0}--> finished", transferCount)); } #endif if (LogCalls) { Debug.Log("All finished"); //Debug.Log(string.Format(@"active --{0}--> finished", transferCount)); } IsWorking = false; // dispatch ALL_PROCESSED event DispatchEvent(new Event(ALL_PROCESSED)); } if (AutoDisposeResources) { // disposal _forRemoval.ForEach(delegate(AsyncToken d) { DisposeResources(d.Response); }); } _forRemoval.Clear(); DispatchStatusEvent(); #if DEBUG if (DebugMode) Debug.Log(GetInfo()); #endif } private void DoProcessAsync(bool doDispatchAllProcessedEvent) { #if DEBUG if (DebugMode) { //Debug.Log(GetInfo()); Debug.Log(string.Format("# Processing {0} finished", _finished.Count)); } #endif //HandleTimeouts(); // prepare for list removal _forRemoval.Clear(); _forRemoval.AddRange(_finished); //DispatchStatusEvent(); // clear finished _finished.Clear(); // process responders _forRemoval.ForEach(delegate(AsyncToken token) { //if (DebugMode) // Debug.Log("DoProcessAsync: token.Response.isDone: " + token.Response.isDone); ProcessResponders(token); }); // dispatch ALL_PROCESSED event if (doDispatchAllProcessedEvent) { IsWorking = false; DispatchEvent(new Event(ALL_PROCESSED)); } // disposal if (AutoDisposeResources) _forRemoval.ForEach(delegate(AsyncToken token) { DisposeResources(token.Response); }); // now we can clear _forRemoval collection _forRemoval.Clear(); DispatchStatusEvent(); #if DEBUG if (DebugMode) Debug.Log(GetInfo()); #endif } #region _crap //private void HandleTimeouts() //{ // List timeoutRequests = _finished.FindAll(delegate(AsyncToken token) { return !token.Response.isDone; }); // timeoutRequests.ForEach(delegate(AsyncToken token) // { // token.Responders.ForEach(delegate(Responder responder) // { // responder.Fault("Request timeout"); // }); // }); // if (timeoutRequests.Count > 0) // DispatchEvent(new Event(TIMEOUT)); // // remove responses that have time out // _finished.RemoveAll(delegate(AsyncToken token) // { // return !token.Response.isDone; // }); //} #endregion private void ProcessResponders(AsyncToken token) { if (token.IsTimeout){ DispatchEvent(new Event(TIMEOUT)); token.Response.Dispose(); } /* Timeout */ if (token.IsTimeout /*|| !token.Response.isDone*/) // response never returned, timeout expired { #if DEBUG if (DebugMode) { Debug.Log("1 request timeout"); } #endif token.Responders.ForEach(delegate(Responder responder) { // If no fault handler defined for the responder, // and one is defined globally // reference that handler if (null == responder.FaultHandler) { responder.FaultHandler = FaultHandler ?? DefaultFaultHandler; } responder.Fault("Request timeout"); }); } /* Error */ else if (!string.IsNullOrEmpty(token.Response.error)) // NOTE: Response could have isDone == true and still have and error! { #if DEBUG if (DebugMode) { Debug.Log("Error: " + token.Response.error); } #endif token.Responders.ForEach(delegate(Responder responder) { // If no fault handler defined for the responder, // and one is defined globally // reference that handler if (null == responder.FaultHandler) { responder.FaultHandler = FaultHandler ?? DefaultFaultHandler; } responder.Fault(token.Response.error); }); } /* OK */ else { //token.Result = token.Response; // add result to token (for alternative processing) //Debug.Log(1); token.Responders.ForEach(delegate(Responder responder) { try { //if (DebugMode) // Debug.Log("ProcessResponders: token.Response.isDone: " + token.Response.isDone); //Debug.Log(2); switch (ResponseMode) { case ResponseMode.Token: responder.Result(token); break; case ResponseMode.WWW: responder.Result(token.Response); break; } //responder.Result((ResponseMode == ResponseMode.Token) ? token : token.Response); } catch (Exception ex) { Debug.Log("Error executing result responder: " + ex); // NOTE: Silent fail } }); } // clear responders token.ClearResponders(); } /// /// Disposes resources on WWW /// /// public static void DisposeResources(WWW response) { //if (Application.isEditor) // Edit mode //{ // Object.DestroyImmediate(response.audioClip); // Object.DestroyImmediate(response.movie); // Object.DestroyImmediate(response.texture); //} //else // app mode //{ // Object.Destroy(response.audioClip); // Object.Destroy(response.movie); // Object.Destroy(response.texture); //} response.Dispose(); //response = null; //System.GC.Collect(); } public override void Dispose() { base.Dispose(); _queued.ForEach(delegate(AsyncToken token) { token.Dispose(); }); _active.ForEach(delegate(AsyncToken token) { token.Dispose(); }); _finished.ForEach(delegate(AsyncToken token) { token.Dispose(); }); _queued.Clear(); _active.Clear(); _finished.Clear(); _synced.Clear(); _forRemoval.Clear(); IsWorking = false; SystemManager.Instance.UpdateSignal.Disconnect(UpdateSlot); } //public void DisposeAllResponses() //{ // _active.ForEach(delegate(AsyncToken token) // { // DisposeResources(token.Response); // }); //} #endregion #region Helper private void GenerateWww(AsyncToken token) { token.Response = token.Request.CreateWww(); // the WWW request is now alive! token.Timeout = Timeout; token.StartTime = DateTime.Now; } private static void HandleResponders(IEnumerable responders, AsyncToken token) { foreach (Responder responder in responders) { token.AddResponder(responder); } } private string GetInfo() { return string.Format(@"[{0} queued; {1} active; {2} finished; ({3} synced;)]{4}", _queued.Count, _active.Count, _finished.Count, _synced.Count, string.IsNullOrEmpty(Id) ? "" : string.Format(" //{0}", Id)); } #endregion public override string ToString() { return string.Format(@"[HttpConnector [Id=""{0}"", Url=""{1}"", ConcurencyMode=""{2}"", ProcessingMode=""{3}"", MaxConcurrentRequests=""{4}"", Timeout={5}]] Queued: {6}; Active: {7}; Finished: {8}", Id, Url, ConcurencyMode, ProcessingMode, MaxConcurrentRequests, Timeout, _queued.Count, _active.Count, _finished.Count); } /// /// Creates a new object that is a copy of the current instance. /// /// /// A new object that is a copy of this instance. /// /// 2 public object Clone() { return MemberwiseClone(); // shalow copy } /// /// Stops all requests from processing /// public void StopAll() { #if DEBUG if (DebugMode) { Debug.Log("HttpConnector->StopAll"); } #endif Dispose(); } } }