From fccb03227e3f541a4c2f4e0e619074e4c1fb55dd Mon Sep 17 00:00:00 2001 From: Justin Clark-Casey (justincc) Date: Tue, 15 Jan 2013 21:13:22 +0000 Subject: Instead of passing separate engine, part and item components to script APIs, pass down IScriptInstance instead. This is to allow the future co-operative script thread terminate feature to detect and act upon termination requests. This splits the assembly and state loading out from the ScriptInstance() constructor to a separate Load() method in order to facilititate continued script logic regression testing. --- .../ScriptEngine/Shared/Instance/ScriptInstance.cs | 52 ++++++++++++---------- 1 file changed, 29 insertions(+), 23 deletions(-) (limited to 'OpenSim/Region/ScriptEngine/Shared/Instance') diff --git a/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs index f172216..a2ff51b 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs @@ -157,9 +157,6 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public UUID AppDomain { get; set; } - /// - /// Scene part in which this script instance is contained. - /// public SceneObjectPart Part { get; private set; } public string PrimName { get; private set; } @@ -209,43 +206,52 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance 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); Engine = engine; Part = part; - ItemID = itemID; - AssetID = assetID; - PrimName = primName; - ScriptName = scriptName; - m_Assembly = assembly; + 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; + } - lock (Part.TaskInventory) - { - if (Part.TaskInventory.ContainsKey(ItemID)) - { - ScriptTask = Part.TaskInventory[ItemID]; - } - } + /// + /// Load the script from an assembly into an AppDomain. + /// + /// + /// + /// + public void Load(AppDomain dom, string assembly, StateSource stateSource) + { + m_Assembly = assembly; + m_stateSource = stateSource; ApiManager am = new ApiManager(); foreach (string api in am.GetApis()) { m_Apis[api] = am.CreateApi(api); - m_Apis[api].Initialize(engine, part, ScriptTask); + m_Apis[api].Initialize(this); } try @@ -279,7 +285,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance // // m_log.Debug("[Script] Script instance created"); - part.SetScriptEvents(ItemID, + Part.SetScriptEvents(ItemID, (int)m_Script.GetStateEventFlags(State)); } catch (Exception e) -- cgit v1.1 From 1b5c41c14ad11325be249ea1cce3c65d4d6a89be Mon Sep 17 00:00:00 2001 From: Justin Clark-Casey (justincc) Date: Wed, 16 Jan 2013 00:12:40 +0000 Subject: Implement co-operative script termination if termination comes during a script wait event (llSleep(), etc.) This makes use of EventWaitHandles since various web references indicate that Thread.Interrupt() can also cause runtime instability. If co-op termination is enabled, then termination sets the wait handle instead of waiting for a timeout before possibly aborting the thread. This allows the script to cleanly terminate if it's in a llSleep/LL function delay or the next time it enters such a wait without any timeout period. Co-op termination is not yet testable since checking for termination request within loops that never trigger a wait is not yet implemented. --- .../ScriptEngine/Shared/Instance/ScriptInstance.cs | 52 ++++++- .../Shared/Instance/Tests/CoopTerminationTests.cs | 157 +++++++++++++++++++++ 2 files changed, 206 insertions(+), 3 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/ScriptInstance.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs index a2ff51b..00048a1 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs @@ -200,6 +200,10 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public static readonly long MaxMeasurementPeriod = 30 * TimeSpan.TicksPerMinute; + public bool CoopTermination { get; private set; } + + public EventWaitHandle CoopSleepHandle { get; private set; } + public void ClearQueue() { m_TimerQueued = false; @@ -233,6 +237,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_postOnRez = postOnRez; m_AttachedAvatar = Part.ParentGroup.AttachedAvatar; m_RegionID = Part.ParentGroup.Scene.RegionInfo.RegionID; + + if (Engine.Config.GetString("ScriptStopStrategy", "abort") == "co-op") + { + CoopTermination = true; + CoopSleepHandle = new AutoResetEvent(false); + } } /// @@ -532,9 +542,34 @@ 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 (!CoopTermination) + { + // If we're not co-operative terminating then try and wait for the event to complete before stopping + if (workItem.Wait(new TimeSpan((long)timeout * 100000))) + return true; + } + else + { + 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. + 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(TimeSpan.MaxValue)) + { + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Co-operatively stopped script {0} {1} in {2} {3}", + ScriptName, ItemID, PrimName, ObjectID); + + return true; + } + } } lock (EventQueue) @@ -547,6 +582,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( @@ -786,7 +822,11 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_InEvent = false; m_CurrentEvent = String.Empty; - if ((!(e is TargetInvocationException) || (!(e.InnerException is SelfDeleteException) && !(e.InnerException is ScriptDeleteException))) && !(e is ThreadAbortException)) + if ((!(e is TargetInvocationException) + || (!(e.InnerException is SelfDeleteException) + && !(e.InnerException is ScriptDeleteException) + && !(e.InnerException is ScriptCoopStopException))) + && !(e is ThreadAbortException)) { try { @@ -834,6 +874,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_InSelfDelete = true; Part.Inventory.RemoveInventoryItem(ItemID); } + else if ((e is TargetInvocationException) && (e.InnerException is ScriptCoopStopException)) + { + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Script {0}.{1} in event {2}, state {3} stopped co-operatively.", + PrimName, ScriptName, data.EventName, State); + } } } } 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..f3a6cc9 --- /dev/null +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs @@ -0,0 +1,157 @@ +/* + * 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; +using OpenSim.Tests.Common.Mock; + +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 = new AutoResetEvent(false); + private AutoResetEvent m_stoppedEvent = new AutoResetEvent(false); + + private OSChatMessage m_osChatMessageReceived; + + [TestFixtureSetUp] + public void Init() + { + //AppDomain.CurrentDomain.SetData("APPBASE", Environment.CurrentDirectory + "/bin"); +// Console.WriteLine(AppDomain.CurrentDomain.BaseDirectory); + m_xEngine = new OpenSim.Region.ScriptEngine.XEngine.XEngine(); + + 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"); + + m_scene = new SceneHelpers().SetupScene("My Test", UUID.Random(), 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(); + + UUID userId = TestHelpers.ParseTail(0x1); +// UUID objectId = TestHelpers.ParseTail(0x100); +// UUID itemId = TestHelpers.ParseTail(0x3); + string itemName = "TestStopOnObjectDerezLongSleep() Item"; + + SceneObjectGroup so = SceneHelpers.CreateSceneObject(1, userId, "TestStopOnObjectDerezLongSleep", 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; + + SceneObjectPart partWhereRezzed = m_scene.RezNewScript(userId, itemTemplate, +@"default +{ + state_entry() + { + llSay(0, ""Thin Lizzy""); + llSleep(60); + } +}"); + + TaskInventoryItem rezzedItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); + + // Wait for the script to start the event before we try stopping it. + m_chatEvent.WaitOne(60000); + + Console.WriteLine("Script started with message [{0}]", m_osChatMessageReceived.Message); + + // 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 sleep 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; + m_chatEvent.Set(); + } + } +} \ No newline at end of file -- cgit v1.1 From b8949024bc55c62b9268b35d4f2a568760b9d7d3 Mon Sep 17 00:00:00 2001 From: Justin Clark-Casey (justincc) Date: Wed, 16 Jan 2013 01:45:09 +0000 Subject: Revert "Implement co-operative script termination if termination comes during a script wait event (llSleep(), etc.)" Doing this as a favour to Melanie. This will be back with passing the wait handles directly to the api. This reverts commit 1b5c41c14ad11325be249ea1cce3c65d4d6a89be. --- .../ScriptEngine/Shared/Instance/ScriptInstance.cs | 52 +------ .../Shared/Instance/Tests/CoopTerminationTests.cs | 157 --------------------- 2 files changed, 3 insertions(+), 206 deletions(-) delete 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/ScriptInstance.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs index 00048a1..a2ff51b 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs @@ -200,10 +200,6 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public static readonly long MaxMeasurementPeriod = 30 * TimeSpan.TicksPerMinute; - public bool CoopTermination { get; private set; } - - public EventWaitHandle CoopSleepHandle { get; private set; } - public void ClearQueue() { m_TimerQueued = false; @@ -237,12 +233,6 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_postOnRez = postOnRez; m_AttachedAvatar = Part.ParentGroup.AttachedAvatar; m_RegionID = Part.ParentGroup.Scene.RegionInfo.RegionID; - - if (Engine.Config.GetString("ScriptStopStrategy", "abort") == "co-op") - { - CoopTermination = true; - CoopSleepHandle = new AutoResetEvent(false); - } } /// @@ -542,34 +532,9 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance } // Wait for the current event to complete. - if (!m_InSelfDelete) + if (!m_InSelfDelete && workItem.Wait(new TimeSpan((long)timeout * 100000))) { - if (!CoopTermination) - { - // If we're not co-operative terminating then try and wait for the event to complete before stopping - if (workItem.Wait(new TimeSpan((long)timeout * 100000))) - return true; - } - else - { - 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. - 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(TimeSpan.MaxValue)) - { - m_log.DebugFormat( - "[SCRIPT INSTANCE]: Co-operatively stopped script {0} {1} in {2} {3}", - ScriptName, ItemID, PrimName, ObjectID); - - return true; - } - } + return true; } lock (EventQueue) @@ -582,7 +547,6 @@ 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( @@ -822,11 +786,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_InEvent = false; m_CurrentEvent = String.Empty; - if ((!(e is TargetInvocationException) - || (!(e.InnerException is SelfDeleteException) - && !(e.InnerException is ScriptDeleteException) - && !(e.InnerException is ScriptCoopStopException))) - && !(e is ThreadAbortException)) + if ((!(e is TargetInvocationException) || (!(e.InnerException is SelfDeleteException) && !(e.InnerException is ScriptDeleteException))) && !(e is ThreadAbortException)) { try { @@ -874,12 +834,6 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_InSelfDelete = true; Part.Inventory.RemoveInventoryItem(ItemID); } - else if ((e is TargetInvocationException) && (e.InnerException is ScriptCoopStopException)) - { - m_log.DebugFormat( - "[SCRIPT INSTANCE]: Script {0}.{1} in event {2}, state {3} stopped co-operatively.", - PrimName, ScriptName, data.EventName, State); - } } } } diff --git a/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs deleted file mode 100644 index f3a6cc9..0000000 --- a/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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; -using OpenSim.Tests.Common.Mock; - -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 = new AutoResetEvent(false); - private AutoResetEvent m_stoppedEvent = new AutoResetEvent(false); - - private OSChatMessage m_osChatMessageReceived; - - [TestFixtureSetUp] - public void Init() - { - //AppDomain.CurrentDomain.SetData("APPBASE", Environment.CurrentDirectory + "/bin"); -// Console.WriteLine(AppDomain.CurrentDomain.BaseDirectory); - m_xEngine = new OpenSim.Region.ScriptEngine.XEngine.XEngine(); - - 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"); - - m_scene = new SceneHelpers().SetupScene("My Test", UUID.Random(), 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(); - - UUID userId = TestHelpers.ParseTail(0x1); -// UUID objectId = TestHelpers.ParseTail(0x100); -// UUID itemId = TestHelpers.ParseTail(0x3); - string itemName = "TestStopOnObjectDerezLongSleep() Item"; - - SceneObjectGroup so = SceneHelpers.CreateSceneObject(1, userId, "TestStopOnObjectDerezLongSleep", 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; - - SceneObjectPart partWhereRezzed = m_scene.RezNewScript(userId, itemTemplate, -@"default -{ - state_entry() - { - llSay(0, ""Thin Lizzy""); - llSleep(60); - } -}"); - - TaskInventoryItem rezzedItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); - - // Wait for the script to start the event before we try stopping it. - m_chatEvent.WaitOne(60000); - - Console.WriteLine("Script started with message [{0}]", m_osChatMessageReceived.Message); - - // 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 sleep 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; - m_chatEvent.Set(); - } - } -} \ No newline at end of file -- cgit v1.1 From 0963ece25bdef16852f5fd8ae4515a2f05d8b6e4 Mon Sep 17 00:00:00 2001 From: Justin Clark-Casey (justincc) Date: Wed, 16 Jan 2013 02:07:43 +0000 Subject: Implement co-operative script termination if termination comes during a script wait event (llSleep(), etc.) This makes use of EventWaitHandles since various web references indicate that Thread.Interrupt() can also cause runtime instability. If co-op termination is enabled, then termination sets the wait handle instead of waiting for a timeout before possibly aborting the thread. This allows the script to cleanly terminate if it's in a llSleep/LL function delay or the next time it enters such a wait without any timeout period. Co-op termination is not yet testable since checking for termination request within loops that never trigger a wait is not yet implemented. This commit, unlike 1b5c41c, passes the wait handle as an extra parameter through IScript.Initialize() instead of passing IScriptInstance itself. --- .../ScriptEngine/Shared/Instance/ScriptInstance.cs | 54 ++++++- .../Shared/Instance/Tests/CoopTerminationTests.cs | 157 +++++++++++++++++++++ 2 files changed, 207 insertions(+), 4 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/ScriptInstance.cs b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs index a2ff51b..75aea2b 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/ScriptInstance.cs @@ -200,6 +200,10 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance public static readonly long MaxMeasurementPeriod = 30 * TimeSpan.TicksPerMinute; + private bool m_coopTermination; + + private EventWaitHandle m_coopSleepHandle; + public void ClearQueue() { m_TimerQueued = false; @@ -233,6 +237,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_postOnRez = postOnRez; m_AttachedAvatar = Part.ParentGroup.AttachedAvatar; m_RegionID = Part.ParentGroup.Scene.RegionInfo.RegionID; + + if (Engine.Config.GetString("ScriptStopStrategy", "abort") == "co-op") + { + m_coopTermination = true; + m_coopSleepHandle = new AutoResetEvent(false); + } } /// @@ -251,7 +261,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance foreach (string api in am.GetApis()) { m_Apis[api] = am.CreateApi(api); - m_Apis[api].Initialize(this); + m_Apis[api].Initialize(Engine, Part, ScriptTask, m_coopSleepHandle); } try @@ -532,9 +542,34 @@ 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(new TimeSpan((long)timeout * 100000))) + return true; + } + else + { + 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(TimeSpan.MaxValue)) + { + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Co-operatively stopped script {0} {1} in {2} {3}", + ScriptName, ItemID, PrimName, ObjectID); + + return true; + } + } } lock (EventQueue) @@ -547,6 +582,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( @@ -786,7 +822,11 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_InEvent = false; m_CurrentEvent = String.Empty; - if ((!(e is TargetInvocationException) || (!(e.InnerException is SelfDeleteException) && !(e.InnerException is ScriptDeleteException))) && !(e is ThreadAbortException)) + if ((!(e is TargetInvocationException) + || (!(e.InnerException is SelfDeleteException) + && !(e.InnerException is ScriptDeleteException) + && !(e.InnerException is ScriptCoopStopException))) + && !(e is ThreadAbortException)) { try { @@ -834,6 +874,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance m_InSelfDelete = true; Part.Inventory.RemoveInventoryItem(ItemID); } + else if ((e is TargetInvocationException) && (e.InnerException is ScriptCoopStopException)) + { + m_log.DebugFormat( + "[SCRIPT INSTANCE]: Script {0}.{1} in event {2}, state {3} stopped co-operatively.", + PrimName, ScriptName, data.EventName, State); + } } } } 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..8c3e9e0 --- /dev/null +++ b/OpenSim/Region/ScriptEngine/Shared/Instance/Tests/CoopTerminationTests.cs @@ -0,0 +1,157 @@ +/* + * 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; +using OpenSim.Tests.Common.Mock; + +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 = new AutoResetEvent(false); + private AutoResetEvent m_stoppedEvent = new AutoResetEvent(false); + + private OSChatMessage m_osChatMessageReceived; + + [TestFixtureSetUp] + public void Init() + { + //AppDomain.CurrentDomain.SetData("APPBASE", Environment.CurrentDirectory + "/bin"); +// Console.WriteLine(AppDomain.CurrentDomain.BaseDirectory); + m_xEngine = new OpenSim.Region.ScriptEngine.XEngine.XEngine(); + + 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"); + + m_scene = new SceneHelpers().SetupScene("My Test", UUID.Random(), 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(); + + UUID userId = TestHelpers.ParseTail(0x1); +// UUID objectId = TestHelpers.ParseTail(0x100); +// UUID itemId = TestHelpers.ParseTail(0x3); + string itemName = "TestStopOnObjectDerezLongSleep() Item"; + + SceneObjectGroup so = SceneHelpers.CreateSceneObject(1, userId, "TestStopOnObjectDerezLongSleep", 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; + + SceneObjectPart partWhereRezzed = m_scene.RezNewScript(userId, itemTemplate, +@"default +{ + state_entry() + { + llSay(0, ""Thin Lizzy""); + llSleep(60); + } +}"); + + TaskInventoryItem rezzedItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); + + // Wait for the script to start the event before we try stopping it. + m_chatEvent.WaitOne(60000); + + Console.WriteLine("Script started with message [{0}]", m_osChatMessageReceived.Message); + + // 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 sleep 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; + m_chatEvent.Set(); + } + } +} \ No newline at end of file -- cgit v1.1