Skip to content

Commit ca20d4a

Browse files
committed
Add thread safety to hook calls and subscriptions
1 parent 91420c4 commit ca20d4a

File tree

1 file changed

+110
-88
lines changed

1 file changed

+110
-88
lines changed

src/Plugins/PluginManager.cs

Lines changed: 110 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,41 @@ namespace Oxide.Core.Plugins
1111
/// </summary>
1212
public sealed class PluginManager
1313
{
14+
private enum SubscriptionChangeType : byte
15+
{
16+
Subscribe = 0,
17+
Unsubscribe = 1
18+
}
19+
20+
private struct SubscriptionChange
21+
{
22+
public Plugin Plugin { get; }
23+
24+
public SubscriptionChangeType Change { get; }
25+
26+
public SubscriptionChange(Plugin plugin, SubscriptionChangeType type)
27+
{
28+
Plugin = plugin;
29+
Change = type;
30+
}
31+
}
32+
33+
private class HookSubscriptions
34+
{
35+
public IList<Plugin> Plugins { get; }
36+
37+
public int CallDepth { get; set; }
38+
39+
public Queue<SubscriptionChange> PendingChanges { get; }
40+
41+
public HookSubscriptions()
42+
{
43+
Plugins = new List<Plugin>();
44+
PendingChanges = new Queue<SubscriptionChange>();
45+
CallDepth = 0;
46+
}
47+
}
48+
1449
/// <summary>
1550
/// Gets the logger to which this plugin manager writes
1651
/// </summary>
@@ -35,20 +70,11 @@ public sealed class PluginManager
3570
private readonly IDictionary<string, Plugin> loadedPlugins;
3671

3772
// All hook subscriptions
38-
private readonly IDictionary<string, IList<Plugin>> hookSubscriptions;
39-
40-
// The current depth of hook calls
41-
private int hookCallDepth;
73+
private readonly IDictionary<string, HookSubscriptions> hookSubscriptions;
4274

4375
// Stores the last time a deprecation warning was printed for a specific hook
4476
private readonly Dictionary<string, float> lastDeprecatedWarningAt = new Dictionary<string, float>();
4577

46-
// Pending hook subscriptions
47-
private readonly Dictionary<string, IList<Plugin>> pendingHookSubscriptions = new Dictionary<string, IList<Plugin>>();
48-
49-
// Pending hook unsubscriptions
50-
private readonly Dictionary<string, IList<Plugin>> pendingHookUnsubscriptions = new Dictionary<string, IList<Plugin>>();
51-
5278
// Re-usable conflict list used for hook calls
5379
private readonly List<string> hookConflicts = new List<string>();
5480

@@ -59,7 +85,7 @@ public PluginManager(Logger logger)
5985
{
6086
// Initialize
6187
loadedPlugins = new Dictionary<string, Plugin>();
62-
hookSubscriptions = new Dictionary<string, IList<Plugin>>();
88+
hookSubscriptions = new Dictionary<string, HookSubscriptions>();
6389
Logger = logger;
6490
}
6591

@@ -93,11 +119,15 @@ public bool RemovePlugin(Plugin plugin)
93119
}
94120

95121
loadedPlugins.Remove(plugin.Name);
96-
foreach (IList<Plugin> list in hookSubscriptions.Values)
122+
123+
lock (hookSubscriptions)
97124
{
98-
if (list.Contains(plugin))
125+
foreach (HookSubscriptions sub in hookSubscriptions.Values)
99126
{
100-
list.Remove(plugin);
127+
if (sub.Plugins.Contains(plugin))
128+
{
129+
sub.Plugins.Remove(plugin);
130+
}
101131
}
102132
}
103133

@@ -134,32 +164,27 @@ internal void SubscribeToHook(string hook, Plugin plugin)
134164
return;
135165
}
136166

137-
IList<Plugin> sublist;
138-
// Avoids modifying the plugin list while iterating over it during a hook call
139-
if (hookCallDepth > 0)
140-
{
141-
if (!pendingHookSubscriptions.TryGetValue(hook, out sublist))
142-
{
143-
sublist = new List<Plugin>();
144-
pendingHookSubscriptions.Add(hook, sublist);
145-
}
167+
HookSubscriptions sublist;
146168

147-
if (!sublist.Contains(plugin))
169+
lock (hookSubscriptions)
170+
{
171+
if (!hookSubscriptions.TryGetValue(hook, out sublist))
148172
{
149-
sublist.Add(plugin);
173+
sublist = new HookSubscriptions();
174+
hookSubscriptions[hook] = sublist;
150175
}
151-
152-
return;
153176
}
154177

155-
if (!hookSubscriptions.TryGetValue(hook, out sublist))
178+
// Avoids modifying the plugin list while iterating over it during a hook call
179+
if (sublist.CallDepth > 0)
156180
{
157-
sublist = new List<Plugin>();
158-
hookSubscriptions.Add(hook, sublist);
181+
sublist.PendingChanges.Enqueue(new SubscriptionChange(plugin, SubscriptionChangeType.Subscribe));
182+
return;
159183
}
160-
if (!sublist.Contains(plugin))
184+
185+
if (!sublist.Plugins.Contains(plugin))
161186
{
162-
sublist.Add(plugin);
187+
sublist.Plugins.Add(plugin);
163188
}
164189
//Logger.Write(LogType.Debug, $"Plugin {plugin.Name} is subscribing to hook '{hook}'!");
165190
}
@@ -176,27 +201,24 @@ internal void UnsubscribeToHook(string hook, Plugin plugin)
176201
return;
177202
}
178203

179-
IList<Plugin> sublist;
180-
// Avoids modifying the plugin list while iterating over it during a hook call
181-
if (hookCallDepth > 0)
182-
{
183-
if (!pendingHookUnsubscriptions.TryGetValue(hook, out sublist))
184-
{
185-
sublist = new List<Plugin>();
186-
pendingHookUnsubscriptions.Add(hook, sublist);
187-
}
204+
HookSubscriptions sublist;
188205

189-
if (!sublist.Contains(plugin))
206+
lock (hookSubscriptions)
207+
{
208+
if (!hookSubscriptions.TryGetValue(hook, out sublist))
190209
{
191-
sublist.Add(plugin);
210+
return;
192211
}
193-
194-
return;
195212
}
196-
if (hookSubscriptions.TryGetValue(hook, out sublist) && sublist.Contains(plugin))
213+
214+
// Avoids modifying the plugin list while iterating over it during a hook call
215+
if (sublist.CallDepth > 0)
197216
{
198-
sublist.Remove(plugin);
217+
sublist.PendingChanges.Enqueue(new SubscriptionChange(plugin, SubscriptionChangeType.Unsubscribe));
218+
return;
199219
}
220+
221+
sublist.Plugins.Remove(plugin);
200222
//Logger.Write(LogType.Debug, $"Plugin {plugin.Name} is unsubscribing to hook '{hook}'!");
201223
}
202224

@@ -208,36 +230,43 @@ internal void UnsubscribeToHook(string hook, Plugin plugin)
208230
/// <returns></returns>
209231
public object CallHook(string hook, params object[] args)
210232
{
233+
HookSubscriptions subscriptions;
234+
211235
// Locate the sublist
212-
if (!hookSubscriptions.TryGetValue(hook, out IList<Plugin> plugins))
236+
lock (hookSubscriptions)
213237
{
214-
return null;
238+
if (!hookSubscriptions.TryGetValue(hook, out subscriptions))
239+
{
240+
return null;
241+
}
215242
}
216243

217-
if (plugins.Count == 0)
244+
245+
if (subscriptions.Plugins.Count == 0)
218246
{
219247
return null;
220248
}
221249

222250
// Loop each item
223-
object[] values = ArrayPool.Get(plugins.Count);
251+
object[] values = ArrayPool.Get(subscriptions.Plugins.Count);
224252
int returnCount = 0;
225253
object finalValue = null;
226254
Plugin finalPlugin = null;
227255

228-
hookCallDepth++;
256+
subscriptions.CallDepth++;
229257

230258
try
231259
{
232-
for (int i = 0; i < plugins.Count; i++)
260+
for (int i = 0; i < subscriptions.Plugins.Count; i++)
233261
{
262+
Plugin plugin = subscriptions.Plugins[i];
234263
// Call the hook
235-
object value = plugins[i].CallHook(hook, args);
264+
object value = plugin.CallHook(hook, args);
236265
if (value != null)
237266
{
238267
values[i] = value;
239268
finalValue = value;
240-
finalPlugin = plugins[i];
269+
finalPlugin = plugin;
241270
returnCount++;
242271
}
243272
}
@@ -253,8 +282,9 @@ public object CallHook(string hook, params object[] args)
253282
{
254283
// Notify log of hook conflict
255284
hookConflicts.Clear();
256-
for (int i = 0; i < plugins.Count; i++)
285+
for (int i = 0; i < subscriptions.Plugins.Count; i++)
257286
{
287+
Plugin plugin = subscriptions.Plugins[i];
258288
object value = values[i];
259289
if (value == null)
260290
{
@@ -265,14 +295,14 @@ public object CallHook(string hook, params object[] args)
265295
{
266296
if (!values[i].Equals(finalValue))
267297
{
268-
hookConflicts.Add($"{plugins[i].Name} - {value} ({value.GetType().Name})");
298+
hookConflicts.Add($"{plugin.Name} - {value} ({value.GetType().Name})");
269299
}
270300
}
271301
else
272302
{
273303
if (values[i] != finalValue)
274304
{
275-
hookConflicts.Add($"{plugins[i].Name} - {value} ({value.GetType().Name})");
305+
hookConflicts.Add($"{plugin.Name} - {value} ({value.GetType().Name})");
276306
}
277307
}
278308
}
@@ -286,49 +316,34 @@ public object CallHook(string hook, params object[] args)
286316
}
287317
finally
288318
{
289-
hookCallDepth--;
290-
if (hookCallDepth == 0)
319+
subscriptions.CallDepth--;
320+
if (subscriptions.CallDepth == 0)
291321
{
292-
ProcessHookChanges();
322+
ProcessHookChanges(subscriptions);
293323
}
294324
}
295325

296326
return finalValue;
297327
}
298328

299-
private void ProcessHookChanges()
329+
private void ProcessHookChanges(HookSubscriptions subscriptions)
300330
{
301-
foreach (var pair in pendingHookSubscriptions)
331+
while (subscriptions.PendingChanges.Count != 0)
302332
{
303-
if (!hookSubscriptions.TryGetValue(pair.Key, out IList<Plugin> list))
304-
{
305-
list = new List<Plugin>();
306-
hookSubscriptions.Add(pair.Key, list);
307-
}
333+
SubscriptionChange change = subscriptions.PendingChanges.Dequeue();
308334

309-
foreach (var plugin in pair.Value)
335+
if (change.Change == SubscriptionChangeType.Subscribe)
310336
{
311-
if (!list.Contains(plugin))
337+
if (!subscriptions.Plugins.Contains(change.Plugin))
312338
{
313-
list.Add(plugin);
339+
subscriptions.Plugins.Add(change.Plugin);
314340
}
315341
}
316-
}
317-
318-
pendingHookSubscriptions.Clear();
319-
320-
foreach (var pair in pendingHookUnsubscriptions)
321-
{
322-
if (hookSubscriptions.TryGetValue(pair.Key, out IList<Plugin> list))
342+
else
323343
{
324-
foreach (var plugin in pair.Value)
325-
{
326-
list.Remove(plugin);
327-
}
344+
subscriptions.Plugins.Remove(change.Plugin);
328345
}
329346
}
330-
331-
pendingHookUnsubscriptions.Clear();
332347
}
333348

334349
/// <summary>
@@ -341,21 +356,28 @@ private void ProcessHookChanges()
341356
/// <returns></returns>
342357
public object CallDeprecatedHook(string oldHook, string newHook, DateTime expireDate, params object[] args)
343358
{
344-
if (!hookSubscriptions.TryGetValue(oldHook, out IList<Plugin> plugins))
359+
HookSubscriptions subscriptions;
360+
361+
lock (hookSubscriptions)
345362
{
346-
return null;
363+
if (!hookSubscriptions.TryGetValue(oldHook, out subscriptions))
364+
{
365+
return null;
366+
}
347367
}
348368

349-
if (plugins.Count == 0)
369+
370+
if (subscriptions.Plugins.Count == 0)
350371
{
351372
return null;
352373
}
353374

354375
float now = Interface.Oxide.Now;
355376
if (!lastDeprecatedWarningAt.TryGetValue(oldHook, out float lastWarningAt) || now - lastWarningAt > 3600f)
356377
{
378+
// TODO: Add better handling
357379
lastDeprecatedWarningAt[oldHook] = now;
358-
Interface.Oxide.LogWarning($"'{plugins[0].Name} v{plugins[0].Version}' is using deprecated hook '{oldHook}', which will stop working on {expireDate.ToString("D")}. Please ask the author to update to '{newHook}'");
380+
Interface.Oxide.LogWarning($"'{subscriptions.Plugins[0].Name} v{subscriptions.Plugins[0].Version}' is using deprecated hook '{oldHook}', which will stop working on {expireDate.ToString("D")}. Please ask the author to update to '{newHook}'");
359381
}
360382

361383
return CallHook(oldHook, args);

0 commit comments

Comments
 (0)