1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 20:47:28 +08:00

Merge pull request #27698 from bdach/banish-rulesets-that-do-stupid-crap-to-the-BLAGOLE

Attempt to disable custom rulesets that can be linked to an unhandled crash
This commit is contained in:
Dean Herbert 2024-04-01 15:47:47 +08:00 committed by GitHub
commit 95b50576bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 79 additions and 15 deletions

View File

@ -678,16 +678,21 @@ namespace osu.Game
/// <summary>
/// Allows a maximum of one unhandled exception, per second of execution.
/// </summary>
private bool onExceptionThrown(Exception _)
/// <returns>Whether to ignore the exception and continue running.</returns>
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")} .");
if (Interlocked.Decrement(ref allowableExceptions) < 0)
{
Logger.Log("Too many unhandled exceptions, crashing out.");
RulesetStore.TryDisableCustomRulesetsCausing(ex);
return false;
}
Logger.Log($"Unhandled exception has been allowed with {allowableExceptions} more allowable exceptions.");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return continueExecution;
return true;
}
protected override void Dispose(bool isDisposing)

View File

@ -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<RulesetInfo> AvailableRulesets => availableRulesets;
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
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,48 @@ 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)
{
try
{
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);
}
}
}
}
catch (Exception ex)
{
Logger.Log($"Attempt to trace back crash to custom ruleset failed: {ex}");
}
}
}
}

View File

@ -18,6 +18,8 @@ namespace osu.Game.Rulesets
private const string ruleset_library_prefix = @"osu.Game.Rulesets";
protected readonly Dictionary<Assembly, Type> LoadedAssemblies = new Dictionary<Assembly, Type>();
protected readonly HashSet<Assembly> UserRulesetAssemblies = new HashSet<Assembly>();
protected readonly Storage? RulesetStorage;
/// <summary>
/// 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);
}
/// <summary>
@ -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)