From 7f8cffafd2f20c100d7dc2d21ef7e5889c51a272 Mon Sep 17 00:00:00 2001 From: John Hurliman Date: Mon, 15 Dec 2008 19:13:24 +0000 Subject: [PATCH] * Moved BlockingQueue, DoubleDictionary, and ExpiringCache into OpenMetaverseTypes.dll * First attempt at an EventQueueServer implementation, untested * Implemented a capabilities server that can route capabilities to local callbacks or remote URIs * Modified HttpServer.HttpRequestCallback to return a bool: true to close the connection, false to leave it open * Removed all locks from HttpServer and added try/catch around HttpListenerContext operations * Added Color4.FromHSV() git-svn-id: http://libopenmetaverse.googlecode.com/svn/libopenmetaverse/trunk@2379 52acb1d6-8a22-11de-b505-999d5b087335 --- OpenMetaverse/AssetManager.cs | 38 -- OpenMetaverse/Capabilities/CapsServer.cs | 220 +++++++ .../Capabilities/EventQueueServer.cs | 224 ++++++- .../Capabilities/HttpRequestSignature.cs | 14 +- OpenMetaverse/Capabilities/HttpServer.cs | 86 ++- OpenMetaverse/{ => Types}/BlockingQueue.cs | 0 OpenMetaverse/Types/Color4.cs | 126 +++- .../Types}/DoubleDictionary.cs | 28 +- OpenMetaverse/Types/ExpiringCache.cs | 558 ++++++++++++++++++ Programs/Simian/Simian.cs | 18 +- 10 files changed, 1209 insertions(+), 103 deletions(-) create mode 100644 OpenMetaverse/Capabilities/CapsServer.cs rename OpenMetaverse/{ => Types}/BlockingQueue.cs (100%) rename {Programs/Simian => OpenMetaverse/Types}/DoubleDictionary.cs (64%) create mode 100644 OpenMetaverse/Types/ExpiringCache.cs diff --git a/OpenMetaverse/AssetManager.cs b/OpenMetaverse/AssetManager.cs index 01ea5321..131cf258 100644 --- a/OpenMetaverse/AssetManager.cs +++ b/OpenMetaverse/AssetManager.cs @@ -208,45 +208,7 @@ namespace OpenMetaverse } #endregion Enums - /* - public static class AssetTypeParser - { - private static readonly ReversableDictionary AssetTypeMap = new ReversableDictionary(); - static AssetTypeParser() - { - AssetTypeMap.Add("animatn", AssetType.Animation); - AssetTypeMap.Add("clothing", AssetType.Clothing); - AssetTypeMap.Add("callcard", AssetType.CallingCard); - AssetTypeMap.Add("object", AssetType.Object); - AssetTypeMap.Add("texture", AssetType.Texture); - AssetTypeMap.Add("sound", AssetType.Sound); - AssetTypeMap.Add("bodypart", AssetType.Bodypart); - AssetTypeMap.Add("gesture", AssetType.Gesture); - AssetTypeMap.Add("lsltext", AssetType.LSLText); - AssetTypeMap.Add("landmark", AssetType.Landmark); - AssetTypeMap.Add("notecard", AssetType.Notecard); - AssetTypeMap.Add("category", AssetType.Folder); - } - public static AssetType Parse(string str) - { - AssetType t; - if (AssetTypeMap.TryGetValue(str, out t)) - return t; - else - return AssetType.Unknown; - } - - public static string StringValueOf(AssetType type) - { - string str; - if (AssetTypeMap.TryGetKey(type, out str)) - return str; - else - return "unknown"; - } - } - */ #region Transfer Classes /// diff --git a/OpenMetaverse/Capabilities/CapsServer.cs b/OpenMetaverse/Capabilities/CapsServer.cs new file mode 100644 index 00000000..1dad2caf --- /dev/null +++ b/OpenMetaverse/Capabilities/CapsServer.cs @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2007-2008, openmetaverse.org + * 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. + * - Neither the name of the openmetaverse.org 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 OWNER 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. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using OpenMetaverse.StructuredData; + +namespace OpenMetaverse.Capabilities +{ + public class CapsServer + { + private struct CapsRedirector + { + public HttpServer.HttpRequestCallback LocalCallback; + public Uri RemoteResource; + + public CapsRedirector(HttpServer.HttpRequestCallback localCallback, Uri remoteResource) + { + LocalCallback = localCallback; + RemoteResource = remoteResource; + } + } + + HttpServer server; + bool serverOwned; + HttpServer.HttpRequestHandler capsHandler; + ExpiringCache expiringCaps = new ExpiringCache(); + Dictionary fixedCaps = new Dictionary(); + object syncRoot = new object(); + + public CapsServer(List listeningPrefixes) + { + serverOwned = true; + capsHandler = BuildCapsHandler("^/"); + server = new HttpServer(listeningPrefixes); + } + + public CapsServer(HttpServer httpServer, string handlerPath) + { + serverOwned = false; + capsHandler = BuildCapsHandler(handlerPath); + server = httpServer; + } + + public void Start() + { + server.AddHandler(capsHandler); + + if (serverOwned) + server.Start(); + } + + public void Stop() + { + if (serverOwned) + server.Stop(); + + server.RemoveHandler(capsHandler); + } + + public UUID CreateCapability(HttpServer.HttpRequestCallback localHandler) + { + UUID id = UUID.Random(); + CapsRedirector redirector = new CapsRedirector(localHandler, null); + + lock (syncRoot) + fixedCaps.Add(id, redirector); + + return id; + } + + public UUID CreateCapability(Uri remoteResource) + { + UUID id = UUID.Random(); + CapsRedirector redirector = new CapsRedirector(null, remoteResource); + + lock (syncRoot) + fixedCaps.Add(id, redirector); + + return id; + } + + public UUID CreateCapability(HttpServer.HttpRequestCallback localHandler, double ttlSeconds) + { + UUID id = UUID.Random(); + CapsRedirector redirector = new CapsRedirector(localHandler, null); + + lock (syncRoot) + expiringCaps.Add(id, redirector, DateTime.Now + TimeSpan.FromSeconds(ttlSeconds)); + + return id; + } + + public UUID CreateCapability(Uri remoteResource, double ttlSeconds) + { + UUID id = UUID.Random(); + CapsRedirector redirector = new CapsRedirector(null, remoteResource); + + lock (syncRoot) + expiringCaps.Add(id, redirector, DateTime.Now + TimeSpan.FromSeconds(ttlSeconds)); + + return id; + } + + public bool RemoveCapability(UUID id) + { + lock (syncRoot) + { + if (expiringCaps.Remove(id)) + return true; + else + return fixedCaps.Remove(id); + } + } + + bool CapsCallback(ref HttpListenerContext context) + { + UUID capsID; + CapsRedirector redirector; + bool success; + string uuidString = context.Request.Url.Segments[context.Request.Url.Segments.Length - 1]; + + if (UUID.TryParse(uuidString, out capsID)) + { + lock (syncRoot) + success = (expiringCaps.TryGetValue(capsID, out redirector) || fixedCaps.TryGetValue(capsID, out redirector)); + + if (success) + { + if (redirector.RemoteResource != null) + ProxyCapCallback(ref context, redirector.RemoteResource); + else + redirector.LocalCallback(ref context); + + return true; + } + } + + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + return true; + } + + void ProxyCapCallback(ref HttpListenerContext context, Uri remoteResource) + { + const int BUFFER_SIZE = 2048; + int numBytes; + byte[] buffer = new byte[BUFFER_SIZE]; + + // Proxy the request + HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(remoteResource); + + request.Method = context.Request.HttpMethod; + request.Headers.Add(context.Request.Headers); + + if (context.Request.HasEntityBody) + { + // Copy the request stream + using (Stream writeStream = request.GetRequestStream()) + { + while ((numBytes = context.Request.InputStream.Read(buffer, 0, BUFFER_SIZE)) > 0) + writeStream.Write(buffer, 0, numBytes); + + context.Request.InputStream.Close(); + } + } + + System.Security.Cryptography.X509Certificates.X509Certificate2 cert = context.Request.GetClientCertificate(); + ; + + // Proxy the response + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + + context.Response.StatusCode = (int)response.StatusCode; + context.Response.StatusDescription = response.StatusDescription; + context.Response.Headers = response.Headers; + + // Copy the response stream + using (Stream readStream = response.GetResponseStream()) + { + while ((numBytes = readStream.Read(buffer, 0, BUFFER_SIZE)) > 0) + context.Response.OutputStream.Write(buffer, 0, numBytes); + + context.Response.OutputStream.Close(); + } + } + + HttpServer.HttpRequestHandler BuildCapsHandler(string path) + { + HttpRequestSignature signature = new HttpRequestSignature(); + signature.ContentType = "application/xml"; + signature.Path = path; + return new HttpServer.HttpRequestHandler(signature, CapsCallback); + } + } +} diff --git a/OpenMetaverse/Capabilities/EventQueueServer.cs b/OpenMetaverse/Capabilities/EventQueueServer.cs index 6b4f9f35..9ee6c430 100644 --- a/OpenMetaverse/Capabilities/EventQueueServer.cs +++ b/OpenMetaverse/Capabilities/EventQueueServer.cs @@ -25,25 +25,231 @@ */ using System; +using System.Collections.Generic; using System.Net; +using System.IO; +using System.Threading; +using System.Xml; +using OpenMetaverse.StructuredData; namespace OpenMetaverse.Capabilities { + public class EventQueueEvent + { + public string Name; + public OSDMap Body; + + public EventQueueEvent(string name, OSDMap body) + { + Name = name; + Body = body; + } + } + public class EventQueueServer { - public EventQueueServer(HttpServer server, string path) + /// The number of milliseconds to wait before the connection times out + /// and an empty response is sent to the client. This value should be higher + /// than BATCH_WAIT_INTERVAL for the timeout to function properly + const int CONNECTION_TIMEOUT = 120000; + + /// This interval defines the amount of time to wait, in milliseconds, + /// for new events to show up on the queue before sending a response to the + /// client and completing the HTTP request. The interval also specifies the + /// maximum time that can pass before the queue shuts down after Stop() or the + /// class destructor is called + const int BATCH_WAIT_INTERVAL = 100; + + /// Since multiple events can be batched together and sent in the same + /// response, this prevents the event queue thread from infinitely dequeueing + /// events and never sending a response if there is a constant stream of new + /// events + const int MAX_EVENTS_PER_RESPONSE = 5; + + HttpServer server; + HttpServer.HttpRequestHandler handler; + BlockingQueue eventQueue = new BlockingQueue(); + int currentID = 0; + bool running = true; + + public EventQueueServer(HttpServer server, HttpServer.HttpRequestHandler handler) { - HttpRequestSignature signature = new HttpRequestSignature(); - signature.Method = "post"; - signature.ContentType = String.Empty; - signature.Path = path; - HttpServer.HttpRequestCallback callback = new HttpServer.HttpRequestCallback(EventQueueHandler); - HttpServer.HttpRequestHandler handler = new HttpServer.HttpRequestHandler(signature, callback); - server.AddHandler(handler); + this.server = server; + this.handler = handler; } - protected void EventQueueHandler(HttpRequestSignature signature, ref HttpListenerContext context) + ~EventQueueServer() { + Stop(); + } + + public void Stop() + { + running = false; + try { server.RemoveHandler(handler); } + catch (Exception) { } + } + + public void SendEvent(string eventName, OSDMap body) + { + SendEvent(new EventQueueEvent(eventName, body)); + } + + public void SendEvent(EventQueueEvent eventQueueEvent) + { + eventQueue.Enqueue(eventQueueEvent); + } + + public void SendEvents(IList events) + { + for (int i = 0; i < events.Count; i++) + eventQueue.Enqueue(events[i]); + } + + public bool EventQueueHandler(ref HttpListenerContext context) + { + // Decode the request + OSD request = null; + + try { request = OSDParser.DeserializeLLSDXml(new XmlTextReader(context.Request.InputStream)); } + catch (Exception) { } + + if (request != null && request.Type == OSDType.Map) + { + OSDMap requestMap = (OSDMap)request; + int ack = requestMap["ack"].AsInteger(); + bool done = requestMap["done"].AsBoolean(); + + if (ack != currentID - 1) + { + Logger.Log(String.Format("[EventQueue] Received an ack for id {0}, last id sent was {1}", + ack, currentID - 1), Helpers.LogLevel.Warning); + } + + if (!done) + { + StartEventQueueThread(context); + + // Tell HttpServer to leave the connection open + return false; + } + else + { + Logger.Log(String.Format("[EventQueue] Shutting down the event queue {0} at the client's request", + context.Request.Url), Helpers.LogLevel.Info); + Stop(); + + context.Response.KeepAlive = false; + return true; + } + } + else + { + Logger.Log(String.Format("[EventQueue] Received a request with invalid or missing LLSD at {0}, closing the connection", + context.Request.Url), Helpers.LogLevel.Warning); + + context.Response.KeepAlive = false; + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + return true; + } + } + + void StartEventQueueThread(HttpListenerContext httpContext) + { + // Spawn a new thread to hold the connection open and return from our precious IOCP thread + Thread thread = new Thread(new ThreadStart( + delegate() + { + EventQueueEvent eventQueueEvent = null; + int totalMsPassed = 0; + + while (running) + { + if (eventQueue.Dequeue(BATCH_WAIT_INTERVAL, ref eventQueueEvent)) + { + // An event was dequeued + totalMsPassed = 0; + + List eventsToSend = new List(); + eventsToSend.Add(eventQueueEvent); + + DateTime start = DateTime.Now; + int batchMsPassed = 0; + + // Wait BATCH_WAIT_INTERVAL milliseconds looking for more events, + // or until the size of the current batch equals MAX_EVENTS_PER_RESPONSE + while (batchMsPassed < BATCH_WAIT_INTERVAL && eventsToSend.Count < MAX_EVENTS_PER_RESPONSE) + { + if (eventQueue.Dequeue(BATCH_WAIT_INTERVAL - batchMsPassed, ref eventQueueEvent)) + eventsToSend.Add(eventQueueEvent); + + batchMsPassed = (int)(DateTime.Now - start).TotalMilliseconds; + } + + SendResponse(httpContext, eventsToSend); + return; + } + else + { + // BATCH_WAIT_INTERVAL milliseconds passed with no event. Check if the connection + // has timed out yet. + totalMsPassed += BATCH_WAIT_INTERVAL; + + if (totalMsPassed >= CONNECTION_TIMEOUT) + { + Logger.DebugLog(String.Format( + "[EventQueue] {0}ms passed without an event, timing out the event queue", + totalMsPassed)); + SendResponse(httpContext, null); + return; + } + } + } + } + )); + + thread.Start(); + } + + void SendResponse(HttpListenerContext httpContext, List eventsToSend) + { + if (eventsToSend != null) + { + OSDArray responseArray = new OSDArray(eventsToSend.Count); + + // Put all of the events in an array + for (int i = 0; i < eventsToSend.Count; i++) + { + EventQueueEvent currentEvent = eventsToSend[i]; + + OSDMap eventMap = new OSDMap(2); + eventMap.Add("body", currentEvent.Body); + eventMap.Add("message", OSD.FromString(currentEvent.Name)); + responseArray.Add(eventMap); + } + + // Create a map containing the events array and the id of this response + OSDMap responseMap = new OSDMap(2); + responseMap.Add("events", responseArray); + responseMap.Add("id", OSD.FromInteger(currentID++)); + + // Serialize the events and send the response + byte[] buffer = OSDParser.SerializeLLSDXmlBytes(responseMap); + httpContext.Response.KeepAlive = true; + httpContext.Response.ContentType = "application/xml"; + httpContext.Response.ContentLength64 = buffer.Length; + httpContext.Response.OutputStream.Write(buffer, 0, buffer.Length); + httpContext.Response.OutputStream.Close(); + httpContext.Response.Close(); + } + else + { + // The 502 response started as a bug in the LL event queue server implementation, + // but is now hardcoded into the protocol as the code to use for a timeout + httpContext.Response.StatusCode = (int)HttpStatusCode.BadGateway; + httpContext.Response.KeepAlive = true; + httpContext.Response.Close(); + } } } } diff --git a/OpenMetaverse/Capabilities/HttpRequestSignature.cs b/OpenMetaverse/Capabilities/HttpRequestSignature.cs index f96a44b6..b698e7c6 100644 --- a/OpenMetaverse/Capabilities/HttpRequestSignature.cs +++ b/OpenMetaverse/Capabilities/HttpRequestSignature.cs @@ -41,6 +41,7 @@ namespace OpenMetaverse.Capabilities private string contentType; private string path; + /// HTTP method public string Method { get { return method; } @@ -50,6 +51,7 @@ namespace OpenMetaverse.Capabilities else method = String.Empty; } } + /// HTTP Content-Type public string ContentType { get { return contentType; } @@ -59,16 +61,21 @@ namespace OpenMetaverse.Capabilities else contentType = String.Empty; } } + /// Relative URL path public string Path { get { return path; } set { - if (!String.IsNullOrEmpty(value)) path = value.ToLower(); + if (!String.IsNullOrEmpty(value)) path = value; else path = String.Empty; } } + /// + /// Constructor + /// + /// HTTP request to build a signature for public HttpRequestSignature(HttpListenerContext context) { method = contentType = path = String.Empty; @@ -123,6 +130,11 @@ namespace OpenMetaverse.Capabilities return hash; } + public override string ToString() + { + return String.Format("{0} {1} Content-Type: {2}", method, path, contentType); + } + /// /// Does pattern matching to determine if an incoming HTTP request /// matches a given pattern. The incoming request must be on the diff --git a/OpenMetaverse/Capabilities/HttpServer.cs b/OpenMetaverse/Capabilities/HttpServer.cs index 68416c52..f6959a71 100644 --- a/OpenMetaverse/Capabilities/HttpServer.cs +++ b/OpenMetaverse/Capabilities/HttpServer.cs @@ -32,7 +32,7 @@ namespace OpenMetaverse.Capabilities { public class HttpServer { - public delegate void HttpRequestCallback(HttpRequestSignature signature, ref HttpListenerContext context); + #region HttpRequestHandler struct public struct HttpRequestHandler : IEquatable { @@ -55,34 +55,41 @@ namespace OpenMetaverse.Capabilities return Signature.GetHashCode(); } + public override string ToString() + { + return Signature.ToString(); + } + public bool Equals(HttpRequestHandler handler) { return this.Signature == handler.Signature; } } - HttpListener server; - //int serverPort; - //bool sslEnabled; - // TODO: Replace this with an immutable list to avoid locking - List requestHandlers; + #endregion HttpRequestHandler struct - bool isRunning; + public delegate bool HttpRequestCallback(ref HttpListenerContext context); + + HttpListener server = null; + HttpRequestHandler[] requestHandlers = new HttpRequestHandler[0]; + bool isRunning = false; public HttpServer(int port, bool ssl) { - //serverPort = port; - //sslEnabled = ssl; server = new HttpListener(); if (ssl) server.Prefixes.Add(String.Format("https://+:{0}/", port)); else server.Prefixes.Add(String.Format("http://+:{0}/", port)); + } - requestHandlers = new List(); + public HttpServer(List prefixes) + { + server = new HttpListener(); - isRunning = false; + for (int i = 0; i < prefixes.Count; i++) + server.Prefixes.Add(prefixes[i]); } public void AddHandler(string method, string contentType, string path, HttpRequestCallback callback) @@ -96,12 +103,27 @@ namespace OpenMetaverse.Capabilities public void AddHandler(HttpRequestHandler handler) { - lock (requestHandlers) requestHandlers.Add(handler); + HttpRequestHandler[] newHandlers = new HttpRequestHandler[requestHandlers.Length + 1]; + + for (int i = 0; i < requestHandlers.Length; i++) + newHandlers[i] = requestHandlers[i]; + newHandlers[requestHandlers.Length] = handler; + + // CLR guarantees this is an atomic operation + requestHandlers = newHandlers; } public void RemoveHandler(HttpRequestHandler handler) { - lock (requestHandlers) requestHandlers.Remove(handler); + HttpRequestHandler[] newHandlers = new HttpRequestHandler[requestHandlers.Length - 1]; + + int j = 0; + for (int i = 0; i < requestHandlers.Length; i++) + if (!requestHandlers[i].Signature.ExactlyEquals(handler.Signature)) + newHandlers[j++] = handler; + + // CLR guarantees this is an atomic operation + requestHandlers = newHandlers; } public void Start() @@ -145,27 +167,39 @@ namespace OpenMetaverse.Capabilities HttpRequestSignature signature = new HttpRequestSignature(context); // Look for a signature match in our handlers - lock (requestHandlers) + for (int i = 0; i < requestHandlers.Length; i++) { - for (int i = 0; i < requestHandlers.Count; i++) - { - HttpRequestHandler handler = requestHandlers[i]; + HttpRequestHandler handler = requestHandlers[i]; - if (signature == handler.Signature) + if (handler.Signature != null && signature == handler.Signature) + { + bool closeConnection = true; + + // Request signature matched, handle it + try { closeConnection = handler.Callback(ref context); } + catch (Exception ex) { Logger.Log("Exception in HTTP handler: " + ex.Message, Helpers.LogLevel.Error, ex); } + + if (closeConnection) { - // Request signature matched, handle it - handler.Callback(signature, ref context); - return; + // Close the connection + try { context.Response.Close(); } + catch (Exception) { } } + + return; } } // No registered handler matched this request's signature. Send a 404 - context.Response.StatusCode = (int)HttpStatusCode.NotFound; - context.Response.StatusDescription = String.Format( - "No request handler registered for Method=\"{0}\", Content-Type=\"{1}\", Path=\"{2}\"", - signature.Method, signature.ContentType, signature.Path); - context.Response.Close(); + try + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + context.Response.StatusDescription = String.Format( + "No request handler registered for Method=\"{0}\", Content-Type=\"{1}\", Path=\"{2}\"", + signature.Method, signature.ContentType, signature.Path); + context.Response.Close(); + } + catch (Exception) { } } } } diff --git a/OpenMetaverse/BlockingQueue.cs b/OpenMetaverse/Types/BlockingQueue.cs similarity index 100% rename from OpenMetaverse/BlockingQueue.cs rename to OpenMetaverse/Types/BlockingQueue.cs diff --git a/OpenMetaverse/Types/Color4.cs b/OpenMetaverse/Types/Color4.cs index 5ecc2d8b..f9271f70 100644 --- a/OpenMetaverse/Types/Color4.cs +++ b/OpenMetaverse/Types/Color4.cs @@ -259,26 +259,23 @@ namespace OpenMetaverse // Achromatic, hue is undefined return -1f; } - else + else if (R == max) { - if (R == max) - { - float bDelta = (((max - B) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); - float gDelta = (((max - G) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); - return bDelta - gDelta; - } - else if (G == max) - { - float rDelta = (((max - R) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); - float bDelta = (((max - B) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); - return (HUE_MAX / 3f) + rDelta - bDelta; - } - else // B == max - { - float gDelta = (((max - G) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); - float rDelta = (((max - R) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); - return ((2f * HUE_MAX) / 3f) + gDelta - rDelta; - } + float bDelta = (((max - B) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); + float gDelta = (((max - G) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); + return bDelta - gDelta; + } + else if (G == max) + { + float rDelta = (((max - R) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); + float bDelta = (((max - B) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); + return (HUE_MAX / 3f) + rDelta - bDelta; + } + else // B == max + { + float gDelta = (((max - G) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); + float rDelta = (((max - R) * (HUE_MAX / 6f)) + ((max - min) / 2f)) / (max - min); + return ((2f * HUE_MAX) / 3f) + gDelta - rDelta; } } @@ -286,6 +283,93 @@ namespace OpenMetaverse #region Static Methods + /// + /// Create an RGB color from a hue, saturation, value combination + /// + /// Hue + /// Saturation + /// Value + /// An fully opaque RGB color (alpha is 1.0) + public static Color4 FromHSV(double hue, double saturation, double value) + { + double r = 0d; + double g = 0d; + double b = 0d; + + if (saturation == 0d) + { + // If s is 0, all colors are the same. + // This is some flavor of gray. + r = value; + g = value; + b = value; + } + else + { + double p; + double q; + double t; + + double fractionalSector; + int sectorNumber; + double sectorPos; + + // The color wheel consists of 6 sectors. + // Figure out which sector you//re in. + sectorPos = hue / 60d; + sectorNumber = (int)(Math.Floor(sectorPos)); + + // get the fractional part of the sector. + // That is, how many degrees into the sector + // are you? + fractionalSector = sectorPos - sectorNumber; + + // Calculate values for the three axes + // of the color. + p = value * (1d - saturation); + q = value * (1d - (saturation * fractionalSector)); + t = value * (1d - (saturation * (1d - fractionalSector))); + + // Assign the fractional colors to r, g, and b + // based on the sector the angle is in. + switch (sectorNumber) + { + case 0: + r = value; + g = t; + b = p; + break; + case 1: + r = q; + g = value; + b = p; + break; + case 2: + r = p; + g = value; + b = t; + break; + case 3: + r = p; + g = q; + b = value; + break; + case 4: + r = t; + g = p; + b = value; + break; + case 5: + r = value; + g = p; + b = q; + break; + } + } + + return new Color4((float)r, (float)g, (float)b, 1f); + } + #endregion Static Methods #region Overrides @@ -331,10 +415,10 @@ namespace OpenMetaverse #endregion Operators - /// A Color4 with zero RGB values and full alpha (1.0) + /// A Color4 with zero RGB values and fully opaque (alpha 1.0) public readonly static Color4 Black = new Color4(0f, 0f, 0f, 1f); - /// A Color4 with full RGB values (1.0) and full alpha (1.0) + /// A Color4 with full RGB values (1.0) and fully opaque (alpha 1.0) public readonly static Color4 White = new Color4(1f, 1f, 1f, 1f); } } diff --git a/Programs/Simian/DoubleDictionary.cs b/OpenMetaverse/Types/DoubleDictionary.cs similarity index 64% rename from Programs/Simian/DoubleDictionary.cs rename to OpenMetaverse/Types/DoubleDictionary.cs index afc21636..dcae0d59 100644 --- a/Programs/Simian/DoubleDictionary.cs +++ b/OpenMetaverse/Types/DoubleDictionary.cs @@ -1,7 +1,33 @@ +/* + * Copyright (c) 2008, openmetaverse.org + * 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. + * - Neither the name of the openmetaverse.org 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 OWNER 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. + */ + using System; using System.Collections.Generic; -namespace Simian +namespace OpenMetaverse { public class DoubleDictionary { diff --git a/OpenMetaverse/Types/ExpiringCache.cs b/OpenMetaverse/Types/ExpiringCache.cs new file mode 100644 index 00000000..bd69799d --- /dev/null +++ b/OpenMetaverse/Types/ExpiringCache.cs @@ -0,0 +1,558 @@ +/* + * Copyright (c) 2008, openmetaverse.org + * 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. + * - Neither the name of the openmetaverse.org 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 OWNER 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. + */ + +using System; +using System.Threading; +using System.Collections.Generic; + +namespace OpenMetaverse +{ + #region TimedCacheKey Class + + class TimedCacheKey : IComparable + { + private DateTime expirationDate; + private bool slidingExpiration; + private TimeSpan slidingExpirationWindowSize; + private TKey key; + + public DateTime ExpirationDate { get { return expirationDate; } } + public TKey Key { get { return key; } } + public bool SlidingExpiration { get { return slidingExpiration; } } + public TimeSpan SlidingExpirationWindowSize { get { return slidingExpirationWindowSize; } } + + public TimedCacheKey(TKey key, DateTime expirationDate) + { + this.key = key; + this.slidingExpiration = false; + this.expirationDate = expirationDate; + } + + public TimedCacheKey(TKey key, TimeSpan slidingExpirationWindowSize) + { + this.key = key; + this.slidingExpiration = true; + this.slidingExpirationWindowSize = slidingExpirationWindowSize; + Accessed(); + } + + public void Accessed() + { + if (slidingExpiration) + expirationDate = DateTime.Now.Add(slidingExpirationWindowSize); + } + + public int CompareTo(TKey other) + { + return key.GetHashCode().CompareTo(other.GetHashCode()); + } + } + + #endregion + + public sealed class ExpiringCache + { + #region Private fields + + /// For thread safety + ReaderWriterLock readWriteLock = new ReaderWriterLock(); + const double CACHE_PURGE_HZ = 1.0; + const int MAX_LOCK_WAIT = 5000; // milliseconds + + Dictionary, TValue> timedStorage = new Dictionary, TValue>(); + Dictionary> timedStorageIndex = new Dictionary>(); + private System.Timers.Timer timer = new System.Timers.Timer(TimeSpan.FromSeconds(CACHE_PURGE_HZ).TotalMilliseconds); + object isPurging = new object(); + + #endregion + + #region Constructor + + public ExpiringCache() + { + timer.Elapsed += PurgeCache; + timer.Start(); + } + + #endregion + + #region Public methods + + public bool Add(TKey key, TValue value, DateTime expiration) + { + // Synchronise access to storage structures. A read lock may + // already be acquired before this method is called. + bool LockUpgraded = readWriteLock.IsReaderLockHeld; + LockCookie lc = new LockCookie(); + if (LockUpgraded) + { + lc = readWriteLock.UpgradeToWriterLock(MAX_LOCK_WAIT); + } + else + { + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + } + try + { + // This is the actual adding of the key + if (timedStorageIndex.ContainsKey(key)) + { + return false; + } + else + { + TimedCacheKey internalKey = new TimedCacheKey(key, expiration); + timedStorage.Add(internalKey, value); + timedStorageIndex.Add(key, internalKey); + return true; + } + } + finally + { + // Restore lock state + if (LockUpgraded) + { + readWriteLock.DowngradeFromWriterLock(ref lc); + } + else + { + readWriteLock.ReleaseWriterLock(); + } + } + } + + public bool Add(TKey key, TValue value, TimeSpan slidingExpiration) + { + // Synchronise access to storage structures. A read lock may + // already be acquired before this method is called. + bool LockUpgraded = readWriteLock.IsReaderLockHeld; + LockCookie lc = new LockCookie(); + if (LockUpgraded) + { + lc = readWriteLock.UpgradeToWriterLock(MAX_LOCK_WAIT); + } + else + { + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + } + try + { + // This is the actual adding of the key + if (timedStorageIndex.ContainsKey(key)) + { + return false; + } + else + { + TimedCacheKey internalKey = new TimedCacheKey(key, slidingExpiration); + timedStorage.Add(internalKey, value); + timedStorageIndex.Add(key, internalKey); + return true; + } + } + finally + { + // Restore lock state + if (LockUpgraded) + { + readWriteLock.DowngradeFromWriterLock(ref lc); + } + else + { + readWriteLock.ReleaseWriterLock(); + } + } + } + + public bool AddOrUpdate(TKey key, TValue value, DateTime expiration) + { + readWriteLock.AcquireReaderLock(MAX_LOCK_WAIT); + try + { + if (Contains(key)) + { + Update(key, value, expiration); + return false; + } + else + { + Add(key, value, expiration); + return true; + } + } + finally { readWriteLock.ReleaseReaderLock(); } + } + + public bool AddOrUpdate(TKey key, TValue value, TimeSpan slidingExpiration) + { + readWriteLock.AcquireReaderLock(MAX_LOCK_WAIT); + try + { + if (Contains(key)) + { + Update(key, value, slidingExpiration); + return false; + } + else + { + Add(key, value, slidingExpiration); + return true; + } + } + finally { readWriteLock.ReleaseReaderLock(); } + } + + public void Clear() + { + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + try + { + timedStorage.Clear(); + timedStorageIndex.Clear(); + } + finally { readWriteLock.ReleaseWriterLock(); } + } + + public bool Contains(TKey key) + { + readWriteLock.AcquireReaderLock(MAX_LOCK_WAIT); + try + { + return timedStorageIndex.ContainsKey(key); + } + finally { readWriteLock.ReleaseReaderLock(); } + } + + public int Count + { + get + { + return timedStorage.Count; + } + } + + public object this[TKey key] + { + get + { + TValue o; + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + try + { + if (timedStorageIndex.ContainsKey(key)) + { + TimedCacheKey tkey = timedStorageIndex[key]; + o = timedStorage[tkey]; + timedStorage.Remove(tkey); + tkey.Accessed(); + timedStorage.Add(tkey, o); + return o; + } + else + { + throw new ArgumentException("Key not found in the cache"); + } + } + finally { readWriteLock.ReleaseWriterLock(); } + } + } + + public TValue this[TKey key, DateTime expiration] + { + set + { + AddOrUpdate(key, value, expiration); + } + } + + public TValue this[TKey key, TimeSpan slidingExpiration] + { + set + { + AddOrUpdate(key, value, slidingExpiration); + } + } + + public bool Remove(TKey key) + { + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + try + { + if (timedStorageIndex.ContainsKey(key)) + { + timedStorage.Remove(timedStorageIndex[key]); + timedStorageIndex.Remove(key); + return true; + } + else + { + return false; + } + } + finally { readWriteLock.ReleaseWriterLock(); } + } + + public bool TryGetValue(TKey key, out TValue value) + { + TValue o; + + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + try + { + if (timedStorageIndex.ContainsKey(key)) + { + TimedCacheKey tkey = timedStorageIndex[key]; + o = timedStorage[tkey]; + timedStorage.Remove(tkey); + tkey.Accessed(); + timedStorage.Add(tkey, o); + value = o; + return true; + } + else + { + value = default(TValue); + return false; + } + } + finally { readWriteLock.ReleaseReaderLock(); } + } + + /// + /// Enumerates over all of the stored values without updating access times + /// + /// Action to perform on all of the elements + public void ForEach(Action action) + { + readWriteLock.AcquireReaderLock(MAX_LOCK_WAIT); + try + { + foreach (TValue value in timedStorage.Values) + action(value); + } + finally { readWriteLock.ReleaseReaderLock(); } + } + + public bool Update(TKey key, TValue value) + { + // Synchronise access to storage structures. A read lock may + // already be acquired before this method is called. + LockCookie lc = new LockCookie(); + bool lockUpgrade = readWriteLock.IsReaderLockHeld; + + if (lockUpgrade) + lc = readWriteLock.UpgradeToWriterLock(MAX_LOCK_WAIT); + else + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + + try + { + if (timedStorageIndex.ContainsKey(key)) + { + timedStorage.Remove(timedStorageIndex[key]); + timedStorageIndex[key].Accessed(); + timedStorage.Add(timedStorageIndex[key], value); + return true; + } + else + { + return false; + } + } + finally + { + // Restore lock state + if (lockUpgrade) + readWriteLock.DowngradeFromWriterLock(ref lc); + else + readWriteLock.ReleaseWriterLock(); + } + } + + public bool Update(TKey key, TValue value, DateTime expiration) + { + // Synchronise access to storage structures. A read lock may + // already be acquired before this method is called. + LockCookie lc = new LockCookie(); + bool lockUpgrade = readWriteLock.IsReaderLockHeld; + + if (lockUpgrade) + lc = readWriteLock.UpgradeToWriterLock(MAX_LOCK_WAIT); + else + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + + try + { + if (timedStorageIndex.ContainsKey(key)) + { + timedStorage.Remove(timedStorageIndex[key]); + timedStorageIndex.Remove(key); + } + else + { + return false; + } + + TimedCacheKey internalKey = new TimedCacheKey(key, expiration); + timedStorage.Add(internalKey, value); + timedStorageIndex.Add(key, internalKey); + return true; + } + finally + { + // Restore lock state + if (lockUpgrade) + readWriteLock.DowngradeFromWriterLock(ref lc); + else + readWriteLock.ReleaseWriterLock(); + } + } + + public bool Update(TKey key, TValue value, TimeSpan slidingExpiration) + { + // Synchronise access to storage structures. A read lock may + // already be acquired before this method is called. + LockCookie lc = new LockCookie(); + bool lockUpgrade = readWriteLock.IsReaderLockHeld; + + if (lockUpgrade) + lc = readWriteLock.UpgradeToWriterLock(MAX_LOCK_WAIT); + else + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + + try + { + if (timedStorageIndex.ContainsKey(key)) + { + timedStorage.Remove(timedStorageIndex[key]); + timedStorageIndex.Remove(key); + } + else + { + return false; + } + + TimedCacheKey internalKey = new TimedCacheKey(key, slidingExpiration); + timedStorage.Add(internalKey, value); + timedStorageIndex.Add(key, internalKey); + return true; + } + finally + { + // Restore lock state + if (lockUpgrade) + readWriteLock.DowngradeFromWriterLock(ref lc); + else + readWriteLock.ReleaseWriterLock(); + } + } + + public void CopyTo(Array array, int startIndex) + { + // Error checking + if (array == null) { throw new ArgumentNullException("array"); } + + if (startIndex < 0) { throw new ArgumentOutOfRangeException("startIndex", "startIndex must be >= 0."); } + + if (array.Rank > 1) { throw new ArgumentException("array must be of Rank 1 (one-dimensional)", "array"); } + if (startIndex >= array.Length) { throw new ArgumentException("startIndex must be less than the length of the array.", "startIndex"); } + if (Count > array.Length - startIndex) { throw new ArgumentException("There is not enough space from startIndex to the end of the array to accomodate all items in the cache."); } + + // Copy the data to the array (in a thread-safe manner) + readWriteLock.AcquireReaderLock(MAX_LOCK_WAIT); + try + { + foreach (object o in timedStorage) + { + array.SetValue(o, startIndex); + startIndex++; + } + } + finally { readWriteLock.ReleaseReaderLock(); } + } + + #endregion + + #region Private methods + + /// + /// Purges expired objects from the cache. Called automatically by the purge timer. + /// + private void PurgeCache(object sender, System.Timers.ElapsedEventArgs e) + { + // Note: This implementation runs with low priority. If the cache lock + // is heavily contended (many threads) the purge will take a long time + // to obtain the lock it needs and may never be run. + Thread.CurrentThread.Priority = ThreadPriority.BelowNormal; + + // Only let one thread purge at once - a buildup could cause a crash + // This could cause the purge to be delayed while there are lots of read/write ops + // happening on the cache + if (!Monitor.TryEnter(isPurging)) + return; + + try + { + readWriteLock.AcquireWriterLock(MAX_LOCK_WAIT); + try + { + List expiredItems = new List(); + + foreach (TimedCacheKey timedKey in timedStorage.Keys) + { + if (timedKey.ExpirationDate < e.SignalTime) + { + // Mark the object for purge + expiredItems.Add(timedKey.Key); + } + else + { + break; + } + } + + foreach (TKey key in expiredItems) + { + TimedCacheKey timedKey = timedStorageIndex[key]; + timedStorageIndex.Remove(timedKey.Key); + timedStorage.Remove(timedKey); + } + } + catch (ApplicationException) + { + // Unable to obtain write lock to the timed cache storage object + } + finally + { + readWriteLock.ReleaseWriterLock(); + } + } + finally { Monitor.Exit(isPurging); } + } + + #endregion + } +} diff --git a/Programs/Simian/Simian.cs b/Programs/Simian/Simian.cs index deac082f..f9ec17f6 100644 --- a/Programs/Simian/Simian.cs +++ b/Programs/Simian/Simian.cs @@ -149,22 +149,23 @@ namespace Simian HttpServer.Start(); } - void LoginWebpageHeadHandler(HttpRequestSignature signature, ref HttpListenerContext context) + bool LoginWebpageHeadHandler(ref HttpListenerContext context) { context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.StatusDescription = "OK"; - context.Response.Close(); + return true; } - void LoginWebpageGetHandler(HttpRequestSignature signature, ref HttpListenerContext context) + bool LoginWebpageGetHandler(ref HttpListenerContext context) { string pageContent = "Simian

Welcome to Simian

"; byte[] pageData = Encoding.UTF8.GetBytes(pageContent); context.Response.OutputStream.Write(pageData, 0, pageData.Length); - context.Response.Close(); + context.Response.OutputStream.Close(); + return true; } - void LoginXmlRpcPostHandler(HttpRequestSignature signature, ref HttpListenerContext context) + bool LoginXmlRpcPostHandler(ref HttpListenerContext context) { string firstName = String.Empty, @@ -259,9 +260,11 @@ namespace Simian { Logger.Log("XmlRpc login error: " + ex.Message, Helpers.LogLevel.Error, ex); } + + return true; } - void LoginLLSDPostHandler(HttpRequestSignature signature, ref HttpListenerContext context) + bool LoginLLSDPostHandler(ref HttpListenerContext context) { string body = String.Empty; @@ -270,7 +273,8 @@ namespace Simian body = reader.ReadToEnd(); } - Console.WriteLine(body); + Logger.DebugLog("LLSD login is not implemented:\n" + body); + return true; } LoginResponseData HandleLogin(string firstName, string lastName, string password, string start, string version, string channel)