diff options
author | Justin Clark-Casey (justincc) | 2013-01-16 00:12:40 +0000 |
---|---|---|
committer | Justin Clark-Casey (justincc) | 2013-01-16 00:12:40 +0000 |
commit | 1b5c41c14ad11325be249ea1cce3c65d4d6a89be (patch) | |
tree | 987e140c9402c48cc8daf59d2b8af165646cc93a /OpenSim/Region/ScriptEngine/Shared | |
parent | Instead of passing separate engine, part and item components to script APIs, ... (diff) | |
download | opensim-SC_OLD-1b5c41c14ad11325be249ea1cce3c65d4d6a89be.zip opensim-SC_OLD-1b5c41c14ad11325be249ea1cce3c65d4d6a89be.tar.gz opensim-SC_OLD-1b5c41c14ad11325be249ea1cce3c65d4d6a89be.tar.bz2 opensim-SC_OLD-1b5c41c14ad11325be249ea1cce3c65d4d6a89be.tar.xz |
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.
Diffstat (limited to 'OpenSim/Region/ScriptEngine/Shared')
4 files changed, 248 insertions, 10 deletions
diff --git a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/LSL_Api.cs b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/LSL_Api.cs index 44072c6..b992efa 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/LSL_Api.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/LSL_Api.cs | |||
@@ -83,6 +83,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api | |||
83 | public class LSL_Api : MarshalByRefObject, ILSL_Api, IScriptApi | 83 | public class LSL_Api : MarshalByRefObject, ILSL_Api, IScriptApi |
84 | { | 84 | { |
85 | private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | 85 | private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
86 | |||
87 | /// <summary> | ||
88 | /// Instance of this script. | ||
89 | /// </summary> | ||
90 | protected IScriptInstance m_scriptInstance; | ||
91 | |||
86 | protected IScriptEngine m_ScriptEngine; | 92 | protected IScriptEngine m_ScriptEngine; |
87 | protected SceneObjectPart m_host; | 93 | protected SceneObjectPart m_host; |
88 | 94 | ||
@@ -112,11 +118,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api | |||
112 | 118 | ||
113 | public void Initialize(IScriptInstance scriptInstance) | 119 | public void Initialize(IScriptInstance scriptInstance) |
114 | { | 120 | { |
115 | m_ScriptEngine = scriptInstance.Engine; | 121 | m_scriptInstance = scriptInstance; |
116 | m_host = scriptInstance.Part; | 122 | m_ScriptEngine = m_scriptInstance.Engine; |
117 | m_item = scriptInstance.ScriptTask; | 123 | m_host = m_scriptInstance.Part; |
124 | m_item = m_scriptInstance.ScriptTask; | ||
118 | 125 | ||
119 | LoadLimits(); // read script limits from config. | 126 | LoadConfig(); |
120 | 127 | ||
121 | m_TransferModule = | 128 | m_TransferModule = |
122 | m_ScriptEngine.World.RequestModuleInterface<IMessageTransferModule>(); | 129 | m_ScriptEngine.World.RequestModuleInterface<IMessageTransferModule>(); |
@@ -129,7 +136,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api | |||
129 | /// <summary> | 136 | /// <summary> |
130 | /// Load configuration items that affect script, object and run-time behavior. */ | 137 | /// Load configuration items that affect script, object and run-time behavior. */ |
131 | /// </summary> | 138 | /// </summary> |
132 | private void LoadLimits() | 139 | private void LoadConfig() |
133 | { | 140 | { |
134 | m_ScriptDelayFactor = | 141 | m_ScriptDelayFactor = |
135 | m_ScriptEngine.Config.GetFloat("ScriptDelayFactor", 1.0f); | 142 | m_ScriptEngine.Config.GetFloat("ScriptDelayFactor", 1.0f); |
@@ -175,7 +182,16 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api | |||
175 | delay = (int)((float)delay * m_ScriptDelayFactor); | 182 | delay = (int)((float)delay * m_ScriptDelayFactor); |
176 | if (delay == 0) | 183 | if (delay == 0) |
177 | return; | 184 | return; |
178 | System.Threading.Thread.Sleep(delay); | 185 | |
186 | Sleep(delay); | ||
187 | } | ||
188 | |||
189 | protected virtual void Sleep(int delay) | ||
190 | { | ||
191 | if (!m_scriptInstance.CoopTermination) | ||
192 | System.Threading.Thread.Sleep(delay); | ||
193 | else if (m_scriptInstance.CoopSleepHandle.WaitOne(delay)) | ||
194 | throw new ScriptCoopStopException(); | ||
179 | } | 195 | } |
180 | 196 | ||
181 | public Scene World | 197 | public Scene World |
@@ -2914,7 +2930,8 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api | |||
2914 | { | 2930 | { |
2915 | // m_log.Info("llSleep snoozing " + sec + "s."); | 2931 | // m_log.Info("llSleep snoozing " + sec + "s."); |
2916 | m_host.AddScriptLPS(1); | 2932 | m_host.AddScriptLPS(1); |
2917 | Thread.Sleep((int)(sec * 1000)); | 2933 | |
2934 | Sleep((int)(sec * 1000)); | ||
2918 | } | 2935 | } |
2919 | 2936 | ||
2920 | public LSL_Float llGetMass() | 2937 | public LSL_Float llGetMass() |
diff --git a/OpenSim/Region/ScriptEngine/Shared/Helpers.cs b/OpenSim/Region/ScriptEngine/Shared/Helpers.cs index 5a58f73..e02d35e 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Helpers.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Helpers.cs | |||
@@ -81,6 +81,24 @@ namespace OpenSim.Region.ScriptEngine.Shared | |||
81 | } | 81 | } |
82 | } | 82 | } |
83 | 83 | ||
84 | /// <summary> | ||
85 | /// Used to signal when the script is stopping in co-operation with the script engine | ||
86 | /// (instead of through Thread.Abort()). | ||
87 | /// </summary> | ||
88 | [Serializable] | ||
89 | public class ScriptCoopStopException : Exception | ||
90 | { | ||
91 | public ScriptCoopStopException() | ||
92 | { | ||
93 | } | ||
94 | |||
95 | protected ScriptCoopStopException( | ||
96 | SerializationInfo info, | ||
97 | StreamingContext context) | ||
98 | { | ||
99 | } | ||
100 | } | ||
101 | |||
84 | public class DetectParams | 102 | public class DetectParams |
85 | { | 103 | { |
86 | public const int AGENT = 1; | 104 | public const int AGENT = 1; |
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 | |||
200 | 200 | ||
201 | public static readonly long MaxMeasurementPeriod = 30 * TimeSpan.TicksPerMinute; | 201 | public static readonly long MaxMeasurementPeriod = 30 * TimeSpan.TicksPerMinute; |
202 | 202 | ||
203 | public bool CoopTermination { get; private set; } | ||
204 | |||
205 | public EventWaitHandle CoopSleepHandle { get; private set; } | ||
206 | |||
203 | public void ClearQueue() | 207 | public void ClearQueue() |
204 | { | 208 | { |
205 | m_TimerQueued = false; | 209 | m_TimerQueued = false; |
@@ -233,6 +237,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance | |||
233 | m_postOnRez = postOnRez; | 237 | m_postOnRez = postOnRez; |
234 | m_AttachedAvatar = Part.ParentGroup.AttachedAvatar; | 238 | m_AttachedAvatar = Part.ParentGroup.AttachedAvatar; |
235 | m_RegionID = Part.ParentGroup.Scene.RegionInfo.RegionID; | 239 | m_RegionID = Part.ParentGroup.Scene.RegionInfo.RegionID; |
240 | |||
241 | if (Engine.Config.GetString("ScriptStopStrategy", "abort") == "co-op") | ||
242 | { | ||
243 | CoopTermination = true; | ||
244 | CoopSleepHandle = new AutoResetEvent(false); | ||
245 | } | ||
236 | } | 246 | } |
237 | 247 | ||
238 | /// <summary> | 248 | /// <summary> |
@@ -532,9 +542,34 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance | |||
532 | } | 542 | } |
533 | 543 | ||
534 | // Wait for the current event to complete. | 544 | // Wait for the current event to complete. |
535 | if (!m_InSelfDelete && workItem.Wait(new TimeSpan((long)timeout * 100000))) | 545 | if (!m_InSelfDelete) |
536 | { | 546 | { |
537 | return true; | 547 | if (!CoopTermination) |
548 | { | ||
549 | // If we're not co-operative terminating then try and wait for the event to complete before stopping | ||
550 | if (workItem.Wait(new TimeSpan((long)timeout * 100000))) | ||
551 | return true; | ||
552 | } | ||
553 | else | ||
554 | { | ||
555 | m_log.DebugFormat( | ||
556 | "[SCRIPT INSTANCE]: Co-operatively stopping script {0} {1} in {2} {3}", | ||
557 | ScriptName, ItemID, PrimName, ObjectID); | ||
558 | |||
559 | // This will terminate the event on next handle check by the script. | ||
560 | CoopSleepHandle.Set(); | ||
561 | |||
562 | // For now, we will wait forever since the event should always cleanly terminate once LSL loop | ||
563 | // checking is implemented. May want to allow a shorter timeout option later. | ||
564 | if (workItem.Wait(TimeSpan.MaxValue)) | ||
565 | { | ||
566 | m_log.DebugFormat( | ||
567 | "[SCRIPT INSTANCE]: Co-operatively stopped script {0} {1} in {2} {3}", | ||
568 | ScriptName, ItemID, PrimName, ObjectID); | ||
569 | |||
570 | return true; | ||
571 | } | ||
572 | } | ||
538 | } | 573 | } |
539 | 574 | ||
540 | lock (EventQueue) | 575 | lock (EventQueue) |
@@ -547,6 +582,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance | |||
547 | 582 | ||
548 | // If the event still hasn't stopped and we the stop isn't the result of script or object removal, then | 583 | // If the event still hasn't stopped and we the stop isn't the result of script or object removal, then |
549 | // forcibly abort the work item (this aborts the underlying thread). | 584 | // forcibly abort the work item (this aborts the underlying thread). |
585 | // Co-operative termination should never reach this point. | ||
550 | if (!m_InSelfDelete) | 586 | if (!m_InSelfDelete) |
551 | { | 587 | { |
552 | m_log.DebugFormat( | 588 | m_log.DebugFormat( |
@@ -786,7 +822,11 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance | |||
786 | m_InEvent = false; | 822 | m_InEvent = false; |
787 | m_CurrentEvent = String.Empty; | 823 | m_CurrentEvent = String.Empty; |
788 | 824 | ||
789 | if ((!(e is TargetInvocationException) || (!(e.InnerException is SelfDeleteException) && !(e.InnerException is ScriptDeleteException))) && !(e is ThreadAbortException)) | 825 | if ((!(e is TargetInvocationException) |
826 | || (!(e.InnerException is SelfDeleteException) | ||
827 | && !(e.InnerException is ScriptDeleteException) | ||
828 | && !(e.InnerException is ScriptCoopStopException))) | ||
829 | && !(e is ThreadAbortException)) | ||
790 | { | 830 | { |
791 | try | 831 | try |
792 | { | 832 | { |
@@ -834,6 +874,12 @@ namespace OpenSim.Region.ScriptEngine.Shared.Instance | |||
834 | m_InSelfDelete = true; | 874 | m_InSelfDelete = true; |
835 | Part.Inventory.RemoveInventoryItem(ItemID); | 875 | Part.Inventory.RemoveInventoryItem(ItemID); |
836 | } | 876 | } |
877 | else if ((e is TargetInvocationException) && (e.InnerException is ScriptCoopStopException)) | ||
878 | { | ||
879 | m_log.DebugFormat( | ||
880 | "[SCRIPT INSTANCE]: Script {0}.{1} in event {2}, state {3} stopped co-operatively.", | ||
881 | PrimName, ScriptName, data.EventName, State); | ||
882 | } | ||
837 | } | 883 | } |
838 | } | 884 | } |
839 | } | 885 | } |
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 @@ | |||
1 | /* | ||
2 | * Copyright (c) Contributors, http://opensimulator.org/ | ||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. | ||
4 | * | ||
5 | * Redistribution and use in source and binary forms, with or without | ||
6 | * modification, are permitted provided that the following conditions are met: | ||
7 | * * Redistributions of source code must retain the above copyright | ||
8 | * notice, this list of conditions and the following disclaimer. | ||
9 | * * Redistributions in binary form must reproduce the above copyright | ||
10 | * notice, this list of conditions and the following disclaimer in the | ||
11 | * documentation and/or other materials provided with the distribution. | ||
12 | * * Neither the name of the OpenSimulator Project nor the | ||
13 | * names of its contributors may be used to endorse or promote products | ||
14 | * derived from this software without specific prior written permission. | ||
15 | * | ||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY | ||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY | ||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
26 | */ | ||
27 | |||
28 | using System; | ||
29 | using System.Collections.Generic; | ||
30 | using System.Threading; | ||
31 | using Nini.Config; | ||
32 | using NUnit.Framework; | ||
33 | using OpenMetaverse; | ||
34 | using OpenSim.Framework; | ||
35 | using OpenSim.Region.CoreModules.Scripting.WorldComm; | ||
36 | using OpenSim.Region.Framework.Scenes; | ||
37 | using OpenSim.Region.Framework.Interfaces; | ||
38 | using OpenSim.Region.ScriptEngine.XEngine; | ||
39 | using OpenSim.Tests.Common; | ||
40 | using OpenSim.Tests.Common.Mock; | ||
41 | |||
42 | namespace OpenSim.Region.ScriptEngine.Shared.Instance.Tests | ||
43 | { | ||
44 | /// <summary> | ||
45 | /// Test that co-operative script thread termination is working correctly. | ||
46 | /// </summary> | ||
47 | [TestFixture] | ||
48 | public class CoopTerminationTests : OpenSimTestCase | ||
49 | { | ||
50 | private TestScene m_scene; | ||
51 | private OpenSim.Region.ScriptEngine.XEngine.XEngine m_xEngine; | ||
52 | |||
53 | private AutoResetEvent m_chatEvent = new AutoResetEvent(false); | ||
54 | private AutoResetEvent m_stoppedEvent = new AutoResetEvent(false); | ||
55 | |||
56 | private OSChatMessage m_osChatMessageReceived; | ||
57 | |||
58 | [TestFixtureSetUp] | ||
59 | public void Init() | ||
60 | { | ||
61 | //AppDomain.CurrentDomain.SetData("APPBASE", Environment.CurrentDirectory + "/bin"); | ||
62 | // Console.WriteLine(AppDomain.CurrentDomain.BaseDirectory); | ||
63 | m_xEngine = new OpenSim.Region.ScriptEngine.XEngine.XEngine(); | ||
64 | |||
65 | IniConfigSource configSource = new IniConfigSource(); | ||
66 | |||
67 | IConfig startupConfig = configSource.AddConfig("Startup"); | ||
68 | startupConfig.Set("DefaultScriptEngine", "XEngine"); | ||
69 | |||
70 | IConfig xEngineConfig = configSource.AddConfig("XEngine"); | ||
71 | xEngineConfig.Set("Enabled", "true"); | ||
72 | xEngineConfig.Set("StartDelay", "0"); | ||
73 | |||
74 | // These tests will not run with AppDomainLoading = true, at least on mono. For unknown reasons, the call | ||
75 | // to AssemblyResolver.OnAssemblyResolve fails. | ||
76 | xEngineConfig.Set("AppDomainLoading", "false"); | ||
77 | |||
78 | xEngineConfig.Set("ScriptStopStrategy", "co-op"); | ||
79 | |||
80 | m_scene = new SceneHelpers().SetupScene("My Test", UUID.Random(), 1000, 1000, configSource); | ||
81 | SceneHelpers.SetupSceneModules(m_scene, configSource, m_xEngine); | ||
82 | m_scene.StartScripts(); | ||
83 | } | ||
84 | |||
85 | /// <summary> | ||
86 | /// Test co-operative termination on derez of an object containing a script with a long-running event. | ||
87 | /// </summary> | ||
88 | /// <remarks> | ||
89 | /// TODO: Actually compiling the script is incidental to this test. Really want a way to compile test scripts | ||
90 | /// within the build itself. | ||
91 | /// </remarks> | ||
92 | [Test] | ||
93 | public void TestStopOnLongSleep() | ||
94 | { | ||
95 | TestHelpers.InMethod(); | ||
96 | TestHelpers.EnableLogging(); | ||
97 | |||
98 | UUID userId = TestHelpers.ParseTail(0x1); | ||
99 | // UUID objectId = TestHelpers.ParseTail(0x100); | ||
100 | // UUID itemId = TestHelpers.ParseTail(0x3); | ||
101 | string itemName = "TestStopOnObjectDerezLongSleep() Item"; | ||
102 | |||
103 | SceneObjectGroup so = SceneHelpers.CreateSceneObject(1, userId, "TestStopOnObjectDerezLongSleep", 0x100); | ||
104 | m_scene.AddNewSceneObject(so, true); | ||
105 | |||
106 | InventoryItemBase itemTemplate = new InventoryItemBase(); | ||
107 | // itemTemplate.ID = itemId; | ||
108 | itemTemplate.Name = itemName; | ||
109 | itemTemplate.Folder = so.UUID; | ||
110 | itemTemplate.InvType = (int)InventoryType.LSL; | ||
111 | |||
112 | m_scene.EventManager.OnChatFromWorld += OnChatFromWorld; | ||
113 | |||
114 | SceneObjectPart partWhereRezzed = m_scene.RezNewScript(userId, itemTemplate, | ||
115 | @"default | ||
116 | { | ||
117 | state_entry() | ||
118 | { | ||
119 | llSay(0, ""Thin Lizzy""); | ||
120 | llSleep(60); | ||
121 | } | ||
122 | }"); | ||
123 | |||
124 | TaskInventoryItem rezzedItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); | ||
125 | |||
126 | // Wait for the script to start the event before we try stopping it. | ||
127 | m_chatEvent.WaitOne(60000); | ||
128 | |||
129 | Console.WriteLine("Script started with message [{0}]", m_osChatMessageReceived.Message); | ||
130 | |||
131 | // FIXME: This is a very poor way of trying to avoid a low-probability race condition where the script | ||
132 | // executes llSay() but has not started the sleep before we try to stop it. | ||
133 | Thread.Sleep(1000); | ||
134 | |||
135 | // We need a way of carrying on if StopScript() fail, since it won't return if the script isn't actually | ||
136 | // stopped. This kind of multi-threading is far from ideal in a regression test. | ||
137 | new Thread(() => { m_xEngine.StopScript(rezzedItem.ItemID); m_stoppedEvent.Set(); }).Start(); | ||
138 | |||
139 | if (!m_stoppedEvent.WaitOne(30000)) | ||
140 | Assert.Fail("Script did not co-operatively stop."); | ||
141 | |||
142 | bool running; | ||
143 | TaskInventoryItem scriptItem = partWhereRezzed.Inventory.GetInventoryItem(itemName); | ||
144 | Assert.That( | ||
145 | SceneObjectPartInventory.TryGetScriptInstanceRunning(m_scene, scriptItem, out running), Is.True); | ||
146 | Assert.That(running, Is.False); | ||
147 | } | ||
148 | |||
149 | private void OnChatFromWorld(object sender, OSChatMessage oscm) | ||
150 | { | ||
151 | // Console.WriteLine("Got chat [{0}]", oscm.Message); | ||
152 | |||
153 | m_osChatMessageReceived = oscm; | ||
154 | m_chatEvent.Set(); | ||
155 | } | ||
156 | } | ||
157 | } \ No newline at end of file | ||