From 134f86e8d5c414409631b25b8c6f0ee45fbd8631 Mon Sep 17 00:00:00 2001 From: David Walter Seikel Date: Thu, 3 Nov 2016 21:44:39 +1000 Subject: Initial update to OpenSim 0.8.2.1 source code. --- .../Shared/Instance/Properties/AssemblyInfo.cs | 4 +- .../ScriptEngine/Shared/Instance/ScriptInstance.cs | 806 +++++++++++++-------- .../Shared/Instance/Tests/CoopTerminationTests.cs | 513 +++++++++++++ 3 files changed, 1003 insertions(+), 320 deletions(-) create mode 100644 OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs (limited to 'OpenSim/Region/ScriptEngine/Shared/Instance') diff --git a/OpenSim/Region/ScriptEngine/Shared/Instance/Properties/AssemblyInfo.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/Properties/AssemblyInfo.cs index 470e1a1..b7c4bab 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Instance/Properties/AssemblyInfo.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("0.7.5.*")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("0.8.3.*")] + diff --git a/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs index 01a5e34..fa6e6fc 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs @@ -51,6 +51,7 @@ using OpenSim.Region.ScriptEngine.Shared.Api.Runtime; using OpenSim.Region.ScriptEngine.Shared.ScriptBase; using OpenSim.Region.ScriptEngine.Shared.CodeTools; using OpenSim.Region.ScriptEngine.Interfaces; +using System.Diagnostics; namespace OpenSim.Region.ScriptEngine.Shared.Instance { @@ -58,6 +59,8 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + public bool StatePersistedHere { get { return m_AttachedAvatar == UUID.Zero; } } + /// /// The current work item if an event for this script is running or waiting to run, /// @@ -71,14 +74,16 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance private bool m_TimerQueued; private DateTime m_EventStart; private bool m_InEvent; - private string m_Assembly; + private string m_assemblyPath; + private string m_dataPath; private string m_CurrentEvent = String.Empty; private bool m_InSelfDelete; private int m_MaxScriptQueue; - private bool m_SaveState = true; + private bool m_SaveState; private int m_ControlEventsInQueue; private int m_LastControlLevel; private bool m_CollisionInQueue; + private bool m_StateChangeInProgress; // The following is for setting a minimum delay between events private double m_minEventDelay; @@ -95,6 +100,9 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public int DebugLevel { get; set; } + public WaitHandle CoopWaitHandle { get; private set; } + public Stopwatch ExecutionTimer { get; private set; } + public Dictionary, KeyValuePair> LineMap { get; set; } private Dictionary m_Apis = new Dictionary(); @@ -121,7 +129,18 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance } } - public bool Running { get; set; } + public bool Running + { + get { return m_running; } + + set + { + m_running = value; + if (m_running) + StayStopped = false; + } + } + private bool m_running; public bool Suspended { @@ -153,23 +172,27 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public string State { get; set; } + public bool StayStopped { get; set; } + public IScriptEngine Engine { get; private set; } public UUID AppDomain { get; set; } + public SceneObjectPart Part { get; private set; } + public string PrimName { get; private set; } public string ScriptName { get; private set; } public UUID ItemID { get; private set; } - public UUID ObjectID { get; private set; } + public UUID ObjectID { get { return Part.UUID; } } - public uint LocalID { get; private set; } + public uint LocalID { get { return Part.LocalId; } } - public UUID RootObjectID { get; private set; } + public UUID RootObjectID { get { return Part.ParentGroup.UUID; } } - public uint RootLocalID { get; private set; } + public uint RootLocalID { get { return Part.ParentGroup.LocalId; } } public UUID AssetID { get; private set; } @@ -192,83 +215,92 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public DateTime TimeStarted { get; private set; } - public long MeasurementPeriodTickStart { get; private set; } + public MetricsCollectorTime ExecutionTime { get; private set; } - public long MeasurementPeriodExecutionTime { get; private set; } + private static readonly int MeasurementWindow = 30 * 1000; // show the *recent* time used by the script, to find currently active scripts - public static readonly long MaxMeasurementPeriod = 30 * TimeSpan.TicksPerMinute; + private bool m_coopTermination; + + private EventWaitHandle m_coopSleepHandle; public void ClearQueue() { m_TimerQueued = false; + m_StateChangeInProgress = false; EventQueue.Clear(); } - public ScriptInstance(IScriptEngine engine, SceneObjectPart part, - UUID itemID, UUID assetID, string assembly, - AppDomain dom, string primName, string scriptName, - int startParam, bool postOnRez, StateSource stateSource, - int maxScriptQueue) + public ScriptInstance( + IScriptEngine engine, SceneObjectPart part, TaskInventoryItem item, + int startParam, bool postOnRez, + int maxScriptQueue) { State = "default"; EventQueue = new Queue(32); + ExecutionTimer = new Stopwatch(); Engine = engine; - LocalID = part.LocalId; - ObjectID = part.UUID; - RootLocalID = part.ParentGroup.LocalId; - RootObjectID = part.ParentGroup.UUID; - ItemID = itemID; - AssetID = assetID; - PrimName = primName; - ScriptName = scriptName; - m_Assembly = assembly; + Part = part; + ScriptTask = item; + + // This is currently only here to allow regression tests to get away without specifying any inventory + // item when they are testing script logic that doesn't require an item. + if (ScriptTask != null) + { + ScriptName = ScriptTask.Name; + ItemID = ScriptTask.ItemID; + AssetID = ScriptTask.AssetID; + } + + PrimName = part.ParentGroup.Name; StartParam = startParam; m_MaxScriptQueue = maxScriptQueue; - m_stateSource = stateSource; m_postOnRez = postOnRez; - m_AttachedAvatar = part.ParentGroup.AttachedAvatar; - m_RegionID = part.ParentGroup.Scene.RegionInfo.RegionID; + m_AttachedAvatar = Part.ParentGroup.AttachedAvatar; + m_RegionID = Part.ParentGroup.Scene.RegionInfo.RegionID; - if (part != null) - { - lock (part.TaskInventory) - { - if (part.TaskInventory.ContainsKey(ItemID)) - { - ScriptTask = part.TaskInventory[ItemID]; - } - } - } + m_SaveState = StatePersistedHere; + + ExecutionTime = new MetricsCollectorTime(MeasurementWindow, 10); + +// m_log.DebugFormat( +// "[SCRIPT INSTANCE]: Instantiated script instance {0} (id {1}) in part {2} (id {3}) in object {4} attached avatar {5} in {6}", +// ScriptTask.Name, ScriptTask.ItemID, Part.Name, Part.UUID, Part.ParentGroup.Name, m_AttachedAvatar, Engine.World.Name); + } + + /// + /// Load the script from an assembly into an AppDomain. + /// + /// + /// + /// + /// Path for all script associated data (state, etc.). In a multi-region set up + /// with all scripts loading into the same AppDomain this may not be the same place as the DLL itself. + /// + /// + /// false if load failed, true if suceeded + public bool Load( + IScript script, EventWaitHandle coopSleepHandle, string assemblyPath, + string dataPath, StateSource stateSource, bool coopTermination) + { + m_Script = script; + m_coopSleepHandle = coopSleepHandle; + m_assemblyPath = assemblyPath; + m_dataPath = dataPath; + m_stateSource = stateSource; + m_coopTermination = coopTermination; + + if (m_coopTermination) + CoopWaitHandle = coopSleepHandle; + else + CoopWaitHandle = null; ApiManager am = new ApiManager(); foreach (string api in am.GetApis()) { m_Apis[api] = am.CreateApi(api); - m_Apis[api].Initialize(engine, part, ScriptTask); - } - - try - { - if (dom != System.AppDomain.CurrentDomain) - m_Script = (IScript)dom.CreateInstanceAndUnwrap( - Path.GetFileNameWithoutExtension(assembly), - "SecondLife.Script"); - else - m_Script = (IScript)Assembly.Load( - Path.GetFileNameWithoutExtension(assembly)).CreateInstance( - "SecondLife.Script"); - - //ILease lease = (ILease)RemotingServices.GetLifetimeService(m_Script as ScriptBaseClass); - //RemotingServices.GetLifetimeService(m_Script as ScriptBaseClass); -// lease.Register(this); - } - catch (Exception e) - { - m_log.ErrorFormat( - "[SCRIPT INSTANCE]: Error loading assembly {0}. Exception {1}{2}", - assembly, e.Message, e.StackTrace); + m_Apis[api].Initialize(Engine, Part, ScriptTask); } try @@ -278,26 +310,28 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_Script.InitApi(kv.Key, kv.Value); } -// // m_log.Debug("[Script] Script instance created"); + // // m_log.Debug("[Script] Script instance created"); - part.SetScriptEvents(ItemID, - (int)m_Script.GetStateEventFlags(State)); + Part.SetScriptEvents(ItemID, (int)m_Script.GetStateEventFlags(State)); } catch (Exception e) { m_log.ErrorFormat( - "[SCRIPT INSTANCE]: Error loading script instance from assembly {0}. Exception {1}{2}", - assembly, e.Message, e.StackTrace); + "[SCRIPT INSTANCE]: Not starting script {0} (id {1}) in part {2} (id {3}) in object {4} in {5}. Error initializing script instance. Exception {6}{7}", + ScriptTask.Name, ScriptTask.ItemID, Part.Name, Part.UUID, Part.ParentGroup.Name, Engine.World.Name, e.Message, e.StackTrace); - return; + return false; } - m_SaveState = true; + // For attachments, XEngine saves the state into a .state file when XEngine.SetXMLState() is called. + string savedState = Path.Combine(m_dataPath, ItemID.ToString() + ".state"); - string savedState = Path.Combine(Path.GetDirectoryName(assembly), - ItemID.ToString() + ".state"); if (File.Exists(savedState)) { + // m_log.DebugFormat( + // "[SCRIPT INSTANCE]: Found state for script {0} for {1} ({2}) at {3} in {4}", + // ItemID, savedState, Part.Name, Part.ParentGroup.Name, Part.ParentGroup.Scene.Name); + string xml = String.Empty; try @@ -317,13 +351,13 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance ScriptSerializer.Deserialize(xml, this); AsyncCommandManager.CreateFromData(Engine, - LocalID, ItemID, ObjectID, - PluginData); + LocalID, ItemID, ObjectID, + PluginData); -// m_log.DebugFormat("[Script] Successfully retrieved state for script {0}.{1}", PrimName, m_ScriptName); + // m_log.DebugFormat("[Script] Successfully retrieved state for script {0}.{1}", PrimName, m_ScriptName); - part.SetScriptEvents(ItemID, - (int)m_Script.GetStateEventFlags(State)); + Part.SetScriptEvents(ItemID, + (int)m_Script.GetStateEventFlags(State)); if (!Running) m_startOnInit = false; @@ -338,29 +372,33 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_SaveState = false; m_startedFromSavedState = true; } + + // If this script is in an attachment then we no longer need the state file. + if (!StatePersistedHere) + RemoveState(); } - else - { - m_log.WarnFormat( - "[SCRIPT INSTANCE]: Unable to load script state file {0} for script {1} {2} in {3} {4} (assembly {5}). Memory limit exceeded", - savedState, ScriptName, ItemID, PrimName, ObjectID, assembly); - } + // else + // { + // m_log.WarnFormat( + // "[SCRIPT INSTANCE]: Not starting script {0} (id {1}) in part {2} (id {3}) in object {4} in {5}. Unable to load script state file {6}. Memory limit exceeded.", + // ScriptTask.Name, ScriptTask.ItemID, Part.Name, Part.UUID, Part.ParentGroup.Name, Engine.World.Name, savedState); + // } } catch (Exception e) { - m_log.ErrorFormat( - "[SCRIPT INSTANCE]: Unable to load script state file {0} for script {1} {2} in {3} {4} (assembly {5}). XML is {6}. Exception {7}{8}", - savedState, ScriptName, ItemID, PrimName, ObjectID, assembly, xml, e.Message, e.StackTrace); + m_log.ErrorFormat( + "[SCRIPT INSTANCE]: Not starting script {0} (id {1}) in part {2} (id {3}) in object {4} in {5}. Unable to load script state file {6}. XML is {7}. Exception {8}{9}", + ScriptTask.Name, ScriptTask.ItemID, Part.Name, Part.UUID, Part.ParentGroup.Name, Engine.World.Name, savedState, xml, e.Message, e.StackTrace); } } -// else -// { -// ScenePresence presence = Engine.World.GetScenePresence(part.OwnerID); + // else + // { + // m_log.DebugFormat( + // "[SCRIPT INSTANCE]: Did not find state for script {0} for {1} ({2}) at {3} in {4}", + // ItemID, savedState, Part.Name, Part.ParentGroup.Name, Part.ParentGroup.Scene.Name); + // } -// if (presence != null && (!postOnRez)) -// presence.ControllingClient.SendAgentAlertMessage("Compile successful", false); - -// } + return true; } public void Init() @@ -418,33 +456,27 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance PostEvent(new EventParams("attach", new object[] { new LSL_Types.LSLString(m_AttachedAvatar.ToString()) }, new DetectParams[0])); } - } } private void ReleaseControls() { - SceneObjectPart part = Engine.World.GetSceneObjectPart(LocalID); - - if (part != null) + int permsMask; + UUID permsGranter; + lock (Part.TaskInventory) { - int permsMask; - UUID permsGranter; - lock (part.TaskInventory) - { - if (!part.TaskInventory.ContainsKey(ItemID)) - return; + if (!Part.TaskInventory.ContainsKey(ItemID)) + return; - permsGranter = part.TaskInventory[ItemID].PermsGranter; - permsMask = part.TaskInventory[ItemID].PermsMask; - } + permsGranter = Part.TaskInventory[ItemID].PermsGranter; + permsMask = Part.TaskInventory[ItemID].PermsMask; + } - if ((permsMask & ScriptBaseClass.PERMISSION_TAKE_CONTROLS) != 0) - { - ScenePresence presence = Engine.World.GetScenePresence(permsGranter); - if (presence != null) - presence.UnRegisterControlEventsToScript(LocalID, ItemID); - } + if ((permsMask & ScriptBaseClass.PERMISSION_TAKE_CONTROLS) != 0) + { + ScenePresence presence = Engine.World.GetScenePresence(permsGranter); + if (presence != null) + presence.UnRegisterControlEventsToScript(LocalID, ItemID); } } @@ -456,15 +488,23 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public void RemoveState() { - string savedState = Path.Combine(Path.GetDirectoryName(m_Assembly), - ItemID.ToString() + ".state"); + string savedState = Path.Combine(m_dataPath, ItemID.ToString() + ".state"); + +// m_log.DebugFormat( +// "[SCRIPT INSTANCE]: Deleting state {0} for script {1} (id {2}) in part {3} (id {4}) in object {5} in {6}.", +// savedState, ScriptTask.Name, ScriptTask.ItemID, Part.Name, Part.UUID, Part.ParentGroup.Name, Engine.World.Name); try { File.Delete(savedState); } - catch(Exception) + catch (Exception e) { + m_log.Warn( + string.Format( + "[SCRIPT INSTANCE]: Could not delete script state {0} for script {1} (id {2}) in part {3} (id {4}) in object {5} in {6}. Exception ", + savedState, ScriptTask.Name, ScriptTask.ItemID, Part.Name, Part.UUID, Part.ParentGroup.Name, Engine.World.Name), + e); } } @@ -487,8 +527,10 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance Running = true; TimeStarted = DateTime.Now; - MeasurementPeriodTickStart = Util.EnvironmentTickCount(); - MeasurementPeriodExecutionTime = 0; + + // Note: we don't reset ExecutionTime. The reason is that runaway scripts are stopped and restarted + // automatically, and we *do* want to show that they had high CPU in that case. If we had reset + // ExecutionTime here then runaway scripts, paradoxically, would never show up in the "Top Scripts" dialog. if (EventQueue.Count > 0) { @@ -500,16 +542,20 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance } } - public bool Stop(int timeout) + public bool Stop(int timeout, bool clearEventQueue = false) { -// m_log.DebugFormat( -// "[SCRIPT INSTANCE]: Stopping script {0} {1} in {2} {3} with timeout {4} {5} {6}", -// ScriptName, ItemID, PrimName, ObjectID, timeout, m_InSelfDelete, DateTime.Now.Ticks); + if (DebugLevel >= 1) + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Stopping script {0} {1} in {2} {3} with timeout {4} {5} {6}", + ScriptName, ItemID, PrimName, ObjectID, timeout, m_InSelfDelete, DateTime.Now.Ticks); IScriptWorkItem workItem; lock (EventQueue) { + if (clearEventQueue) + ClearQueue(); + if (!Running) return true; @@ -533,9 +579,36 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance } // Wait for the current event to complete. - if (!m_InSelfDelete && workItem.Wait(new TimeSpan((long)timeout * 100000))) + if (!m_InSelfDelete) { - return true; + if (!m_coopTermination) + { + // If we're not co-operative terminating then try and wait for the event to complete before stopping + if (workItem.Wait(timeout)) + return true; + } + else + { + if (DebugLevel >= 1) + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Co-operatively stopping script {0} {1} in {2} {3}", + ScriptName, ItemID, PrimName, ObjectID); + + // This will terminate the event on next handle check by the script. + m_coopSleepHandle.Set(); + + // For now, we will wait forever since the event should always cleanly terminate once LSL loop + // checking is implemented. May want to allow a shorter timeout option later. + if (workItem.Wait(Timeout.Infinite)) + { + if (DebugLevel >= 1) + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Co-operatively stopped script {0} {1} in {2} {3}", + ScriptName, ItemID, PrimName, ObjectID); + + return true; + } + } } lock (EventQueue) @@ -548,6 +621,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance // If the event still hasn't stopped and we the stop isn't the result of script or object removal, then // forcibly abort the work item (this aborts the underlying thread). + // Co-operative termination should never reach this point. if (!m_InSelfDelete) { m_log.DebugFormat( @@ -570,12 +644,32 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance if (state == State) return; - PostEvent(new EventParams("state_exit", new Object[0], - new DetectParams[0])); - PostEvent(new EventParams("state", new Object[] { state }, - new DetectParams[0])); - PostEvent(new EventParams("state_entry", new Object[0], - new DetectParams[0])); + EventParams lastTimerEv = null; + + lock (EventQueue) + { + // Remove all queued events, remembering the last timer event + while (EventQueue.Count > 0) + { + EventParams tempv = (EventParams)EventQueue.Dequeue(); + if (tempv.EventName == "timer") lastTimerEv = tempv; + } + + // Post events + PostEvent(new EventParams("state_exit", new Object[0], + new DetectParams[0])); + PostEvent(new EventParams("state", new Object[] { state }, + new DetectParams[0])); + PostEvent(new EventParams("state_entry", new Object[0], + new DetectParams[0])); + + // Requeue the timer event after the state changing events + if (lastTimerEv != null) EventQueue.Enqueue(lastTimerEv); + + // This will stop events from being queued and processed + // until the new state is started + m_StateChangeInProgress = true; + } throw new EventAbortException(); } @@ -607,6 +701,10 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance lock (EventQueue) { + // The only events that persist across state changes are timers + if (m_StateChangeInProgress && data.EventName != "timer") + return; + if (EventQueue.Count >= m_MaxScriptQueue) return; @@ -677,196 +775,222 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance if (Suspended) return 0; - EventParams data = null; + ExecutionTimer.Restart(); + + try + { + return EventProcessorInt(); + } + finally + { + ExecutionTimer.Stop(); + ExecutionTime.AddSample(ExecutionTimer); + Part.ParentGroup.Scene.AddScriptExecutionTime(ExecutionTimer.ElapsedTicks); + } + } + } + + private object EventProcessorInt() + { + EventParams data = null; - lock (EventQueue) + lock (EventQueue) + { + data = (EventParams)EventQueue.Dequeue(); + if (data == null) // Shouldn't happen { - data = (EventParams)EventQueue.Dequeue(); - if (data == null) // Shouldn't happen + if (EventQueue.Count > 0 && Running && !ShuttingDown) { - if (EventQueue.Count > 0 && Running && !ShuttingDown) - { - m_CurrentWorkItem = Engine.QueueEventHandler(this); - } - else - { - m_CurrentWorkItem = null; - } - return 0; + m_CurrentWorkItem = Engine.QueueEventHandler(this); } - - if (data.EventName == "timer") - m_TimerQueued = false; - if (data.EventName == "control") + else { - if (m_ControlEventsInQueue > 0) - m_ControlEventsInQueue--; + m_CurrentWorkItem = null; } - if (data.EventName == "collision") - m_CollisionInQueue = false; + return 0; } - SceneObjectPart part = Engine.World.GetSceneObjectPart(LocalID); + if (data.EventName == "timer") + m_TimerQueued = false; + if (data.EventName == "control") + { + if (m_ControlEventsInQueue > 0) + m_ControlEventsInQueue--; + } + if (data.EventName == "collision") + m_CollisionInQueue = false; + } - if (DebugLevel >= 2) + if (DebugLevel >= 2) + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Processing event {0} for {1}/{2}({3})/{4}({5}) @ {6}/{7}", + data.EventName, + ScriptName, + Part.Name, + Part.LocalId, + Part.ParentGroup.Name, + Part.ParentGroup.UUID, + Part.AbsolutePosition, + Part.ParentGroup.Scene.Name); + + m_DetectParams = data.DetectParams; + + if (data.EventName == "state") // Hardcoded state change + { + State = data.Params[0].ToString(); + + if (DebugLevel >= 1) m_log.DebugFormat( - "[SCRIPT INSTANCE]: Processing event {0} for {1}/{2}({3})/{4}({5}) @ {6}/{7}", - data.EventName, - ScriptName, - part.Name, - part.LocalId, - part.ParentGroup.Name, - part.ParentGroup.UUID, - part.AbsolutePosition, - part.ParentGroup.Scene.Name); - - m_DetectParams = data.DetectParams; - - if (data.EventName == "state") // Hardcoded state change + "[SCRIPT INSTANCE]: Changing state to {0} for {1}/{2}({3})/{4}({5}) @ {6}/{7}", + State, + ScriptName, + Part.Name, + Part.LocalId, + Part.ParentGroup.Name, + Part.ParentGroup.UUID, + Part.AbsolutePosition, + Part.ParentGroup.Scene.Name); + + AsyncCommandManager.StateChange(Engine, + LocalID, ItemID); + // we are effectively in the new state now, so we can resume queueing + // and processing other non-timer events + m_StateChangeInProgress = false; + + Part.SetScriptEvents(ItemID, (int)m_Script.GetStateEventFlags(State)); + } + else + { + if (Engine.World.PipeEventsForScript(LocalID) || + data.EventName == "control") // Don't freeze avies! { - State = data.Params[0].ToString(); + // m_log.DebugFormat("[Script] Delivered event {2} in state {3} to {0}.{1}", + // PrimName, ScriptName, data.EventName, State); - if (DebugLevel >= 1) - m_log.DebugFormat( - "[SCRIPT INSTANCE]: Changing state to {0} for {1}/{2}({3})/{4}({5}) @ {6}/{7}", - State, - ScriptName, - part.Name, - part.LocalId, - part.ParentGroup.Name, - part.ParentGroup.UUID, - part.AbsolutePosition, - part.ParentGroup.Scene.Name); - - AsyncCommandManager.RemoveScript(Engine, - LocalID, ItemID); - - if (part != null) + try { - part.SetScriptEvents(ItemID, - (int)m_Script.GetStateEventFlags(State)); - } - } - else - { - if (Engine.World.PipeEventsForScript(LocalID) || - data.EventName == "control") // Don't freeze avies! - { - // m_log.DebugFormat("[Script] Delivered event {2} in state {3} to {0}.{1}", - // PrimName, ScriptName, data.EventName, State); + m_CurrentEvent = data.EventName; + m_EventStart = DateTime.Now; + m_InEvent = true; try { - m_CurrentEvent = data.EventName; - m_EventStart = DateTime.Now; - m_InEvent = true; - - int start = Util.EnvironmentTickCount(); - - // Reset the measurement period when we reach the end of the current one. - if (start - MeasurementPeriodTickStart > MaxMeasurementPeriod) - MeasurementPeriodTickStart = start; - m_Script.ExecuteEvent(State, data.EventName, data.Params); - - MeasurementPeriodExecutionTime += Util.EnvironmentTickCount() - start; - + } + finally + { m_InEvent = false; m_CurrentEvent = String.Empty; + } - if (m_SaveState) - { - // This will be the very first event we deliver - // (state_entry) in default state - // - SaveState(m_Assembly); + if (m_SaveState) + { + // This will be the very first event we deliver + // (state_entry) in default state + // + SaveState(); - m_SaveState = false; - } + m_SaveState = false; } - catch (Exception e) + } + catch (Exception e) + { + // m_log.DebugFormat( + // "[SCRIPT] Exception in script {0} {1}: {2}{3}", + // ScriptName, ItemID, e.Message, e.StackTrace); + + if ((!(e is TargetInvocationException) + || (!(e.InnerException is SelfDeleteException) + && !(e.InnerException is ScriptDeleteException) + && !(e.InnerException is ScriptCoopStopException))) + && !(e is ThreadAbortException)) { -// m_log.DebugFormat( -// "[SCRIPT] Exception in script {0} {1}: {2}{3}", -// ScriptName, ItemID, e.Message, e.StackTrace); - - m_InEvent = false; - m_CurrentEvent = String.Empty; - - if ((!(e is TargetInvocationException) || (!(e.InnerException is SelfDeleteException) && !(e.InnerException is ScriptDeleteException))) && !(e is ThreadAbortException)) + try { - try - { - // DISPLAY ERROR INWORLD - string text = FormatException(e); - - if (text.Length > 1000) - text = text.Substring(0, 1000); - Engine.World.SimChat(Utils.StringToBytes(text), - ChatTypeEnum.DebugChannel, 2147483647, - part.AbsolutePosition, - part.Name, part.UUID, false); - - - m_log.DebugFormat( - "[SCRIPT INSTANCE]: Runtime error in script {0}, part {1} {2} at {3} in {4}, displayed error {5}, actual exception {6}", - ScriptName, - PrimName, - part.UUID, - part.AbsolutePosition, - part.ParentGroup.Scene.Name, - text.Replace("\n", "\\n"), - e.InnerException); - } - catch (Exception) - { - } - // catch (Exception e2) // LEGIT: User Scripting - // { - // m_log.Error("[SCRIPT]: "+ - // "Error displaying error in-world: " + - // e2.ToString()); - // m_log.Error("[SCRIPT]: " + - // "Errormessage: Error compiling script:\r\n" + - // e.ToString()); - // } + // DISPLAY ERROR INWORLD + string text = FormatException(e); + + if (text.Length > 1000) + text = text.Substring(0, 1000); + Engine.World.SimChat(Utils.StringToBytes(text), + ChatTypeEnum.DebugChannel, 2147483647, + Part.AbsolutePosition, + Part.Name, Part.UUID, false); + + + m_log.Debug(string.Format( + "[SCRIPT INSTANCE]: Runtime error in script {0} (event {1}), part {2} {3} at {4} in {5} ", + ScriptName, + data.EventName, + PrimName, + Part.UUID, + Part.AbsolutePosition, + Part.ParentGroup.Scene.Name), + e); } - else if ((e is TargetInvocationException) && (e.InnerException is SelfDeleteException)) + catch (Exception) { - m_InSelfDelete = true; - if (part != null) - Engine.World.DeleteSceneObject(part.ParentGroup, false); - } - else if ((e is TargetInvocationException) && (e.InnerException is ScriptDeleteException)) - { - m_InSelfDelete = true; - if (part != null) - part.Inventory.RemoveInventoryItem(ItemID); } + // catch (Exception e2) // LEGIT: User Scripting + // { + // m_log.Error("[SCRIPT]: "+ + // "Error displaying error in-world: " + + // e2.ToString()); + // m_log.Error("[SCRIPT]: " + + // "Errormessage: Error compiling script:\r\n" + + // e.ToString()); + // } + } + else if ((e is TargetInvocationException) && (e.InnerException is SelfDeleteException)) + { + m_InSelfDelete = true; + Engine.World.DeleteSceneObject(Part.ParentGroup, false); + } + else if ((e is TargetInvocationException) && (e.InnerException is ScriptDeleteException)) + { + m_InSelfDelete = true; + Part.Inventory.RemoveInventoryItem(ItemID); + } + else if ((e is TargetInvocationException) && (e.InnerException is ScriptCoopStopException)) + { + if (DebugLevel >= 1) + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Script {0}.{1} in event {2}, state {3} stopped co-operatively.", + PrimName, ScriptName, data.EventName, State); } } } + } - // If there are more events and we are currently running and not shutting down, then ask the - // script engine to run the next event. - lock (EventQueue) + // If there are more events and we are currently running and not shutting down, then ask the + // script engine to run the next event. + lock (EventQueue) + { + // Increase processed events counter and prevent wrap; + if (++EventsProcessed == 1000000) + EventsProcessed = 100000; + + if ((EventsProcessed % 100000) == 0 && DebugLevel > 0) { - EventsProcessed++; + m_log.DebugFormat("[SCRIPT INSTANCE]: Script \"{0}\" (Object \"{1}\" {2} @ {3}.{4}, Item ID {5}, Asset {6}) in event {7}: processed {8:n0} script events", + ScriptTask.Name, + Part.ParentGroup.Name, Part.ParentGroup.UUID, Part.ParentGroup.AbsolutePosition, Part.ParentGroup.Scene.Name, + ScriptTask.ItemID, ScriptTask.AssetID, data.EventName, EventsProcessed); + } - if (EventQueue.Count > 0 && Running && !ShuttingDown) - { - m_CurrentWorkItem = Engine.QueueEventHandler(this); - } - else - { - m_CurrentWorkItem = null; - } + if (EventQueue.Count > 0 && Running && !ShuttingDown) + { + m_CurrentWorkItem = Engine.QueueEventHandler(this); } + else + { + m_CurrentWorkItem = null; + } + } - m_DetectParams = null; + m_DetectParams = null; - return 0; - } + return 0; } public int EventTime() @@ -888,19 +1012,21 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance ReleaseControls(); Stop(timeout); - SceneObjectPart part = Engine.World.GetSceneObjectPart(LocalID); - part.Inventory.GetInventoryItem(ItemID).PermsMask = 0; - part.Inventory.GetInventoryItem(ItemID).PermsGranter = UUID.Zero; + Part.Inventory.GetInventoryItem(ItemID).PermsMask = 0; + Part.Inventory.GetInventoryItem(ItemID).PermsGranter = UUID.Zero; AsyncCommandManager.RemoveScript(Engine, LocalID, ItemID); EventQueue.Clear(); m_Script.ResetVars(); + StartParam = 0; State = "default"; - part.SetScriptEvents(ItemID, + Part.SetScriptEvents(ItemID, (int)m_Script.GetStateEventFlags(State)); if (running) Start(); - m_SaveState = true; + + m_SaveState = StatePersistedHere; + PostEvent(new EventParams("state_entry", new Object[0], new DetectParams[0])); } @@ -913,21 +1039,22 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance ReleaseControls(); m_Script.ResetVars(); - SceneObjectPart part = Engine.World.GetSceneObjectPart(LocalID); - part.Inventory.GetInventoryItem(ItemID).PermsMask = 0; - part.Inventory.GetInventoryItem(ItemID).PermsGranter = UUID.Zero; + Part.Inventory.GetInventoryItem(ItemID).PermsMask = 0; + Part.Inventory.GetInventoryItem(ItemID).PermsGranter = UUID.Zero; AsyncCommandManager.RemoveScript(Engine, LocalID, ItemID); EventQueue.Clear(); m_Script.ResetVars(); + string oldState = State; + StartParam = 0; State = "default"; - part.SetScriptEvents(ItemID, + Part.SetScriptEvents(ItemID, (int)m_Script.GetStateEventFlags(State)); - if (m_CurrentEvent != "state_entry") + if (m_CurrentEvent != "state_entry" || oldState != "default") { - m_SaveState = true; + m_SaveState = StatePersistedHere; PostEvent(new EventParams("state_entry", new Object[0], new DetectParams[0])); throw new EventAbortException(); @@ -944,6 +1071,9 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public void SetVars(Dictionary vars) { +// foreach (KeyValuePair kvp in vars) +// m_log.DebugFormat("[SCRIPT INSTANCE]: Setting var {0}={1}", kvp.Key, kvp.Value); + m_Script.SetVars(vars); } @@ -967,42 +1097,63 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance return m_DetectParams[idx].Key; } - public void SaveState(string assembly) + public void SaveState() { - // If we're currently in an event, just tell it to save upon return - // - if (m_InEvent) - { - m_SaveState = true; + if (!Running && !StayStopped) return; - } + // We cannot call this inside the EventQueue lock since it will currently take AsyncCommandManager.staticLock. + // This may already be held by AsyncCommandManager.DoOneCmdHandlerPass() which in turn can take EventQueue + // lock via ScriptInstance.PostEvent(). PluginData = AsyncCommandManager.GetSerializationData(Engine, ItemID); - string xml = ScriptSerializer.Serialize(this); - - // Compare hash of the state we just just created with the state last written to disk - // If the state is different, update the disk file. - UUID hash = UUID.Parse(Utils.MD5String(xml)); - - if (hash != m_CurrentStateHash) + // We need to lock here to avoid any race with a thread that is removing this script. + lock (EventQueue) { - try + // Check again to avoid a race with a thread in Stop() + if (!Running && !StayStopped) + return; + + // If we're currently in an event, just tell it to save upon return + // + if (m_InEvent) { - FileStream fs = File.Create(Path.Combine(Path.GetDirectoryName(assembly), ItemID.ToString() + ".state")); - Byte[] buf = Util.UTF8NoBomEncoding.GetBytes(xml); - fs.Write(buf, 0, buf.Length); - fs.Close(); + m_SaveState = true; + return; } - catch(Exception) + + // m_log.DebugFormat( + // "[SCRIPT INSTANCE]: Saving state for script {0} (id {1}) in part {2} (id {3}) in object {4} in {5}", + // ScriptTask.Name, ScriptTask.ItemID, Part.Name, Part.UUID, Part.ParentGroup.Name, Engine.World.Name); + + string xml = ScriptSerializer.Serialize(this); + + // Compare hash of the state we just just created with the state last written to disk + // If the state is different, update the disk file. + UUID hash = UUID.Parse(Utils.MD5String(xml)); + + if (hash != m_CurrentStateHash) { - // m_log.Error("Unable to save xml\n"+e.ToString()); + try + { + using (FileStream fs = File.Create(Path.Combine(m_dataPath, ItemID.ToString() + ".state"))) + { + Byte[] buf = Util.UTF8NoBomEncoding.GetBytes(xml); + fs.Write(buf, 0, buf.Length); + } + } + catch(Exception) + { + // m_log.Error("Unable to save xml\n"+e.ToString()); + } + //if (!File.Exists(Path.Combine(Path.GetDirectoryName(assembly), ItemID.ToString() + ".state"))) + //{ + // throw new Exception("Completed persistence save, but no file was created"); + //} + m_CurrentStateHash = hash; } - //if (!File.Exists(Path.Combine(Path.GetDirectoryName(assembly), ItemID.ToString() + ".state"))) - //{ - // throw new Exception("Completed persistence save, but no file was created"); - //} - m_CurrentStateHash = hash; + + StayStopped = false; } } @@ -1072,7 +1223,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public string GetAssemblyName() { - return m_Assembly; + return m_assemblyPath; } public string GetXMLState() @@ -1109,4 +1260,23 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance Suspended = false; } } + + /// + /// Xengine event wait handle. + /// + /// + /// This class exists becase XEngineScriptBase gets a reference to this wait handle. We need to make sure that + /// when scripts are running in different AppDomains the lease does not expire. + /// FIXME: Like LSL_Api, etc., this effectively leaks memory since the GC will never collect it. To avoid this, + /// proper remoting sponsorship needs to be implemented across the board. + /// + public class XEngineEventWaitHandle : EventWaitHandle + { + public XEngineEventWaitHandle(bool initialState, EventResetMode mode) : base(initialState, mode) {} + + public override Object InitializeLifetimeService() + { + return null; + } + } } diff --git a/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs new file mode 100644 index 0000000..5b9794b --- /dev/null +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs @@ -0,0 +1,513 @@ +/* + * 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 OpenSimulator 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.Generic; +using System.Threading; +using Nini.Config; +using NUnit.Framework; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Region.CoreModules.Scripting.WorldComm; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.ScriptEngine.XEngine; +using OpenSim.Tests.Common; + +namespace OpenSim.Region.ScriptEngine.Shared.Instance.Tests +{ + /// + /// Test that co-operative script thread termination is working correctly. + /// + [TestFixture] + public class CoopTerminationTests : OpenSimTestCase + { + private TestScene m_scene; + private OpenSim.Region.ScriptEngine.XEngine.XEngine m_xEngine; + + private AutoResetEvent m_chatEvent; + private AutoResetEvent m_stoppedEvent; + + private OSChatMessage m_osChatMessageReceived; + + /// + /// Number of chat messages received so far. Reset before each test. + /// + private int m_chatMessagesReceived; + + /// + /// Number of chat messages expected. m_chatEvent is not fired until this number is reached or exceeded. + /// + private int m_chatMessagesThreshold; + + [SetUp] + public void Init() + { + m_osChatMessageReceived = null; + m_chatMessagesReceived = 0; + m_chatMessagesThreshold = 0; + m_chatEvent = new AutoResetEvent(false); + m_stoppedEvent = new AutoResetEvent(false); + + //AppDomain.CurrentDomain.SetData("APPBASE", Environment.CurrentDirectory + "/bin"); +// Console.WriteLine(AppDomain.CurrentDomain.BaseDirectory); + m_xEngine = new OpenSim.Region.ScriptEngine.XEngine.XEngine(); + m_xEngine.DebugLevel = 1; + + IniConfigSource configSource = new IniConfigSource(); + + IConfig startupConfig = configSource.AddConfig("Startup"); + startupConfig.Set("DefaultScriptEngine", "XEngine"); + + IConfig xEngineConfig = configSource.AddConfig("XEngine"); + xEngineConfig.Set("Enabled", "true"); + xEngineConfig.Set("StartDelay", "0"); + + // These tests will not run with AppDomainLoading = true, at least on mono. For unknown reasons, the call + // to AssemblyResolver.OnAssemblyResolve fails. + xEngineConfig.Set("AppDomainLoading", "false"); + + xEngineConfig.Set("ScriptStopStrategy", "co-op"); + + // Make sure loops aren't actually being terminated by a script delay wait. + xEngineConfig.Set("ScriptDelayFactor", 0); + + // This is really just set for debugging the test. + xEngineConfig.Set("WriteScriptSourceToDebugFile", true); + + // Set to false if we need to debug test so the old scripts don't get wiped before each separate test +// xEngineConfig.Set("DeleteScriptsOnStartup", false); + + // This is not currently used at all for co-op termination. Bumping up to demonstrate that co-op termination + // has an effect - without it tests will fail due to a 120 second wait for the event to finish. + xEngineConfig.Set("WaitForEventCompletionOnScriptStop", 120000); + + m_scene = new SceneHelpers().SetupScene("My Test", TestHelpers.ParseTail(0x9999), 1000, 1000, configSource); + SceneHelpers.SetupSceneModules(m_scene, configSource, m_xEngine); + m_scene.StartScripts(); + } + + /// + /// Test co-operative termination on derez of an object containing a script with a long-running event. + /// + /// + /// TODO: Actually compiling the script is incidental to this test. Really want a way to compile test scripts + /// within the build itself. + /// + [Test] + public void TestStopOnLongSleep() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + llSay(0, ""Thin Lizzy""); + llSleep(60); + } +}"; + + TestStop(script); + } + + [Test] + public void TestNoStopOnSingleStatementForLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + for (i = 0; i <= 1; i++) llSay(0, ""Iter "" + (string)i); + } +}"; + + TestSingleStatementNoStop(script); + } + + [Test] + public void TestStopOnLongSingleStatementForLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + for (i = 0; i < 2147483647; i++) llSay(0, ""Iter "" + (string)i); + } +}"; + + TestStop(script); + } + + [Test] + public void TestStopOnLongCompoundStatementForLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + for (i = 0; i < 2147483647; i++) + { + llSay(0, ""Iter "" + (string)i); + } + } +}"; + + TestStop(script); + } + + [Test] + public void TestNoStopOnSingleStatementWhileLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + while (i < 2) llSay(0, ""Iter "" + (string)i++); + } +}"; + + TestSingleStatementNoStop(script); + } + + [Test] + public void TestStopOnLongSingleStatementWhileLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + while (1 == 1) + llSay(0, ""Iter "" + (string)i++); + } +}"; + + TestStop(script); + } + + [Test] + public void TestStopOnLongCompoundStatementWhileLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + while (1 == 1) + { + llSay(0, ""Iter "" + (string)i++); + } + } +}"; + + TestStop(script); + } + + [Test] + public void TestNoStopOnSingleStatementDoWhileLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + + do llSay(0, ""Iter "" + (string)i++); + while (i < 2); + } +}"; + + TestSingleStatementNoStop(script); + } + + [Test] + public void TestStopOnLongSingleStatementDoWhileLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + do llSay(0, ""Iter "" + (string)i++); + while (1 == 1); + } +}"; + + TestStop(script); + } + + [Test] + public void TestStopOnLongCompoundStatementDoWhileLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + do + { + llSay(0, ""Iter "" + (string)i++); + } while (1 == 1); + } +}"; + + TestStop(script); + } + + [Test] + public void TestStopOnInfiniteJumpLoop() + { + TestHelpers.InMethod(); + TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + @p1; + llSay(0, ""Iter "" + (string)i++); + jump p1; + } +}"; + + TestStop(script); + } + + // Disabling for now as these are not particularly useful tests (since they fail due to stack overflow before + // termination can even be tried. +// [Test] + public void TestStopOnInfiniteUserFunctionCallLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@" +integer i = 0; + +ufn1() +{ + llSay(0, ""Iter ufn1() "" + (string)i++); + ufn1(); +} + +default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + ufn1(); + } +}"; + + TestStop(script); + } + + // Disabling for now as these are not particularly useful tests (since they fail due to stack overflow before + // termination can even be tried. +// [Test] + public void TestStopOnInfiniteManualEventCallLoop() + { + TestHelpers.InMethod(); +// TestHelpers.EnableLogging(); + + string script = +@"default +{ + state_entry() + { + integer i = 0; + llSay(0, ""Thin Lizzy""); + + llSay(0, ""Iter"" + (string)i++); + default_event_state_entry(); + } +}"; + + TestStop(script); + } + + private SceneObjectPart CreateScript(string script, string itemName, UUID userId) + { +// UUID objectId = TestHelpers.ParseTail(0x100); +// UUID itemId = TestHelpers.ParseTail(0x3); + + SceneObjectGroup so + = SceneHelpers.CreateSceneObject(1, userId, string.Format("Object for {0}", itemName), 0x100); + m_scene.AddNewSceneObject(so, true); + + InventoryItemBase itemTemplate = new InventoryItemBase(); +// itemTemplate.ID = itemId; + itemTemplate.Name = itemName; + itemTemplate.Folder = so.UUID; + itemTemplate.InvType = (int)InventoryType.LSL; + + m_scene.EventManager.OnChatFromWorld += OnChatFromWorld; + + return m_scene.RezNewScript(userId, itemTemplate, script); + } + + private void TestSingleStatementNoStop(string script) + { + // In these tests we expect to see at least 2 chat messages to confirm that the loop is working properly. + m_chatMessagesThreshold = 2; + + UUID userId = TestHelpers.ParseTail(0x1); +// UUID objectId = TestHelpers.ParseTail(0x100); +// UUID itemId = TestHelpers.ParseTail(0x3); + string itemName = "TestNoStop"; + + SceneObjectPart partWhereRezzed = CreateScript(script, itemName, userId); + + // Wait for the script to start the event before we try stopping it. + m_chatEvent.WaitOne(60000); + + if (m_osChatMessageReceived == null) + Assert.Fail("Script did not start"); + else + Assert.That(m_chatMessagesReceived, Is.EqualTo(2)); + + bool running; + TaskInventoryItem scriptItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); + Assert.That( + SceneObjectPartInventory.TryGetScriptInstanceRunning(m_scene, scriptItem, out running), Is.True); + Assert.That(running, Is.True); + } + + private void TestStop(string script) + { + // In these tests we're only interested in the first message to confirm that the script has started. + m_chatMessagesThreshold = 1; + + UUID userId = TestHelpers.ParseTail(0x1); +// UUID objectId = TestHelpers.ParseTail(0x100); +// UUID itemId = TestHelpers.ParseTail(0x3); + string itemName = "TestStop"; + + SceneObjectPart partWhereRezzed = CreateScript(script, itemName, userId); + TaskInventoryItem rezzedItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); + + // Wait for the script to start the event before we try stopping it. + m_chatEvent.WaitOne(60000); + + if (m_osChatMessageReceived != null) + Console.WriteLine("Script started with message [{0}]", m_osChatMessageReceived.Message); + else + Assert.Fail("Script did not start"); + + // FIXME: This is a very poor way of trying to avoid a low-probability race condition where the script + // executes llSay() but has not started the next statement before we try to stop it. + Thread.Sleep(1000); + + // We need a way of carrying on if StopScript() fail, since it won't return if the script isn't actually + // stopped. This kind of multi-threading is far from ideal in a regression test. + new Thread(() => { m_xEngine.StopScript(rezzedItem.ItemID); m_stoppedEvent.Set(); }).Start(); + + if (!m_stoppedEvent.WaitOne(30000)) + Assert.Fail("Script did not co-operatively stop."); + + bool running; + TaskInventoryItem scriptItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); + Assert.That( + SceneObjectPartInventory.TryGetScriptInstanceRunning(m_scene, scriptItem, out running), Is.True); + Assert.That(running, Is.False); + } + + private void OnChatFromWorld(object sender, OSChatMessage oscm) + { + Console.WriteLine("Got chat [{0}]", oscm.Message); + m_osChatMessageReceived = oscm; + + if (++m_chatMessagesReceived >= m_chatMessagesThreshold) + { + m_scene.EventManager.OnChatFromWorld -= OnChatFromWorld; + m_chatEvent.Set(); + } + } + } +} \ No newline at end of file -- cgit v1.1