// // Radegast Metaverse Client // Copyright (c) 2009-2014, Radegast Development Team // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the application "Radegast", nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // $Id$ // using System; using System.Collections.Generic; using System.Linq; using System.Text; #if (COGBOT_LIBOMV || USE_STHREADS) using ThreadPoolUtil; using Thread = ThreadPoolUtil.Thread; using ThreadPool = ThreadPoolUtil.ThreadPool; using Monitor = ThreadPoolUtil.Monitor; #endif using System.Threading; using System.IO; using OpenMetaverse; using OpenMetaverse.StructuredData; using System.Collections.Concurrent; namespace Raindrop { #region enums /// /// Enum representing different modes of handling display names /// public enum NameMode : int { /// No display names Standard, /// Display name followed by (username) if display name is not default Smart, /// Display name followed by (username) OnlyDisplayName, /// Only display DisplayNameAndUserName, } #endregion enums /// /// Manager for looking up avatar names and their caching /// public class NameManager : IDisposable { #region public fields and properties public event EventHandler NameUpdated; public NameMode Mode { get { if (!client.Avatars.DisplayNamesAvailable()) return NameMode.Standard; return (NameMode)instance.GlobalSettings["display_name_mode"].AsInteger(); } set { instance.GlobalSettings["display_name_mode"] = (int)value; } } #endregion public fields and properties #region private fields and properties RaindropInstance instance; GridClient client { get { return instance.Client; } } Timer requestTimer; Timer cacheTimer; string cacheFileName; Queue requests = new Queue(); int MaxNameRequests = 80; // Queue up name request for this many ms, and send a batch requst const int REQUEST_DELAY = 100; // Save name cache after change to names after this many ms const int CACHE_DELAY = 30000; // Consider request failed after this many ms const int MAX_REQ_AGE = 15000; Dictionary names = new Dictionary(); Dictionary activeRequests = new Dictionary(); ConcurrentQueue> PendingLookups; //BlockingQueue> PendingLookups; Thread requestThread = null; Semaphore lookupGate; bool useRequestThread; #endregion private fields and properties #region construction and disposal public NameManager(RaindropInstance instance) { this.instance = instance; requestTimer = new Timer(MakeRequest, null, Timeout.Infinite, Timeout.Infinite); cacheTimer = new Timer(SaveCache, null, Timeout.Infinite, Timeout.Infinite); cacheFileName = Path.Combine(instance.UserDir, "name.cache"); LoadCachedNames(); instance.ClientChanged += new EventHandler(instance_ClientChanged); RegisterEvents(client); // Mono HTTPWebRequest sucks balls useRequestThread = instance.MonoRuntime; if (useRequestThread) { PendingLookups = new ConcurrentQueue>(); lookupGate = new Semaphore(4, 4); requestThread = new Thread(new ThreadStart(RequestThread)); requestThread.IsBackground = true; requestThread.Name = "Display Name Request Thread"; requestThread.Start(); } } public void Dispose() { SaveCache(new object()); if (client != null) { DeregisterEvents(client); } if (requestTimer != null) { requestTimer.Dispose(); requestTimer = null; } if (cacheTimer != null) { cacheTimer.Dispose(); cacheTimer = null; } try { if (useRequestThread) { //PendingLookups.Close(); if (requestThread != null) { if (!requestThread.Join(5 * 1000)) { requestThread.Abort(); } requestThread = null; } } } catch { } } #endregion construction and disposal #region private methods public void RequestThread() { //PendingLookups.Open(); while (true) { List req = null; if (!PendingLookups.TryDequeue(/*Timeout.Infinite,*/ out req)) break; lookupGate.WaitOne(90 * 1000); client.Avatars.GetDisplayNames(req, (bool success, AgentDisplayName[] names, UUID[] badIDs) => { if (success) { ProcessDisplayNames(names); } else { Logger.Log("Failed fetching display names", Helpers.LogLevel.Warning, client); } lookupGate.Release(1); }); } } void RegisterEvents(GridClient c) { c.Avatars.UUIDNameReply += new EventHandler(Avatars_UUIDNameReply); c.Avatars.DisplayNameUpdate += new EventHandler(Avatars_DisplayNameUpdate); } void DeregisterEvents(GridClient c) { c.Avatars.UUIDNameReply -= new EventHandler(Avatars_UUIDNameReply); c.Avatars.DisplayNameUpdate -= new EventHandler(Avatars_DisplayNameUpdate); } DateTime UUIDNameOnly = new DateTime(1970, 9, 4, 10, 0, 0, DateTimeKind.Utc); void instance_ClientChanged(object sender, ClientChangedEventArgs e) { DeregisterEvents(e.OldClient); RegisterEvents(e.Client); } void Avatars_DisplayNameUpdate(object sender, DisplayNameUpdateEventArgs e) { lock (names) { e.DisplayName.Updated = DateTime.Now; names[e.DisplayName.ID] = e.DisplayName; } Dictionary ret = new Dictionary(); ret.Add(e.DisplayName.ID, FormatName(e.DisplayName)); TriggerEvent(ret); } void Avatars_UUIDNameReply(object sender, UUIDNameReplyEventArgs e) { Dictionary ret = new Dictionary(); foreach (KeyValuePair kvp in e.Names) { // Remove from the list of active requests if in UUID only (standard mode) if (Mode == NameMode.Standard) { lock (activeRequests) { activeRequests.Remove(kvp.Key); } } lock (names) { if (!names.ContainsKey(kvp.Key)) { names[kvp.Key] = new AgentDisplayName(); names[kvp.Key].ID = kvp.Key; names[kvp.Key].NextUpdate = UUIDNameOnly; names[kvp.Key].IsDefaultDisplayName = true; } names[kvp.Key].Updated = DateTime.Now; string[] parts = kvp.Value.Trim().Split(' '); if (parts.Length == 2) { if (InvalidName(names[kvp.Key].DisplayName)) { names[kvp.Key].DisplayName = string.Format("{0} {1}", parts[0], parts[1]); } names[kvp.Key].LegacyFirstName = parts[0]; names[kvp.Key].LegacyLastName = parts[1]; if (names[kvp.Key].LegacyLastName == "Resident") { names[kvp.Key].UserName = names[kvp.Key].LegacyFirstName.ToLower(); } else { names[kvp.Key].UserName = string.Format("{0}.{1}", parts[0], parts[1]).ToLower(); } ret.Add(kvp.Key, FormatName(names[kvp.Key])); } } } TriggerEvent(ret); TriggerCacheSave(); } string FormatName(AgentDisplayName n) { switch (Mode) { case NameMode.OnlyDisplayName: return n.DisplayName; case NameMode.Smart: if (n.IsDefaultDisplayName) return n.DisplayName; else return string.Format("{0} ({1})", n.DisplayName, n.UserName); case NameMode.DisplayNameAndUserName: return string.Format("{0} ({1})", n.DisplayName, n.UserName); default: return n.LegacyFullName; } } bool InvalidName(string displayName) { if (string.IsNullOrEmpty(displayName) || displayName == "???" || displayName == RaindropInstance.IncompleteName) { return true; } return false; } void ProcessDisplayNames(AgentDisplayName[] names) { Dictionary ret = new Dictionary(); foreach (var name in names) { // Remove from the list of active requests lock (activeRequests) { activeRequests.Remove(name.ID); } if (InvalidName(name.DisplayName)) continue; ret.Add(name.ID, FormatName(name)); name.Updated = DateTime.Now; lock (this.names) { this.names[name.ID] = name; } } TriggerEvent(ret); TriggerCacheSave(); } void MakeRequest(object sync) { List req = new List(); lock (requests) { while (requests.Count > 0) req.Add(requests.Dequeue()); } if (req.Count > 0) { if (Mode == NameMode.Standard || (!client.Avatars.DisplayNamesAvailable())) { client.Avatars.RequestAvatarNames(req); } else // Use display names { if (useRequestThread) { PendingLookups.Enqueue(new List(req)); } else { client.Avatars.GetDisplayNames(req, (bool success, AgentDisplayName[] names, UUID[] badIDs) => { if (success) { ProcessDisplayNames(names); } else { Logger.Log("Failed fetching display names", Helpers.LogLevel.Warning, client); } }); } } } } void SaveCache(object sync) { ThreadPool.QueueUserWorkItem(syncx => { OSDArray namesOSD = new OSDArray(names.Count); lock (names) { foreach (var name in names) { namesOSD.Add(name.Value.GetOSD()); } } OSDMap cache = new OSDMap(1); cache["names"] = namesOSD; byte[] data = OSDParser.SerializeLLSDBinary(cache, false); Logger.DebugLog(string.Format("Caching {0} avatar names to {1}", namesOSD.Count, cacheFileName)); try { File.WriteAllBytes(cacheFileName, data); } catch (Exception ex) { Logger.Log("Failed to save avatar name cache: ", Helpers.LogLevel.Error, client, ex); } }); } void LoadCachedNames() { ThreadPool.QueueUserWorkItem(syncx => { try { byte[] data = File.ReadAllBytes(cacheFileName); OSDMap cache = (OSDMap)OSDParser.DeserializeLLSDBinary(data); OSDArray namesOSD = (OSDArray)cache["names"]; DateTime now = DateTime.Now; TimeSpan maxAge = new TimeSpan(48, 0, 0); NameMode mode = (NameMode)(int)instance.GlobalSettings["display_name_mode"]; lock (names) { for (int i = 0; i < namesOSD.Count; i++) { AgentDisplayName name = AgentDisplayName.FromOSD(namesOSD[i]); if (mode == NameMode.Standard || ((now - name.Updated) < maxAge)) { names[name.ID] = name; } } } Logger.DebugLog(string.Format("Restored {0} names from the avatar name cache", names.Count)); } catch (Exception ex) { Logger.Log("Failed loading cached avatar names: ", Helpers.LogLevel.Warning, client, ex); } }); } void TriggerEvent(Dictionary ret) { if (NameUpdated == null || ret.Count == 0) return; try { NameUpdated(this, new UUIDNameReplyEventArgs(ret)); } catch (Exception ex) { Logger.Log("Failure in event handler: " + ex.Message, Helpers.LogLevel.Warning, client, ex); } } void TriggerNameRequest() { if (requestTimer != null) { requestTimer.Change(REQUEST_DELAY, Timeout.Infinite); } } void TriggerCacheSave() { if (cacheTimer != null) { cacheTimer.Change(CACHE_DELAY, Timeout.Infinite); } } void QueueNameRequest(UUID agentID) { // Check if we're already waiting for an answer on this one lock (activeRequests) { if (activeRequests.ContainsKey(agentID)) { // Logger.Log("Exiting is active " + agentID.ToString(), Helpers.LogLevel.Error); if (Environment.TickCount - activeRequests[agentID] < MAX_REQ_AGE) // Not timeout yet { // Logger.Log("Exiting is active " + agentID.ToString(), Helpers.LogLevel.Error); return; } else { // Logger.Log("Continuing, present but expired " + agentID.ToString(), Helpers.LogLevel.Error); } } else { // Logger.Log("Not present " + agentID.ToString(), Helpers.LogLevel.Error); } // Record time of when we're making this request activeRequests[agentID] = Environment.TickCount; } lock (requests) { if (!requests.Contains(agentID)) { // Logger.Log("Enqueueing " + agentID.ToString(), Helpers.LogLevel.Error); requests.Enqueue(agentID); if (requests.Count >= MaxNameRequests && Mode != NameMode.Standard) { requestTimer.Change(Timeout.Infinite, Timeout.Infinite); MakeRequest(this); } else { TriggerNameRequest(); } } } } #endregion private methods #region public methods /// /// Cleans avatar name cache /// public void CleanCache() { lock (names) { try { names.Clear(); File.Delete(cacheFileName); } catch { } } } /// /// Gets legacy First Last name /// /// UUID of the agent /// public string GetLegacyName(UUID agentID) { if (agentID == UUID.Zero) return "(???) (???)"; lock (names) { if (names.ContainsKey(agentID)) { return names[agentID].LegacyFullName; } } QueueNameRequest(agentID); return RaindropInstance.IncompleteName; } /// /// Gets UserName /// /// UUID of the agent /// public string GetUserName(UUID agentID) { if (agentID == UUID.Zero) return "(???) (???)"; lock (names) { if (names.ContainsKey(agentID)) { return names[agentID].UserName; } } QueueNameRequest(agentID); return RaindropInstance.IncompleteName; } /// /// Gets DisplayName /// /// UUID of the agent /// public string GetDisplayName(UUID agentID) { if (agentID == UUID.Zero) return "(???) (???)"; lock (names) { if (names.ContainsKey(agentID)) { return names[agentID].DisplayName; } } QueueNameRequest(agentID); return RaindropInstance.IncompleteName; } /// /// Get avatar display name, or queue fetching of the name /// /// UUID of avatar to lookup /// Avatar display name or "Loading..." if not in cache public string Get(UUID agentID) { if (agentID == UUID.Zero) return "(???) (???)"; lock (names) { if (names.ContainsKey(agentID)) { if (Mode != NameMode.Standard && names[agentID].NextUpdate == UUIDNameOnly) { QueueNameRequest(agentID); } return FormatName(names[agentID]); } } QueueNameRequest(agentID); return RaindropInstance.IncompleteName; } /// /// Get avatar display name, or queue fetching of the name /// /// UUID of avatar to lookup /// If true, wait until name is recieved, otherwise return immediately /// Avatar display name or "Loading..." if not in cache public string Get(UUID agentID, bool blocking) { if (!blocking) Get(agentID); string name = null; using (ManualResetEvent gotName = new ManualResetEvent(false)) { EventHandler handler = (object sender, UUIDNameReplyEventArgs e) => { if (e.Names.ContainsKey(agentID)) { name = e.Names[agentID]; gotName.Set(); } }; NameUpdated += handler; name = Get(agentID); if (name == RaindropInstance.IncompleteName) { gotName.WaitOne(20 * 1000, false); } NameUpdated -= handler; } return name; } /// /// Get avatar display name, or queue fetching of the name /// /// UUID of avatar to lookup /// If name failed to retrieve, use this /// Avatar display name or the default value if not in cache public string Get(UUID agentID, string defaultValue) { if (Mode == NameMode.Standard) return defaultValue; string name = Get(agentID); if (name == RaindropInstance.IncompleteName) return defaultValue; return name; } /// /// Get avatar display name, or queue fetching of the name /// /// UUID of avatar to lookup /// If true, wait until name is recieved, otherwise return immediately /// If name failed to retrieve, use this /// public string Get(UUID agentID, bool blocking, string defaultValue) { if (Mode == NameMode.Standard) return defaultValue; string name = Get(agentID, blocking); if (name == RaindropInstance.IncompleteName) return defaultValue; return name; } #endregion public methods } }