From 824283ca3c2ab54868ed61fdb0a329221d69e5fa Mon Sep 17 00:00:00 2001 From: Melanie Thielker Date: Fri, 26 Sep 2008 13:16:11 +0000 Subject: Remove all the subclassing complexity and script server interfaces from DNE and move all of DNE into the DotNetEngine directory. Remove references that would cause the script runtime to load the entire engine + scene into each script appdomain. This might help DNE memory consumption. --- .../ScriptEngine/DotNetEngine/AppDomainManager.cs | 254 ++++++++++++ OpenSim/Region/ScriptEngine/DotNetEngine/Common.cs | 56 +++ .../DotNetEngine/Compiler/LSL/Compiler.cs | 4 +- .../ScriptEngine/DotNetEngine/EventQueueManager.cs | 447 ++++++++++++++++++++ .../DotNetEngine/EventQueueThreadClass.cs | 381 +++++++++++++++++ .../ScriptEngine/DotNetEngine/MaintenanceThread.cs | 243 +++++++++++ .../ScriptEngine/DotNetEngine/ScriptEngine.cs | 268 +++++++++++- .../ScriptEngine/DotNetEngine/ScriptManager.cs | 460 ++++++++++++++++++++- 8 files changed, 2095 insertions(+), 18 deletions(-) create mode 100644 OpenSim/Region/ScriptEngine/DotNetEngine/AppDomainManager.cs create mode 100644 OpenSim/Region/ScriptEngine/DotNetEngine/Common.cs create mode 100644 OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueManager.cs create mode 100644 OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueThreadClass.cs create mode 100644 OpenSim/Region/ScriptEngine/DotNetEngine/MaintenanceThread.cs (limited to 'OpenSim/Region/ScriptEngine/DotNetEngine') diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/AppDomainManager.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/AppDomainManager.cs new file mode 100644 index 0000000..969a05e --- /dev/null +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/AppDomainManager.cs @@ -0,0 +1,254 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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 OpenSim Project 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 DEVELOPERS ``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 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; +using System.Collections.Generic; +using System.Reflection; +using OpenSim.Region.ScriptEngine.Common; + +namespace OpenSim.Region.ScriptEngine.DotNetEngine +{ + public class AppDomainManager : iScriptEngineFunctionModule + { + // + // This class does AppDomain handling and loading/unloading of scripts in it. + // It is instanced in "ScriptEngine" and controlled from "ScriptManager" + // + // 1. Create a new AppDomain if old one is full (or doesn't exist) + // 2. Load scripts into AppDomain + // 3. Unload scripts from AppDomain (stopping them and marking them as inactive) + // 4. Unload AppDomain completely when all scripts in it has stopped + // + + private int maxScriptsPerAppDomain = 1; + + /// + /// Internal list of all AppDomains + /// + private List appDomains = new List(); + + /// + /// Structure to keep track of data around AppDomain + /// + private class AppDomainStructure + { + /// + /// The AppDomain itself + /// + public AppDomain CurrentAppDomain; + + /// + /// Number of scripts loaded into AppDomain + /// + public int ScriptsLoaded; + + /// + /// Number of dead scripts + /// + public int ScriptsWaitingUnload; + } + + /// + /// Current AppDomain + /// + private AppDomainStructure currentAD; + + private object getLock = new object(); // Mutex + private object freeLock = new object(); // Mutex + + private ScriptEngine m_scriptEngine; + //public AppDomainManager(ScriptEngine scriptEngine) + public AppDomainManager(ScriptEngine scriptEngine) + { + m_scriptEngine = scriptEngine; + ReadConfig(); + } + + public void ReadConfig() + { + maxScriptsPerAppDomain = m_scriptEngine.ScriptConfigSource.GetInt("ScriptsPerAppDomain", 1); + } + + /// + /// Find a free AppDomain, creating one if necessary + /// + /// Free AppDomain + private AppDomainStructure GetFreeAppDomain() + { + // Console.WriteLine("Finding free AppDomain"); + lock (getLock) + { + // Current full? + if (currentAD != null && currentAD.ScriptsLoaded >= maxScriptsPerAppDomain) + { + // Add it to AppDomains list and empty current + appDomains.Add(currentAD); + currentAD = null; + } + // No current + if (currentAD == null) + { + // Create a new current AppDomain + currentAD = new AppDomainStructure(); + currentAD.CurrentAppDomain = PrepareNewAppDomain(); + } + + // Console.WriteLine("Scripts loaded in this Appdomain: " + currentAD.ScriptsLoaded); + return currentAD; + } + } + + private int AppDomainNameCount; + + /// + /// Create and prepare a new AppDomain for scripts + /// + /// The new AppDomain + private AppDomain PrepareNewAppDomain() + { + // Create and prepare a new AppDomain + AppDomainNameCount++; + // TODO: Currently security match current appdomain + + // Construct and initialize settings for a second AppDomain. + AppDomainSetup ads = new AppDomainSetup(); + ads.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; + ads.DisallowBindingRedirects = true; + ads.DisallowCodeDownload = true; + ads.LoaderOptimization = LoaderOptimization.MultiDomainHost; + ads.ShadowCopyFiles = "false"; // Disable shadowing + ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile; + + + AppDomain AD = AppDomain.CreateDomain("ScriptAppDomain_" + AppDomainNameCount, null, ads); + m_scriptEngine.Log.Info("[" + m_scriptEngine.ScriptEngineName + "]: AppDomain Loading: " + + AssemblyName.GetAssemblyName("OpenSim.Region.ScriptEngine.Common.dll").ToString()); + AD.Load(AssemblyName.GetAssemblyName("OpenSim.Region.ScriptEngine.Common.dll")); + + // Return the new AppDomain + return AD; + } + + /// + /// Unload appdomains that are full and have only dead scripts + /// + private void UnloadAppDomains() + { + lock (freeLock) + { + // Go through all + foreach (AppDomainStructure ads in new ArrayList(appDomains)) + { + // Don't process current AppDomain + if (ads.CurrentAppDomain != currentAD.CurrentAppDomain) + { + // Not current AppDomain + // Is number of unloaded bigger or equal to number of loaded? + if (ads.ScriptsLoaded <= ads.ScriptsWaitingUnload) + { + // Remove from internal list + appDomains.Remove(ads); +//#if DEBUG + //Console.WriteLine("Found empty AppDomain, unloading"); + //long m = GC.GetTotalMemory(true); // This force a garbage collect that freezes some windows plateforms +//#endif + // Unload + AppDomain.Unload(ads.CurrentAppDomain); +//#if DEBUG + //m_scriptEngine.Log.Info("[" + m_scriptEngine.ScriptEngineName + "]: AppDomain unload freed " + (m - GC.GetTotalMemory(true)) + " bytes of memory"); +//#endif + } + } + } + } + } + + public IScript LoadScript(string FileName) + { + // Find next available AppDomain to put it in + AppDomainStructure FreeAppDomain = GetFreeAppDomain(); + +#if DEBUG + m_scriptEngine.Log.Info("[" + m_scriptEngine.ScriptEngineName + "]: Loading into AppDomain: " + FileName); +#endif + IScript mbrt = + (IScript) + FreeAppDomain.CurrentAppDomain.CreateInstanceFromAndUnwrap(FileName, "SecondLife.Script"); + //Console.WriteLine("ScriptEngine AppDomainManager: is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); + FreeAppDomain.ScriptsLoaded++; + + return mbrt; + } + + + /// + /// Increase "dead script" counter for an AppDomain + /// + /// + //[Obsolete("Needs fixing, needs a real purpose in life!!!")] + public void StopScript(AppDomain ad) + { + lock (freeLock) + { +#if DEBUG + m_scriptEngine.Log.Info("[" + m_scriptEngine.ScriptEngineName + "]: Stopping script in AppDomain"); +#endif + // Check if it is current AppDomain + if (currentAD.CurrentAppDomain == ad) + { + // Yes - increase + currentAD.ScriptsWaitingUnload++; + return; + } + + // Lopp through all AppDomains + foreach (AppDomainStructure ads in new ArrayList(appDomains)) + { + if (ads.CurrentAppDomain == ad) + { + // Found it + ads.ScriptsWaitingUnload++; + break; + } + } + } + + UnloadAppDomains(); // Outsite lock, has its own GetLock + } + + /// + /// If set to true then threads and stuff should try to make a graceful exit + /// + public bool PleaseShutdown + { + get { return _PleaseShutdown; } + set { _PleaseShutdown = value; } + } + private bool _PleaseShutdown = false; + } +} diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/Common.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/Common.cs new file mode 100644 index 0000000..3d9e19b --- /dev/null +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/Common.cs @@ -0,0 +1,56 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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 OpenSim Project 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 DEVELOPERS ``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 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. + */ + +namespace OpenSim.Region.ScriptEngine.DotNetEngine +{ + public static class Common + { + public static bool debug = true; + public static ScriptEngine mySE; + + // This class just contains some static log stuff used for debugging. + + //public delegate void SendToDebugEventDelegate(string message); + //public delegate void SendToLogEventDelegate(string message); + //static public event SendToDebugEventDelegate SendToDebugEvent; + //static public event SendToLogEventDelegate SendToLogEvent; + + public static void SendToDebug(string message) + { + //if (Debug == true) + mySE.Log.Info("[" + mySE.ScriptEngineName + "]: Debug: " + message); + //SendToDebugEvent("\r\n" + DateTime.Now.ToString("[HH:mm:ss] ") + message); + } + + public static void SendToLog(string message) + { + //if (Debug == true) + mySE.Log.Info("[" + mySE.ScriptEngineName + "]: LOG: " + message); + //SendToLogEvent("\r\n" + DateTime.Now.ToString("[HH:mm:ss] ") + message); + } + } +} diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/Compiler.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/Compiler.cs index b50e823..4cb74fa 100644 --- a/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/Compiler.cs +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/Compiler/LSL/Compiler.cs @@ -83,8 +83,8 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine.Compiler.LSL private static int instanceID = new Random().Next(0, int.MaxValue); // Unique number to use on our compiled files private static UInt64 scriptCompileCounter = 0; // And a counter - public Common.ScriptEngineBase.ScriptEngine m_scriptEngine; - public Compiler(Common.ScriptEngineBase.ScriptEngine scriptEngine) + public ScriptEngine m_scriptEngine; + public Compiler(ScriptEngine scriptEngine) { m_scriptEngine = scriptEngine; ReadConfig(); diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueManager.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueManager.cs new file mode 100644 index 0000000..7805d67 --- /dev/null +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueManager.cs @@ -0,0 +1,447 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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 OpenSim Project 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 DEVELOPERS ``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 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; +using System.Collections.Generic; +using OpenMetaverse; +using OpenSim.Region.ScriptEngine.Shared; + +namespace OpenSim.Region.ScriptEngine.DotNetEngine +{ + /// + /// EventQueueManager handles event queues + /// Events are queued and executed in separate thread + /// + [Serializable] + public class EventQueueManager : iScriptEngineFunctionModule + { + // + // Class is instanced in "ScriptEngine" and used by "EventManager" which is also instanced in "ScriptEngine". + // + // Class purpose is to queue and execute functions that are received by "EventManager": + // - allowing "EventManager" to release its event thread immediately, thus not interrupting server execution. + // - allowing us to prioritize and control execution of script functions. + // Class can use multiple threads for simultaneous execution. Mutexes are used for thread safety. + // + // 1. Hold an execution queue for scripts + // 2. Use threads to process queue, each thread executes one script function on each pass. + // 3. Catch any script error and process it + // + // + // Notes: + // * Current execution load balancing is optimized for 1 thread, and can cause unfair execute balancing between scripts. + // Not noticeable unless server is under high load. + // + + public ScriptEngine m_ScriptEngine; + + /// + /// List of threads (classes) processing event queue + /// Note that this may or may not be a reference to a static object depending on PrivateRegionThreads config setting. + /// + internal static List eventQueueThreads = new List(); // Thread pool that we work on + /// + /// Locking access to eventQueueThreads AND staticGlobalEventQueueThreads. + /// +// private object eventQueueThreadsLock = new object(); + // Static objects for referencing the objects above if we don't have private threads: + //internal static List staticEventQueueThreads; // A static reference used if we don't use private threads +// internal static object staticEventQueueThreadsLock; // Statick lock object reference for same reason + + /// + /// Global static list of all threads (classes) processing event queue -- used by max enforcment thread + /// + //private List staticGlobalEventQueueThreads = new List(); + + /// + /// Used internally to specify how many threads should exit gracefully + /// + public static int ThreadsToExit; + public static object ThreadsToExitLock = new object(); + + + //public object queueLock = new object(); // Mutex lock object + + /// + /// How many threads to process queue with + /// + internal static int numberOfThreads; + + internal static int EventExecutionMaxQueueSize; + + /// + /// Maximum time one function can use for execution before we perform a thread kill. + /// + private static int maxFunctionExecutionTimems + { + get { return (int)(maxFunctionExecutionTimens / 10000); } + set { maxFunctionExecutionTimens = value * 10000; } + } + + /// + /// Contains nanoseconds version of maxFunctionExecutionTimems so that it matches time calculations better (performance reasons). + /// WARNING! ONLY UPDATE maxFunctionExecutionTimems, NEVER THIS DIRECTLY. + /// + public static long maxFunctionExecutionTimens; + /// + /// Enforce max execution time + /// + public static bool EnforceMaxExecutionTime; + /// + /// Kill script (unload) when it exceeds execution time + /// + private static bool KillScriptOnMaxFunctionExecutionTime; + + /// + /// List of localID locks for mutex processing of script events + /// + private List objectLocks = new List(); + private object tryLockLock = new object(); // Mutex lock object + + /// + /// Queue containing events waiting to be executed + /// + public Queue eventQueue = new Queue(); + + #region " Queue structures " + /// + /// Queue item structure + /// + public struct QueueItemStruct + { + public uint localID; + public UUID itemID; + public string functionName; + public DetectParams[] llDetectParams; + public object[] param; + } + + #endregion + + #region " Initialization / Startup " + public EventQueueManager(ScriptEngine _ScriptEngine) + { + m_ScriptEngine = _ScriptEngine; + + ReadConfig(); + AdjustNumberOfScriptThreads(); + } + + public void ReadConfig() + { + // Refresh config + numberOfThreads = m_ScriptEngine.ScriptConfigSource.GetInt("NumberOfScriptThreads", 2); + maxFunctionExecutionTimems = m_ScriptEngine.ScriptConfigSource.GetInt("MaxEventExecutionTimeMs", 5000); + EnforceMaxExecutionTime = m_ScriptEngine.ScriptConfigSource.GetBoolean("EnforceMaxEventExecutionTime", false); + KillScriptOnMaxFunctionExecutionTime = m_ScriptEngine.ScriptConfigSource.GetBoolean("DeactivateScriptOnTimeout", false); + EventExecutionMaxQueueSize = m_ScriptEngine.ScriptConfigSource.GetInt("EventExecutionMaxQueueSize", 300); + + // Now refresh config in all threads + lock (eventQueueThreads) + { + foreach (EventQueueThreadClass EventQueueThread in eventQueueThreads) + { + EventQueueThread.ReadConfig(); + } + } + } + + #endregion + + #region " Shutdown all threads " + ~EventQueueManager() + { + Stop(); + } + + private void Stop() + { + if (eventQueueThreads != null) + { + // Kill worker threads + lock (eventQueueThreads) + { + foreach (EventQueueThreadClass EventQueueThread in new ArrayList(eventQueueThreads)) + { + AbortThreadClass(EventQueueThread); + } + //eventQueueThreads.Clear(); + //staticGlobalEventQueueThreads.Clear(); + } + } + + // Remove all entries from our event queue + lock (eventQueue) + { + eventQueue.Clear(); + } + } + + #endregion + + #region " Start / stop script execution threads (ThreadClasses) " + private void StartNewThreadClass() + { + EventQueueThreadClass eqtc = new EventQueueThreadClass(); + eventQueueThreads.Add(eqtc); + //m_ScriptEngine.Log.Debug("[" + m_ScriptEngine.ScriptEngineName + "]: Started new script execution thread. Current thread count: " + eventQueueThreads.Count); + } + + private void AbortThreadClass(EventQueueThreadClass threadClass) + { + if (eventQueueThreads.Contains(threadClass)) + eventQueueThreads.Remove(threadClass); + + try + { + threadClass.Stop(); + } + catch (Exception) + { + //m_ScriptEngine.Log.Error("[" + m_ScriptEngine.ScriptEngineName + ":EventQueueManager]: If you see this, could you please report it to Tedd:"); + //m_ScriptEngine.Log.Error("[" + m_ScriptEngine.ScriptEngineName + ":EventQueueManager]: Script thread execution timeout kill ended in exception: " + ex.ToString()); + } + //m_ScriptEngine.Log.Debug("[" + m_ScriptEngine.ScriptEngineName + "]: Killed script execution thread. Remaining thread count: " + eventQueueThreads.Count); + } + #endregion + + #region " Mutex locks for queue access " + /// + /// Try to get a mutex lock on localID + /// + /// + /// + public bool TryLock(uint localID) + { + lock (tryLockLock) + { + if (objectLocks.Contains(localID) == true) + { + return false; + } + else + { + objectLocks.Add(localID); + return true; + } + } + } + + /// + /// Release mutex lock on localID + /// + /// + public void ReleaseLock(uint localID) + { + lock (tryLockLock) + { + if (objectLocks.Contains(localID) == true) + { + objectLocks.Remove(localID); + } + } + } + #endregion + + #region " Check execution queue for a specified Event" + /// + /// checks to see if a specified event type is already in the queue + /// + /// Region object ID + /// Name of the function, will be state + "_event_" + FunctionName + /// true if event is found , false if not found + /// + public bool CheckEeventQueueForEvent(uint localID, string FunctionName) + { + if (eventQueue.Count > 0) + { + lock (eventQueue) + { + foreach (EventQueueManager.QueueItemStruct QIS in eventQueue) + { + if ((QIS.functionName == FunctionName) && (QIS.localID == localID)) + return true; + } + } + } + return false; + } + #endregion + + #region " Add events to execution queue " + /// + /// Add event to event execution queue + /// + /// Region object ID + /// Name of the function, will be state + "_event_" + FunctionName + /// Array of parameters to match event mask + public bool AddToObjectQueue(uint localID, string FunctionName, DetectParams[] qParams, params object[] param) + { + // Determine all scripts in Object and add to their queue + //myScriptEngine.log.Info("[" + ScriptEngineName + "]: EventQueueManager Adding localID: " + localID + ", FunctionName: " + FunctionName); + + // Do we have any scripts in this object at all? If not, return + if (m_ScriptEngine.m_ScriptManager.Scripts.ContainsKey(localID) == false) + { + //Console.WriteLine("Event \String.Empty + FunctionName + "\" for localID: " + localID + ". No scripts found on this localID."); + return false; + } + + List scriptKeys = + m_ScriptEngine.m_ScriptManager.GetScriptKeys(localID); + + foreach (UUID itemID in scriptKeys) + { + // Add to each script in that object + // TODO: Some scripts may not subscribe to this event. Should we NOT add it? Does it matter? + AddToScriptQueue(localID, itemID, FunctionName, qParams, param); + } + return true; + } + + /// + /// Add event to event execution queue + /// + /// Region object ID + /// Region script ID + /// Name of the function, will be state + "_event_" + FunctionName + /// Array of parameters to match event mask + public bool AddToScriptQueue(uint localID, UUID itemID, string FunctionName, DetectParams[] qParams, params object[] param) + { + List keylist = m_ScriptEngine.m_ScriptManager.GetScriptKeys(localID); + + if (!keylist.Contains(itemID)) // We don't manage that script + { + return false; + } + + lock (eventQueue) + { + if (eventQueue.Count >= EventExecutionMaxQueueSize) + { + m_ScriptEngine.Log.Error("[" + m_ScriptEngine.ScriptEngineName + "]: ERROR: Event execution queue item count is at " + eventQueue.Count + ". Config variable \"EventExecutionMaxQueueSize\" is set to " + EventExecutionMaxQueueSize + ", so ignoring new event."); + m_ScriptEngine.Log.Error("[" + m_ScriptEngine.ScriptEngineName + "]: Event ignored: localID: " + localID + ", itemID: " + itemID + ", FunctionName: " + FunctionName); + return false; + } + + // Create a structure and add data + QueueItemStruct QIS = new QueueItemStruct(); + QIS.localID = localID; + QIS.itemID = itemID; + QIS.functionName = FunctionName; + QIS.llDetectParams = qParams; + QIS.param = param; + + // Add it to queue + eventQueue.Enqueue(QIS); + } + return true; + } + #endregion + + #region " Maintenance thread " + + /// + /// Adjust number of script thread classes. It can start new, but if it needs to stop it will just set number of threads in "ThreadsToExit" and threads will have to exit themselves. + /// Called from MaintenanceThread + /// + public void AdjustNumberOfScriptThreads() + { + // Is there anything here for us to do? + if (eventQueueThreads.Count == numberOfThreads) + return; + + lock (eventQueueThreads) + { + int diff = numberOfThreads - eventQueueThreads.Count; + // Positive number: Start + // Negative number: too many are running + if (diff > 0) + { + // We need to add more threads + for (int ThreadCount = eventQueueThreads.Count; ThreadCount < numberOfThreads; ThreadCount++) + { + StartNewThreadClass(); + } + } + if (diff < 0) + { + // We need to kill some threads + lock (ThreadsToExitLock) + { + ThreadsToExit = Math.Abs(diff); + } + } + } + } + + /// + /// Check if any thread class has been executing an event too long + /// + public void CheckScriptMaxExecTime() + { + // Iterate through all ScriptThreadClasses and check how long their current function has been executing + lock (eventQueueThreads) + { + foreach (EventQueueThreadClass EventQueueThread in eventQueueThreads) + { + // Is thread currently executing anything? + if (EventQueueThread.InExecution) + { + // Has execution time expired? + if (DateTime.Now.Ticks - EventQueueThread.LastExecutionStarted > + maxFunctionExecutionTimens) + { + // Yes! We need to kill this thread! + + // Set flag if script should be removed or not + EventQueueThread.KillCurrentScript = KillScriptOnMaxFunctionExecutionTime; + + // Abort this thread + AbortThreadClass(EventQueueThread); + + // We do not need to start another, MaintenenceThread will do that for us + //StartNewThreadClass(); + } + } + } + } + } + #endregion + + ///// + ///// If set to true then threads and stuff should try to make a graceful exit + ///// + //public bool PleaseShutdown + //{ + // get { return _PleaseShutdown; } + // set { _PleaseShutdown = value; } + //} + //private bool _PleaseShutdown = false; + } +} diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueThreadClass.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueThreadClass.cs new file mode 100644 index 0000000..db3f89f --- /dev/null +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/EventQueueThreadClass.cs @@ -0,0 +1,381 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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 OpenSim Project 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 DEVELOPERS ``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 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; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using System.Globalization; +using OpenMetaverse; +using log4net; +using OpenSim.Framework; +using OpenSim.Region.Environment.Scenes.Scripting; + +namespace OpenSim.Region.ScriptEngine.DotNetEngine +{ + /// + /// Because every thread needs some data set for it (time started to execute current function), it will do its work within a class + /// + public class EventQueueThreadClass : iScriptEngineFunctionModule + { + // private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + /// + /// How many ms to sleep if queue is empty + /// + private static int nothingToDoSleepms;// = 50; + private static ThreadPriority MyThreadPriority; + + public long LastExecutionStarted; + public bool InExecution = false; + public bool KillCurrentScript = false; + + //private EventQueueManager eventQueueManager; + public Thread EventQueueThread; + private static int ThreadCount = 0; + + private string ScriptEngineName = "ScriptEngine.Common"; + + public EventQueueThreadClass()//EventQueueManager eqm + { + //eventQueueManager = eqm; + ReadConfig(); + Start(); + } + + ~EventQueueThreadClass() + { + Stop(); + } + + public void ReadConfig() + { + lock (ScriptEngine.ScriptEngines) + { + foreach (ScriptEngine m_ScriptEngine in ScriptEngine.ScriptEngines) + { + ScriptEngineName = m_ScriptEngine.ScriptEngineName; + nothingToDoSleepms = m_ScriptEngine.ScriptConfigSource.GetInt("SleepTimeIfNoScriptExecutionMs", 50); + + // Later with ScriptServer we might want to ask OS for stuff too, so doing this a bit manually + string pri = m_ScriptEngine.ScriptConfigSource.GetString("ScriptThreadPriority", "BelowNormal"); + switch (pri.ToLower()) + { + case "lowest": + MyThreadPriority = ThreadPriority.Lowest; + break; + case "belownormal": + MyThreadPriority = ThreadPriority.BelowNormal; + break; + case "normal": + MyThreadPriority = ThreadPriority.Normal; + break; + case "abovenormal": + MyThreadPriority = ThreadPriority.AboveNormal; + break; + case "highest": + MyThreadPriority = ThreadPriority.Highest; + break; + default: + MyThreadPriority = ThreadPriority.BelowNormal; // Default + m_ScriptEngine.Log.Error("[ScriptEngine.DotNetEngine]: Unknown priority type \"" + pri + + "\" in config file. Defaulting to \"BelowNormal\"."); + break; + } + } + } + // Now set that priority + if (EventQueueThread != null) + if (EventQueueThread.IsAlive) + EventQueueThread.Priority = MyThreadPriority; + } + + /// + /// Start thread + /// + private void Start() + { + EventQueueThread = new Thread(EventQueueThreadLoop); + EventQueueThread.IsBackground = true; + + EventQueueThread.Priority = MyThreadPriority; + EventQueueThread.Name = "EventQueueManagerThread_" + ThreadCount; + EventQueueThread.Start(); + ThreadTracker.Add(EventQueueThread); + + // Look at this... Don't you wish everyone did that solid coding everywhere? :P + if (ThreadCount == int.MaxValue) + ThreadCount = 0; + ThreadCount++; + } + + public void Stop() + { + //PleaseShutdown = true; // Set shutdown flag + //Thread.Sleep(100); // Wait a bit + if (EventQueueThread != null && EventQueueThread.IsAlive == true) + { + try + { + EventQueueThread.Abort(); // Send abort + //EventQueueThread.Join(); // Wait for it + } + catch (Exception) + { + //myScriptEngine.Log.Info("[" + ScriptEngineName + "]: EventQueueManager Exception killing worker thread: " + e.ToString()); + } + } + } + + private EventQueueManager.QueueItemStruct BlankQIS = new EventQueueManager.QueueItemStruct(); + private ScriptEngine lastScriptEngine; + /// + /// Queue processing thread loop + /// + private void EventQueueThreadLoop() + { + CultureInfo USCulture = new CultureInfo("en-US"); + Thread.CurrentThread.CurrentCulture = USCulture; + + //myScriptEngine.Log.Info("[" + ScriptEngineName + "]: EventQueueManager Worker thread spawned"); + try + { + while (true) + { + try + { + while (true) + { + DoProcessQueue(); + } + } + catch (ThreadAbortException) + { + if (lastScriptEngine != null) + lastScriptEngine.Log.Info("[" + ScriptEngineName + "]: ThreadAbortException while executing function."); + } + catch (Exception e) + { + if (lastScriptEngine != null) + lastScriptEngine.Log.Error("[" + ScriptEngineName + "]: Exception in EventQueueThreadLoop: " + e.ToString()); + } + } + } + catch (ThreadAbortException) + { + //myScriptEngine.Log.Info("[" + ScriptEngineName + "]: EventQueueManager Worker thread killed: " + tae.Message); + } + } + + public void DoProcessQueue() + { + //lock (ScriptEngine.ScriptEngines) + //{ + foreach (ScriptEngine m_ScriptEngine in new ArrayList(ScriptEngine.ScriptEngines)) + { + lastScriptEngine = m_ScriptEngine; + // Every now and then check if we should shut down + //if (PleaseShutdown || EventQueueManager.ThreadsToExit > 0) + //{ + // // Someone should shut down, lets get exclusive lock + // lock (EventQueueManager.ThreadsToExitLock) + // { + // // Lets re-check in case someone grabbed it + // if (EventQueueManager.ThreadsToExit > 0) + // { + // // Its crowded here so we'll shut down + // EventQueueManager.ThreadsToExit--; + // Stop(); + // return; + // } + // else + // { + // // We have been asked to shut down + // Stop(); + // return; + // } + // } + //} + + //try + // { + EventQueueManager.QueueItemStruct QIS = BlankQIS; + bool GotItem = false; + + //if (PleaseShutdown) + // return; + + if (m_ScriptEngine.m_EventQueueManager == null || m_ScriptEngine.m_EventQueueManager.eventQueue == null) + continue; + + if (m_ScriptEngine.m_EventQueueManager.eventQueue.Count == 0) + { + // Nothing to do? Sleep a bit waiting for something to do + Thread.Sleep(nothingToDoSleepms); + } + else + { + // Something in queue, process + //myScriptEngine.Log.Info("[" + ScriptEngineName + "]: Processing event for localID: " + QIS.localID + ", itemID: " + QIS.itemID + ", FunctionName: " + QIS.FunctionName); + + // OBJECT BASED LOCK - TWO THREADS WORKING ON SAME OBJECT IS NOT GOOD + lock (m_ScriptEngine.m_EventQueueManager.eventQueue) + { + GotItem = false; + for (int qc = 0; qc < m_ScriptEngine.m_EventQueueManager.eventQueue.Count; qc++) + { + // Get queue item + QIS = m_ScriptEngine.m_EventQueueManager.eventQueue.Dequeue(); + + // Check if object is being processed by someone else + if (m_ScriptEngine.m_EventQueueManager.TryLock(QIS.localID) == false) + { + // Object is already being processed, requeue it + m_ScriptEngine.m_EventQueueManager.eventQueue.Enqueue(QIS); + } + else + { + // We have lock on an object and can process it + GotItem = true; + break; + } + } + } + + if (GotItem == true) + { + // Execute function + try + { + ///cfk 2-7-08 dont need this right now and the default Linux build has DEBUG defined +#if DEBUG + //eventQueueManager.m_ScriptEngine.Log.Debug("[" + ScriptEngineName + "]: " + + // "Executing event:\r\n" + // + "QIS.localID: " + QIS.localID + // + ", QIS.itemID: " + QIS.itemID + // + ", QIS.functionName: " + + // QIS.functionName); +#endif + // Only pipe event if land supports it. + if (m_ScriptEngine.World.PipeEventsForScript(QIS.localID)) + { + LastExecutionStarted = DateTime.Now.Ticks; + KillCurrentScript = false; + InExecution = true; + m_ScriptEngine.m_ScriptManager.ExecuteEvent(QIS.localID, + QIS.itemID, + QIS.functionName, + QIS.llDetectParams, + QIS.param); + InExecution = false; + } + } + catch (Exception e) + { + InExecution = false; + // DISPLAY ERROR INWORLD + string text = "Error executing script function \"" + QIS.functionName + + "\":\r\n"; + if (e.InnerException != null) + { + // Send inner exception + string line = " (unknown line)"; + Regex rx = new Regex(@"SecondLife\.Script\..+[\s:](?\d+)\.?\r?$", RegexOptions.Compiled); + if (rx.Match(e.InnerException.ToString()).Success) + line = " (line " + rx.Match(e.InnerException.ToString()).Result("${line}") + ")"; + text += e.InnerException.Message.ToString() + line; + } + else + { + text += "\r\n"; + // Send normal + text += e.Message.ToString(); + } + if (KillCurrentScript) + text += "\r\nScript will be deactivated!"; + + try + { + if (text.Length > 1500) + text = text.Substring(0, 1500); + IScriptHost m_host = + m_ScriptEngine.World.GetSceneObjectPart(QIS.localID); + //if (m_host != null) + //{ + m_ScriptEngine.World.SimChat(Utils.StringToBytes(text), + ChatTypeEnum.DebugChannel, 2147483647, + m_host.AbsolutePosition, + m_host.Name, m_host.UUID, false); + } + catch (Exception) + { + //} + //else + //{ + // T oconsole + m_ScriptEngine.m_EventQueueManager.m_ScriptEngine.Log.Error("[" + ScriptEngineName + + "]: " + + "Unable to send text in-world:\r\n" + + text); + } + finally + { + // So we are done sending message in-world + if (KillCurrentScript) + { + m_ScriptEngine.m_EventQueueManager.m_ScriptEngine.m_ScriptManager.StopScript( + QIS.localID, QIS.itemID); + } + } + + // Pass it on so it's displayed on the console + // and in the logs (mikem 2008.06.02). + throw e.InnerException; + } + finally + { + InExecution = false; + m_ScriptEngine.m_EventQueueManager.ReleaseLock(QIS.localID); + } + } + } + } + // } + } + + ///// + ///// If set to true then threads and stuff should try to make a graceful exit + ///// + //public bool PleaseShutdown + //{ + // get { return _PleaseShutdown; } + // set { _PleaseShutdown = value; } + //} + //private bool _PleaseShutdown = false; + } +} diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/MaintenanceThread.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/MaintenanceThread.cs new file mode 100644 index 0000000..6c1528f --- /dev/null +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/MaintenanceThread.cs @@ -0,0 +1,243 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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 OpenSim Project 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 DEVELOPERS ``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 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; +using System.Reflection; +using System.Threading; +using log4net; +using OpenSim.Framework; + +namespace OpenSim.Region.ScriptEngine.DotNetEngine +{ + /// + /// This class does maintenance on script engine. + /// + public class MaintenanceThread : iScriptEngineFunctionModule + { + + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + //public ScriptEngine m_ScriptEngine; + private int MaintenanceLoopms; + private int MaintenanceLoopTicks_ScriptLoadUnload; + private int MaintenanceLoopTicks_Other; + + + public MaintenanceThread() + { + //m_ScriptEngine = _ScriptEngine; + + ReadConfig(); + + // Start maintenance thread + StartMaintenanceThread(); + } + + ~MaintenanceThread() + { + StopMaintenanceThread(); + } + + public void ReadConfig() + { + // Bad hack, but we need a m_ScriptEngine :) + lock (ScriptEngine.ScriptEngines) + { + foreach (ScriptEngine m_ScriptEngine in ScriptEngine.ScriptEngines) + { + MaintenanceLoopms = m_ScriptEngine.ScriptConfigSource.GetInt("MaintenanceLoopms", 50); + MaintenanceLoopTicks_ScriptLoadUnload = + m_ScriptEngine.ScriptConfigSource.GetInt("MaintenanceLoopTicks_ScriptLoadUnload", 1); + MaintenanceLoopTicks_Other = + m_ScriptEngine.ScriptConfigSource.GetInt("MaintenanceLoopTicks_Other", 10); + + return; + } + } + } + + #region " Maintenance thread " + /// + /// Maintenance thread. Enforcing max execution time for example. + /// + public Thread MaintenanceThreadThread; + + /// + /// Starts maintenance thread + /// + private void StartMaintenanceThread() + { + if (MaintenanceThreadThread == null) + { + MaintenanceThreadThread = new Thread(MaintenanceLoop); + MaintenanceThreadThread.Name = "ScriptMaintenanceThread"; + MaintenanceThreadThread.IsBackground = true; + MaintenanceThreadThread.Start(); + ThreadTracker.Add(MaintenanceThreadThread); + } + } + + /// + /// Stops maintenance thread + /// + private void StopMaintenanceThread() + { +#if DEBUG + //m_ScriptEngine.Log.Debug("[" + m_ScriptEngine.ScriptEngineName + "]: StopMaintenanceThread() called"); +#endif + //PleaseShutdown = true; + Thread.Sleep(100); + try + { + if (MaintenanceThreadThread != null && MaintenanceThreadThread.IsAlive) + { + MaintenanceThreadThread.Abort(); + } + } + catch (Exception) + { + //m_ScriptEngine.Log.Error("[" + m_ScriptEngine.ScriptEngineName + "]: Exception stopping maintenence thread: " + ex.ToString()); + } + } + + // private ScriptEngine lastScriptEngine; // Keep track of what ScriptEngine instance we are at so we can give exception + /// + /// A thread should run in this loop and check all running scripts + /// + public void MaintenanceLoop() + { + //if (m_ScriptEngine.m_EventQueueManager.maxFunctionExecutionTimens < MaintenanceLoopms) + // m_ScriptEngine.Log.Warn("[" + m_ScriptEngine.ScriptEngineName + "]: " + + // "Configuration error: MaxEventExecutionTimeMs is less than MaintenanceLoopms. The Maintenance Loop will only check scripts once per run."); + + long Last_maxFunctionExecutionTimens = 0; // DateTime.Now.Ticks; + long Last_ReReadConfigFilens = DateTime.Now.Ticks; + int MaintenanceLoopTicks_ScriptLoadUnload_Count = 0; + int MaintenanceLoopTicks_Other_Count = 0; + bool MaintenanceLoopTicks_ScriptLoadUnload_ResetCount = false; + bool MaintenanceLoopTicks_Other_ResetCount = false; + + while (true) + { + try + { + while (true) + { + Thread.Sleep(MaintenanceLoopms); // Sleep before next pass + + // Reset counters? + if (MaintenanceLoopTicks_ScriptLoadUnload_ResetCount) + { + MaintenanceLoopTicks_ScriptLoadUnload_ResetCount = false; + MaintenanceLoopTicks_ScriptLoadUnload_Count = 0; + } + if (MaintenanceLoopTicks_Other_ResetCount) + { + MaintenanceLoopTicks_Other_ResetCount = false; + MaintenanceLoopTicks_Other_Count = 0; + } + + // Increase our counters + MaintenanceLoopTicks_ScriptLoadUnload_Count++; + MaintenanceLoopTicks_Other_Count++; + + + //lock (ScriptEngine.ScriptEngines) + //{ + foreach (ScriptEngine m_ScriptEngine in new ArrayList(ScriptEngine.ScriptEngines)) + { + // lastScriptEngine = m_ScriptEngine; + // Re-reading config every x seconds + if (MaintenanceLoopTicks_Other_Count >= MaintenanceLoopTicks_Other) + { + MaintenanceLoopTicks_Other_ResetCount = true; + if (m_ScriptEngine.RefreshConfigFilens > 0) + { + // Check if its time to re-read config + if (DateTime.Now.Ticks - Last_ReReadConfigFilens > + m_ScriptEngine.RefreshConfigFilens) + { + //Console.WriteLine("Time passed: " + (DateTime.Now.Ticks - Last_ReReadConfigFilens) + ">" + m_ScriptEngine.RefreshConfigFilens); + // Its time to re-read config file + m_ScriptEngine.ReadConfig(); + Last_ReReadConfigFilens = DateTime.Now.Ticks; // Reset time + } + + + // Adjust number of running script threads if not correct + if (m_ScriptEngine.m_EventQueueManager != null) + m_ScriptEngine.m_EventQueueManager.AdjustNumberOfScriptThreads(); + + // Check if any script has exceeded its max execution time + if (EventQueueManager.EnforceMaxExecutionTime) + { + // We are enforcing execution time + if (DateTime.Now.Ticks - Last_maxFunctionExecutionTimens > + EventQueueManager.maxFunctionExecutionTimens) + { + // Its time to check again + m_ScriptEngine.m_EventQueueManager.CheckScriptMaxExecTime(); // Do check + Last_maxFunctionExecutionTimens = DateTime.Now.Ticks; // Reset time + } + } + } + } + if (MaintenanceLoopTicks_ScriptLoadUnload_Count >= MaintenanceLoopTicks_ScriptLoadUnload) + { + MaintenanceLoopTicks_ScriptLoadUnload_ResetCount = true; + // LOAD / UNLOAD SCRIPTS + if (m_ScriptEngine.m_ScriptManager != null) + m_ScriptEngine.m_ScriptManager.DoScriptLoadUnload(); + } + } + //} + } + } + catch(ThreadAbortException) + { + m_log.Error("Thread aborted in MaintenanceLoopThread. If this is during shutdown, please ignore"); + } + catch (Exception ex) + { + m_log.ErrorFormat("Exception in MaintenanceLoopThread. Thread will recover after 5 sec throttle. Exception: {0}", ex.ToString()); + } + } + } + #endregion + + ///// + ///// If set to true then threads and stuff should try to make a graceful exit + ///// + //public bool PleaseShutdown + //{ + // get { return _PleaseShutdown; } + // set { _PleaseShutdown = value; } + //} + //private bool _PleaseShutdown = false; + } +} diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptEngine.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptEngine.cs index e785cc0..7ec71c2 100644 --- a/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptEngine.cs +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptEngine.cs @@ -26,29 +26,281 @@ */ using System; +using System.Collections.Generic; +using System.Reflection; +using log4net; using Nini.Config; +using OpenSim.Region.Interfaces; +using OpenSim.Region.Environment.Interfaces; using OpenSim.Region.Environment.Scenes; +using OpenSim.Region.ScriptEngine.Interfaces; +using OpenMetaverse; +using OpenSim.Region.ScriptEngine.Shared; +using OpenSim.Region.ScriptEngine.Common; namespace OpenSim.Region.ScriptEngine.DotNetEngine { [Serializable] - public class ScriptEngine : Common.ScriptEngineBase.ScriptEngine + public class ScriptEngine : IRegionModule, IEventReceiver, IScriptModule { - // We need to override a few things for our DotNetEngine - public override void Initialise(Scene scene, IConfigSource config) + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public static List ScriptEngines = new List(); + private Scene m_Scene; + public Scene World { - ConfigSource = config; - InitializeEngine(scene, config, true, GetScriptManager()); + get { return m_Scene; } } + public EventManager m_EventManager; // Handles and queues incoming events from OpenSim + public EventQueueManager m_EventQueueManager; // Executes events, handles script threads + public ScriptManager m_ScriptManager; // Load, unload and execute scripts + public AppDomainManager m_AppDomainManager; // Handles loading/unloading of scripts into AppDomains + public static MaintenanceThread m_MaintenanceThread; // Thread that does different kinds of maintenance, for example refreshing config and killing scripts that has been running too long + + public IConfigSource ConfigSource; + public IConfig ScriptConfigSource; + private bool m_enabled = false; - public override Common.ScriptEngineBase.ScriptManager _GetScriptManager() + public IConfig Config { - return new ScriptManager(this); + get { return ScriptConfigSource; } } - public override string ScriptEngineName + /// + /// How many seconds between re-reading config-file. 0 = never. ScriptEngine will try to adjust to new config changes. + /// + public int RefreshConfigFileSeconds { + get { return (int)(RefreshConfigFilens / 10000000); } + set { RefreshConfigFilens = value * 10000000; } + } + public long RefreshConfigFilens; + + public string ScriptEngineName { get { return "ScriptEngine.DotNetEngine"; } } + + public ILog Log + { + get { return m_log; } + } + + public ScriptEngine() + { + Common.mySE = this; // For logging, just need any instance, doesn't matter + lock (ScriptEngines) + { + ScriptEngines.Add(this); // Keep a list of ScriptEngines for shared threads to process all instances + } + } + + public void Initialise(Scene Sceneworld, IConfigSource config) + { + m_log.Info("[" + ScriptEngineName + "]: ScriptEngine initializing"); + + ConfigSource = config; + m_Scene = Sceneworld; + + // Make sure we have config + if (ConfigSource.Configs[ScriptEngineName] == null) + ConfigSource.AddConfig(ScriptEngineName); + ScriptConfigSource = ConfigSource.Configs[ScriptEngineName]; + + m_enabled = ScriptConfigSource.GetBoolean("Enabled", true); + if (!m_enabled) + return; + + //m_log.Info("[" + ScriptEngineName + "]: InitializeEngine"); + + // Create all objects we'll be using + m_EventQueueManager = new EventQueueManager(this); + m_EventManager = new EventManager(this, true); + // We need to start it + m_ScriptManager = new ScriptManager(this); + m_ScriptManager.Setup(); + m_AppDomainManager = new AppDomainManager(this); + if (m_MaintenanceThread == null) + m_MaintenanceThread = new MaintenanceThread(); + + m_log.Info("[" + ScriptEngineName + "]: Reading configuration from config section \"" + ScriptEngineName + "\""); + ReadConfig(); + + m_Scene.StackModuleInterface(this); + } + + public void PostInitialise() + { + if (!m_enabled) + return; + + m_EventManager.HookUpEvents(); + + m_ScriptManager.Start(); + } + + public void Shutdown() + { + // We are shutting down + lock (ScriptEngines) + { + ScriptEngines.Remove(this); + } + } + + public void ReadConfig() + { +#if DEBUG + //m_log.Debug("[" + ScriptEngineName + "]: Refreshing configuration for all modules"); +#endif + RefreshConfigFileSeconds = ScriptConfigSource.GetInt("RefreshConfig", 30); + + + // Create a new object (probably not necessary?) +// ScriptConfigSource = ConfigSource.Configs[ScriptEngineName]; + + if (m_EventQueueManager != null) m_EventQueueManager.ReadConfig(); + if (m_EventManager != null) m_EventManager.ReadConfig(); + if (m_ScriptManager != null) m_ScriptManager.ReadConfig(); + if (m_AppDomainManager != null) m_AppDomainManager.ReadConfig(); + if (m_MaintenanceThread != null) m_MaintenanceThread.ReadConfig(); + } + + #region IRegionModule + + public void Close() + { + } + + public string Name + { + get { return "Common." + ScriptEngineName; } + } + + public bool IsSharedModule + { + get { return false; } + } + + public bool PostObjectEvent(uint localID, EventParams p) + { + return m_EventQueueManager.AddToObjectQueue(localID, p.EventName, p.DetectParams, p.Params); + } + + public bool PostScriptEvent(UUID itemID, EventParams p) + { + uint localID = m_ScriptManager.GetLocalID(itemID); + return m_EventQueueManager.AddToScriptQueue(localID, itemID, p.EventName, p.DetectParams, p.Params); + } + + public DetectParams GetDetectParams(UUID itemID, int number) + { + uint localID = m_ScriptManager.GetLocalID(itemID); + if (localID == 0) + return null; + + IScript Script = m_ScriptManager.GetScript(localID, itemID); + + if (Script == null) + return null; + + DetectParams[] det = m_ScriptManager.GetDetectParams(Script); + + if (number < 0 || number >= det.Length) + return null; + + return det[number]; + } + + public int GetStartParameter(UUID itemID) + { + return 0; + } + #endregion + + public void SetState(UUID itemID, string state) + { + uint localID = m_ScriptManager.GetLocalID(itemID); + if (localID == 0) + return; + + IScript Script = m_ScriptManager.GetScript(localID, itemID); + + if (Script == null) + return; + + string currentState = Script.State; + + if (currentState != state) + { + try + { + m_EventManager.state_exit(localID); + + } + catch (AppDomainUnloadedException) + { + Console.WriteLine("[SCRIPT]: state change called when script was unloaded. Nothing to worry about, but noting the occurance"); + } + + Script.State = state; + + try + { + int eventFlags = m_ScriptManager.GetStateEventFlags(localID, itemID); + SceneObjectPart part = m_Scene.GetSceneObjectPart(itemID); + if (part != null) + part.SetScriptEvents(itemID, eventFlags); + m_EventManager.state_entry(localID); + } + catch (AppDomainUnloadedException) + { + Console.WriteLine("[SCRIPT]: state change called when script was unloaded. Nothing to worry about, but noting the occurance"); + } + } + } + + public bool GetScriptState(UUID itemID) + { + uint localID = m_ScriptManager.GetLocalID(itemID); + if (localID == 0) + return false; + + IScript script = m_ScriptManager.GetScript(localID, itemID); + if (script == null) + return false; + + return script.Exec.Running?true:false; + } + + public void SetScriptState(UUID itemID, bool state) + { + uint localID = m_ScriptManager.GetLocalID(itemID); + if (localID == 0) + return; + + IScript script = m_ScriptManager.GetScript(localID, itemID); + if (script == null) + return; + + script.Exec.Running = state; + } + + public void ApiResetScript(UUID itemID) + { + uint localID = m_ScriptManager.GetLocalID(itemID); + if (localID == 0) + return; + + m_ScriptManager.ResetScript(localID, itemID); + } + + public void ResetScript(UUID itemID) + { + uint localID = m_ScriptManager.GetLocalID(itemID); + if (localID == 0) + return; + + m_ScriptManager.ResetScript(localID, itemID); + } } } diff --git a/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptManager.cs b/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptManager.cs index 8ff3bfd..12a8fe4 100644 --- a/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptManager.cs +++ b/OpenSim/Region/ScriptEngine/DotNetEngine/ScriptManager.cs @@ -34,22 +34,66 @@ using OpenSim.Region.Environment.Scenes; using OpenSim.Region.ScriptEngine.Common; using OpenSim.Region.ScriptEngine.Shared; using OpenSim.Region.ScriptEngine.Shared.Api; -using OpenSim.Region.ScriptEngine.Common.ScriptEngineBase; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Threading; namespace OpenSim.Region.ScriptEngine.DotNetEngine { - public class ScriptManager : Common.ScriptEngineBase.ScriptManager + public class ScriptManager { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - public ScriptManager(Common.ScriptEngineBase.ScriptEngine scriptEngine) - : base(scriptEngine) + #region Declares + + private Thread scriptLoadUnloadThread; + private static Thread staticScriptLoadUnloadThread; + // private int scriptLoadUnloadThread_IdleSleepms; + private Queue LUQueue = new Queue(); + private static bool PrivateThread; + private int LoadUnloadMaxQueueSize; + private Object scriptLock = new Object(); + private bool m_started = false; + private Dictionary detparms = new Dictionary(); + + // Load/Unload structure + private struct LUStruct + { + public uint localID; + public UUID itemID; + public string script; + public LUType Action; + public int startParam; + public bool postOnRez; + } + + private enum LUType + { + Unknown = 0, + Load = 1, + Unload = 2 + } + + // Xantor 20080525: Keep a list of compiled scripts this session for reuse + public Dictionary scriptList = new Dictionary(); + + // Object> + // IMPORTANT: Types and MemberInfo-derived objects require a LOT of memory. + // Instead use RuntimeTypeHandle, RuntimeFieldHandle and RunTimeHandle (IntPtr) instead! + public Dictionary> Scripts = + new Dictionary>(); + + + public Scene World { - base.m_scriptEngine = scriptEngine; + get { return m_scriptEngine.World; } } + + #endregion private Compiler.LSL.Compiler LSLCompiler; - public override void Initialize() + public void Initialize() { // Create our compiler LSLCompiler = new Compiler.LSL.Compiler(m_scriptEngine); @@ -62,7 +106,7 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine // PROVIDE SCRIPT WITH ITS INTERFACE TO OpenSim - public override void _StartScript(uint localID, UUID itemID, string Script, int startParam, bool postOnRez) + public void _StartScript(uint localID, UUID itemID, string Script, int startParam, bool postOnRez) { m_log.DebugFormat( "[{0}]: ScriptManager StartScript: localID: {1}, itemID: {2}", @@ -173,7 +217,7 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine } } - public override void _StopScript(uint localID, UUID itemID) + public void _StopScript(uint localID, UUID itemID) { IScript LSLBC = GetScript(localID, itemID); if (LSLBC == null) @@ -202,5 +246,405 @@ namespace OpenSim.Region.ScriptEngine.DotNetEngine ": " + e.ToString()); } } + + public void ReadConfig() + { + // scriptLoadUnloadThread_IdleSleepms = m_scriptEngine.ScriptConfigSource.GetInt("ScriptLoadUnloadLoopms", 30); + // TODO: Requires sharing of all ScriptManagers to single thread + PrivateThread = true; // m_scriptEngine.ScriptConfigSource.GetBoolean("PrivateScriptLoadUnloadThread", false); + LoadUnloadMaxQueueSize = m_scriptEngine.ScriptConfigSource.GetInt("LoadUnloadMaxQueueSize", 100); + } + + #region Object init/shutdown + + public ScriptEngine m_scriptEngine; + + public ScriptManager(ScriptEngine scriptEngine) + { + m_scriptEngine = scriptEngine; + } + public void Setup() + { + ReadConfig(); + Initialize(); + } + public void Start() + { + m_started = true; + + + AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); + + // + // CREATE THREAD + // Private or shared + // + if (PrivateThread) + { + // Assign one thread per region + //scriptLoadUnloadThread = StartScriptLoadUnloadThread(); + } + else + { + // Shared thread - make sure one exist, then assign it to the private + if (staticScriptLoadUnloadThread == null) + { + //staticScriptLoadUnloadThread = StartScriptLoadUnloadThread(); + } + scriptLoadUnloadThread = staticScriptLoadUnloadThread; + } + } + +// TODO: unused +// private static int privateThreadCount = 0; +// private Thread StartScriptLoadUnloadThread() +// { +// Thread t = new Thread(ScriptLoadUnloadThreadLoop); +// string name = "ScriptLoadUnloadThread:"; +// if (PrivateThread) +// { +// name += "Private:" + privateThreadCount; +// privateThreadCount++; +// } +// else +// { +// name += "Shared"; +// } +// t.Name = name; +// t.IsBackground = true; +// t.Priority = ThreadPriority.Normal; +// t.Start(); +// OpenSim.Framework.ThreadTracker.Add(t); +// return t; +// } + + ~ScriptManager() + { + // Abort load/unload thread + try + { + //PleaseShutdown = true; + //Thread.Sleep(100); + if (scriptLoadUnloadThread != null && scriptLoadUnloadThread.IsAlive == true) + { + scriptLoadUnloadThread.Abort(); + //scriptLoadUnloadThread.Join(); + } + } + catch + { + } + } + + #endregion + + #region Load / Unload scripts (Thread loop) + +// TODO: unused +// private void ScriptLoadUnloadThreadLoop() +// { +// try +// { +// while (true) +// { +// if (LUQueue.Count == 0) +// Thread.Sleep(scriptLoadUnloadThread_IdleSleepms); +// //if (PleaseShutdown) +// // return; +// DoScriptLoadUnload(); +// } +// } +// catch (ThreadAbortException tae) +// { +// string a = tae.ToString(); +// a = String.Empty; +// // Expected +// } +// } + + public void DoScriptLoadUnload() + { + if (!m_started) + return; + + lock (LUQueue) + { + if (LUQueue.Count > 0) + { +m_scriptEngine.Log.InfoFormat("[{0}]: Loading script", m_scriptEngine.ScriptEngineName); + LUStruct item = LUQueue.Dequeue(); + + if (item.Action == LUType.Unload) + { + _StopScript(item.localID, item.itemID); + RemoveScript(item.localID, item.itemID); + } + else if (item.Action == LUType.Load) + { + _StartScript(item.localID, item.itemID, item.script, item.startParam, item.postOnRez); + } + } + } + } + + #endregion + + #region Helper functions + + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + //Console.WriteLine("ScriptManager.CurrentDomain_AssemblyResolve: " + args.Name); + return Assembly.GetExecutingAssembly().FullName == args.Name ? Assembly.GetExecutingAssembly() : null; + } + + #endregion + + + + #region Start/Stop/Reset script + + // private readonly Object startStopLock = new Object(); + + /// + /// Fetches, loads and hooks up a script to an objects events + /// + /// + /// + public void StartScript(uint localID, UUID itemID, string Script, int startParam, bool postOnRez) + { + lock (LUQueue) + { + if ((LUQueue.Count >= LoadUnloadMaxQueueSize) && m_started) + { + m_scriptEngine.Log.Error("[" + m_scriptEngine.ScriptEngineName + "]: ERROR: Load/unload queue item count is at " + LUQueue.Count + ". Config variable \"LoadUnloadMaxQueueSize\" is set to " + LoadUnloadMaxQueueSize + ", so ignoring new script."); + return; + } + + LUStruct ls = new LUStruct(); + ls.localID = localID; + ls.itemID = itemID; + ls.script = Script; + ls.Action = LUType.Load; + ls.startParam = startParam; + ls.postOnRez = postOnRez; + LUQueue.Enqueue(ls); +m_scriptEngine.Log.InfoFormat("[{0}]: Queued script for load", m_scriptEngine.ScriptEngineName); + } + } + + /// + /// Disables and unloads a script + /// + /// + /// + public void StopScript(uint localID, UUID itemID) + { + LUStruct ls = new LUStruct(); + ls.localID = localID; + ls.itemID = itemID; + ls.Action = LUType.Unload; + ls.startParam = 0; + ls.postOnRez = false; + lock (LUQueue) + { + LUQueue.Enqueue(ls); + } + } + + // Create a new instance of the compiler (reuse) + //private Compiler.LSL.Compiler LSLCompiler = new Compiler.LSL.Compiler(); + + + #endregion + + #region Perform event execution in script + + /// + /// Execute a LL-event-function in Script + /// + /// Object the script is located in + /// Script ID + /// Name of function + /// Arguments to pass to function + internal void ExecuteEvent(uint localID, UUID itemID, string FunctionName, DetectParams[] qParams, object[] args) + { + //cfk 2-7-08 dont need this right now and the default Linux build has DEBUG defined + ///#if DEBUG + /// Console.WriteLine("ScriptEngine: Inside ExecuteEvent for event " + FunctionName); + ///#endif + // Execute a function in the script + //m_scriptEngine.Log.Info("[" + ScriptEngineName + "]: Executing Function localID: " + localID + ", itemID: " + itemID + ", FunctionName: " + FunctionName); + //ScriptBaseInterface Script = (ScriptBaseInterface)GetScript(localID, itemID); + IScript Script = GetScript(localID, itemID); + if (Script == null) + { + return; + } + //cfk 2-7-08 dont need this right now and the default Linux build has DEBUG defined + ///#if DEBUG + /// Console.WriteLine("ScriptEngine: Executing event: " + FunctionName); + ///#endif + // Must be done in correct AppDomain, so leaving it up to the script itself + detparms[Script] = qParams; + Script.Exec.ExecuteEvent(FunctionName, args); + detparms.Remove(Script); + } + + public uint GetLocalID(UUID itemID) + { + foreach (KeyValuePair > k in Scripts) + { + if (k.Value.ContainsKey(itemID)) + return k.Key; + } + return 0; + } + + public int GetStateEventFlags(uint localID, UUID itemID) + { + // Console.WriteLine("GetStateEventFlags for <" + localID + "," + itemID + ">"); + try + { + IScript Script = GetScript(localID, itemID); + if (Script == null) + { + return 0; + } + ExecutorBase.scriptEvents evflags = Script.Exec.GetStateEventFlags(); + return (int)evflags; + } + catch (Exception) + { + } + + return 0; + } + + + #endregion + + #region Internal functions to keep track of script + + public List GetScriptKeys(uint localID) + { + if (Scripts.ContainsKey(localID) == false) + return new List(); + + Dictionary Obj; + Scripts.TryGetValue(localID, out Obj); + + return new List(Obj.Keys); + } + + public IScript GetScript(uint localID, UUID itemID) + { + lock (scriptLock) + { + IScript Script = null; + + if (Scripts.ContainsKey(localID) == false) + return null; + + Dictionary Obj; + Scripts.TryGetValue(localID, out Obj); + if (Obj.ContainsKey(itemID) == false) + return null; + + // Get script + Obj.TryGetValue(itemID, out Script); + return Script; + } + } + + public void SetScript(uint localID, UUID itemID, IScript Script) + { + lock (scriptLock) + { + // Create object if it doesn't exist + if (Scripts.ContainsKey(localID) == false) + { + Scripts.Add(localID, new Dictionary()); + } + + // Delete script if it exists + Dictionary Obj; + Scripts.TryGetValue(localID, out Obj); + if (Obj.ContainsKey(itemID) == true) + Obj.Remove(itemID); + + // Add to object + Obj.Add(itemID, Script); + } + } + + public void RemoveScript(uint localID, UUID itemID) + { + if (localID == 0) + localID = GetLocalID(itemID); + + // Don't have that object? + if (Scripts.ContainsKey(localID) == false) + return; + + // Delete script if it exists + Dictionary Obj; + Scripts.TryGetValue(localID, out Obj); + if (Obj.ContainsKey(itemID) == true) + Obj.Remove(itemID); + } + + #endregion + + + public void ResetScript(uint localID, UUID itemID) + { + IScript s = GetScript(localID, itemID); + string script = s.Source; + StopScript(localID, itemID); + SceneObjectPart part = World.GetSceneObjectPart(localID); + part.GetInventoryItem(itemID).PermsMask = 0; + part.GetInventoryItem(itemID).PermsGranter = UUID.Zero; + StartScript(localID, itemID, script, s.StartParam, false); + } + + + #region Script serialization/deserialization + + public void GetSerializedScript(uint localID, UUID itemID) + { + // Serialize the script and return it + // Should not be a problem + FileStream fs = File.Create("SERIALIZED_SCRIPT_" + itemID); + BinaryFormatter b = new BinaryFormatter(); + b.Serialize(fs, GetScript(localID, itemID)); + fs.Close(); + } + + public void PutSerializedScript(uint localID, UUID itemID) + { + // Deserialize the script and inject it into an AppDomain + + // How to inject into an AppDomain? + } + + #endregion + + ///// + ///// If set to true then threads and stuff should try to make a graceful exit + ///// + //public bool PleaseShutdown + //{ + // get { return _PleaseShutdown; } + // set { _PleaseShutdown = value; } + //} + //private bool _PleaseShutdown = false; + + public DetectParams[] GetDetectParams(IScript script) + { + if (detparms.ContainsKey(script)) + return detparms[script]; + + return null; + } } } -- cgit v1.1