diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 81e3d8bed8..8bda8fb6c2 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -678,12 +678,14 @@ namespace osu.Game
///
/// Allows a maximum of one unhandled exception, per second of execution.
///
- private bool onExceptionThrown(Exception _)
+ private bool onExceptionThrown(Exception ex)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
+ RulesetStore.TryDisableCustomRulesetsCausing(ex);
+
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs
index ba6f4583d1..36eae7af2c 100644
--- a/osu.Game/Rulesets/RealmRulesetStore.cs
+++ b/osu.Game/Rulesets/RealmRulesetStore.cs
@@ -3,8 +3,11 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
using System.Linq;
using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -13,17 +16,20 @@ namespace osu.Game.Rulesets
{
public class RealmRulesetStore : RulesetStore
{
+ private readonly RealmAccess realmAccess;
public override IEnumerable AvailableRulesets => availableRulesets;
private readonly List availableRulesets = new List();
- public RealmRulesetStore(RealmAccess realm, Storage? storage = null)
+ public RealmRulesetStore(RealmAccess realmAccess, Storage? storage = null)
: base(storage)
{
- prepareDetachedRulesets(realm);
+ this.realmAccess = realmAccess;
+ prepareDetachedRulesets();
+ informUserAboutBrokenRulesets();
}
- private void prepareDetachedRulesets(RealmAccess realmAccess)
+ private void prepareDetachedRulesets()
{
realmAccess.Write(realm =>
{
@@ -143,5 +149,41 @@ namespace osu.Game.Rulesets
instance.CreateBeatmapProcessor(converter.Convert());
}
+
+ private void informUserAboutBrokenRulesets()
+ {
+ if (RulesetStorage == null)
+ return;
+
+ foreach (string brokenRulesetDll in RulesetStorage.GetFiles(@".", @"*.dll.broken"))
+ {
+ Logger.Log($"Ruleset '{Path.GetFileNameWithoutExtension(brokenRulesetDll)}' has been disabled due to causing a crash.\n\n"
+ + "Please update the ruleset or report the issue to the developers of the ruleset if no updates are available.", level: LogLevel.Important);
+ }
+ }
+
+ internal void TryDisableCustomRulesetsCausing(Exception exception)
+ {
+ var stackTrace = new StackTrace(exception);
+
+ foreach (var frame in stackTrace.GetFrames())
+ {
+ var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly;
+ if (declaringAssembly == null)
+ continue;
+
+ if (UserRulesetAssemblies.Contains(declaringAssembly))
+ {
+ string sourceLocation = declaringAssembly.Location;
+ string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken");
+
+ if (File.Exists(sourceLocation))
+ {
+ Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken.");
+ File.Move(sourceLocation, destinationLocation);
+ }
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index ac36ee6494..f33d42a53e 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets
private const string ruleset_library_prefix = @"osu.Game.Rulesets";
protected readonly Dictionary LoadedAssemblies = new Dictionary();
+ protected readonly HashSet UserRulesetAssemblies = new HashSet();
+ protected readonly Storage? RulesetStorage;
///
/// All available rulesets.
@@ -41,9 +43,9 @@ namespace osu.Game.Rulesets
// to load as unable to locate the game core assembly.
AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
- var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
- if (rulesetStorage != null)
- loadUserRulesets(rulesetStorage);
+ RulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
+ if (RulesetStorage != null)
+ loadUserRulesets(RulesetStorage);
}
///
@@ -105,7 +107,11 @@ namespace osu.Game.Rulesets
var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
- loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
+ {
+ var assembly = loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
+ if (assembly != null)
+ UserRulesetAssemblies.Add(assembly);
+ }
}
private void loadFromDisk()
@@ -126,21 +132,25 @@ namespace osu.Game.Rulesets
}
}
- private void loadRulesetFromFile(string file)
+ private Assembly? loadRulesetFromFile(string file)
{
string filename = Path.GetFileNameWithoutExtension(file);
if (LoadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
- return;
+ return null;
try
{
- addRuleset(Assembly.LoadFrom(file));
+ var assembly = Assembly.LoadFrom(file);
+ addRuleset(assembly);
+ return assembly;
}
catch (Exception e)
{
LogFailedLoad(filename, e);
}
+
+ return null;
}
private void addRuleset(Assembly assembly)