- Fixed NRE in TryBeginDispose

- Made `OnException` event useful.
- Added some null checks where expected.
- Fixed overridden Unload not being called.
- Removed partial from AssemblyManager.cs
- Made ClearTypesList() actually work.
- Made exception details show in console on release builds.
- Made content package name show on plugin load.
- Made execution standard instead of none for autogenerated and erroneous RunConfigs.
This commit is contained in:
MapleWheels
2023-10-22 16:13:46 -04:00
committed by Evil Factory
parent ac068aa3f9
commit e984633ca5
4 changed files with 91 additions and 75 deletions
@@ -25,7 +25,7 @@ namespace Barotrauma;
/// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin. /// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin.
/// All plugins are loaded into their own AssemblyLoadContext along with their dependencies. /// All plugins are loaded into their own AssemblyLoadContext along with their dependencies.
/// </summary> /// </summary>
public partial class AssemblyManager public class AssemblyManager
{ {
#region ExternalAPI #region ExternalAPI
@@ -143,16 +143,23 @@ public partial class AssemblyManager
{ {
if (!_subTypesLookupCache.TryAdd(typeName, list1)) if (!_subTypesLookupCache.TryAdd(typeName, list1))
{ {
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to add subtypes to cache of type {typeName}!"); ModUtils.Logging.PrintError(
$"{nameof(AssemblyManager)}: Unable to add subtypes to cache of type {typeName}!");
} }
} }
else else
{ {
ModUtils.Logging.PrintMessage($"{nameof(AssemblyManager)}: Warning: No types found during search for subtypes of {typeName}"); ModUtils.Logging.PrintMessage(
$"{nameof(AssemblyManager)}: Warning: No types found during search for subtypes of {typeName}");
} }
return list1; return list1;
} }
catch (Exception e)
{
this.OnException?.Invoke($"{nameof(AssemblyManager)}::{nameof(GetSubTypesInLoadedAssemblies)}() | Error: {e.Message}", e);
return ImmutableList<Type>.Empty;
}
finally finally
{ {
OpsLockLoaded.ExitReadLock(); OpsLockLoaded.ExitReadLock();
@@ -187,7 +194,6 @@ public partial class AssemblyManager
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="types"></param> /// <param name="types"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns> /// <returns></returns>
public bool TryGetSubTypesFromACL(Guid id, out IEnumerable<Type> types) public bool TryGetSubTypesFromACL(Guid id, out IEnumerable<Type> types)
{ {
@@ -206,7 +212,7 @@ public partial class AssemblyManager
/// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string. /// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string.
/// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ". /// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ".
/// </summary> /// </summary>
/// <param name="name">The string name of the type to search for.</param> /// <param name="typeName">The string name of the type to search for.</param>
/// <returns>An Enumerator for matching types. List will be empty if bad params are supplied.</returns> /// <returns>An Enumerator for matching types. List will be empty if bad params are supplied.</returns>
public IEnumerable<Type> GetTypesByName(string typeName) public IEnumerable<Type> GetTypesByName(string typeName)
{ {
@@ -246,9 +252,16 @@ public partial class AssemblyManager
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{ {
t = assembly.GetType(typeName, false, false); try
if (t is not null) {
types.Add(byRef ? t.MakeByRefType() : t); t = assembly.GetType(typeName, false, false);
if (t is not null)
types.Add(byRef ? t.MakeByRefType() : t);
}
catch (Exception e)
{
this.OnException?.Invoke($"{nameof(AssemblyManager)}::{nameof(GetTypesByName)}() | Error: {e.Message}", e);
}
} }
} }
finally finally
@@ -316,9 +329,9 @@ public partial class AssemblyManager
/// <returns></returns> /// <returns></returns>
public IEnumerable<LoadedACL> GetAllLoadedACLs() public IEnumerable<LoadedACL> GetAllLoadedACLs()
{ {
OpsLockLoaded.EnterReadLock();
try try
{ {
OpsLockLoaded.EnterReadLock();
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList(); return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
} }
finally finally
@@ -361,6 +374,9 @@ public partial class AssemblyManager
if (compiledAssemblyName.IsNullOrWhiteSpace()) if (compiledAssemblyName.IsNullOrWhiteSpace())
return AssemblyLoadingSuccessState.BadName; return AssemblyLoadingSuccessState.BadName;
if (syntaxTree is null)
return AssemblyLoadingSuccessState.InvalidAssembly;
if (!GetOrCreateACL(id, friendlyName, out var acl)) if (!GetOrCreateACL(id, friendlyName, out var acl))
return AssemblyLoadingSuccessState.ACLLoadFailure; return AssemblyLoadingSuccessState.ACLLoadFailure;
@@ -419,8 +435,10 @@ public partial class AssemblyManager
if (filePaths is null) if (filePaths is null)
{ {
throw new ArgumentNullException( var exception = new ArgumentNullException(
$"{nameof(AssemblyManager)}::{nameof(LoadAssembliesFromLocations)}() | file paths supplied is null!"); $"{nameof(AssemblyManager)}::{nameof(LoadAssembliesFromLocations)}() | file paths supplied is null!");
this.OnException?.Invoke($"Error: {exception.Message}", exception);
throw exception;
} }
ImmutableList<string> assemblyFilePaths = filePaths.ToImmutableList(); // copy the list before loading ImmutableList<string> assemblyFilePaths = filePaths.ToImmutableList(); // copy the list before loading
@@ -468,12 +486,15 @@ public partial class AssemblyManager
{ {
if (loadedAcl.Value.Acl is not null) if (loadedAcl.Value.Acl is not null)
{ {
foreach (Delegate del in IsReadyToUnloadACL.GetInvocationList()) if (IsReadyToUnloadACL is not null)
{ {
if (del is System.Func<LoadedACL, bool> { } func) foreach (Delegate del in IsReadyToUnloadACL.GetInvocationList())
{ {
if (!func.Invoke(loadedAcl.Value)) if (del is System.Func<LoadedACL, bool> { } func)
return false; // Not ready, exit {
if (!func.Invoke(loadedAcl.Value))
return false; // Not ready, exit
}
} }
} }
@@ -492,9 +513,10 @@ public partial class AssemblyManager
LoadedACLs.Clear(); LoadedACLs.Clear();
return true; return true;
} }
catch catch(Exception e)
{ {
// should never happen // should never happen
this.OnException?.Invoke($"{nameof(TryBeginDispose)}() | Error: {e.Message}", e);
return false; return false;
} }
finally finally
@@ -609,9 +631,9 @@ public partial class AssemblyManager
} }
} }
catch catch(Exception e)
{ {
// should never happen but in-case this.OnException?.Invoke($"{nameof(GetOrCreateACL)}Error: {e.Message}", e);
acl = null; acl = null;
return false; return false;
} }
@@ -648,9 +670,9 @@ public partial class AssemblyManager
return true; return true;
} }
catch catch (Exception e)
{ {
// should never happen this.OnException?.Invoke($"{nameof(DisposeACL)}() | Error: {e.Message}", e);
return false; return false;
} }
finally finally
@@ -677,8 +699,9 @@ public partial class AssemblyManager
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t); .ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
_subTypesLookupCache.Clear(); _subTypesLookupCache.Clear();
} }
catch(ArgumentException _) catch(ArgumentException ae)
{ {
this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {ae.Message}", ae);
try try
{ {
// some types must've had duplicate type names, build the list while filtering // some types must've had duplicate type names, build the list while filtering
@@ -699,6 +722,7 @@ public partial class AssemblyManager
} }
catch (Exception e) catch (Exception e)
{ {
this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {e.Message}", e);
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to create list of default assembly types! Default AssemblyLoadContext types searching not available."); ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to create list of default assembly types! Default AssemblyLoadContext types searching not available.");
#if DEBUG #if DEBUG
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Exception Details :{e.Message} | {e.InnerException}"); ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Exception Details :{e.Message} | {e.InnerException}");
@@ -729,14 +753,14 @@ public partial class AssemblyManager
public readonly Guid Id; public readonly Guid Id;
private ImmutableDictionary<string, Type> _assembliesTypes = ImmutableDictionary<string, Type>.Empty; private ImmutableDictionary<string, Type> _assembliesTypes = ImmutableDictionary<string, Type>.Empty;
public readonly MemoryFileAssemblyContextLoader Acl; public readonly MemoryFileAssemblyContextLoader Acl;
private readonly AssemblyManager _manager;
internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName) internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName)
{ {
this.Id = id; this.Id = id;
this.Acl = new(manager); this.Acl = new(manager)
this._manager = manager; {
this.Acl.FriendlyName = friendlyName; FriendlyName = friendlyName
};
} }
public ImmutableDictionary<string, Type> AssembliesTypes => _assembliesTypes; public ImmutableDictionary<string, Type> AssembliesTypes => _assembliesTypes;
@@ -752,7 +776,7 @@ public partial class AssemblyManager
.SelectMany(a => a.GetSafeTypes()) .SelectMany(a => a.GetSafeTypes())
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t); .ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
} }
catch(ArgumentException _) catch(ArgumentException)
{ {
// some types must've had duplicate type names, build the list while filtering // some types must've had duplicate type names, build the list while filtering
Dictionary<string, Type> types = new(); Dictionary<string, Type> types = new();
@@ -774,7 +798,7 @@ public partial class AssemblyManager
internal void ClearTypesList() internal void ClearTypesList()
{ {
_assembliesTypes.Clear(); _assembliesTypes = ImmutableDictionary<string, Type>.Empty;
} }
} }
@@ -802,12 +826,12 @@ public static class AssemblyExtensions
{ {
return re.Types.Where(x => x != null)!; return re.Types.Where(x => x != null)!;
} }
catch (InvalidOperationException ioe) catch (InvalidOperationException)
{ {
return new List<Type>(); return new List<Type>();
} }
} }
catch (Exception e) catch (Exception)
{ {
return new List<Type>(); return new List<Type>();
} }
@@ -11,6 +11,7 @@ using Barotrauma.Steam;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using MonoMod.Utils; using MonoMod.Utils;
// ReSharper disable InconsistentNaming
namespace Barotrauma; namespace Barotrauma;
@@ -147,12 +148,12 @@ public sealed class CsPackageManager : IDisposable
/// <summary> /// <summary>
/// Whether or not plugins' types have been instantiated. /// Whether or not plugins' types have been instantiated.
/// </summary> /// </summary>
public bool PluginsInitialized { get; private set; } = false; public bool PluginsInitialized { get; private set; }
/// <summary> /// <summary>
/// Whether or not plugins are fully loaded. /// Whether or not plugins are fully loaded.
/// </summary> /// </summary>
public bool PluginsLoaded { get; private set; } = false; public bool PluginsLoaded { get; private set; }
public IEnumerable<ContentPackage> GetCurrentPackagesByLoadOrder() => _currentPackagesByLoadOrder; public IEnumerable<ContentPackage> GetCurrentPackagesByLoadOrder() => _currentPackagesByLoadOrder;
@@ -333,7 +334,7 @@ public sealed class CsPackageManager : IDisposable
throw new DirectoryNotFoundException("No publicized assemblies found."); throw new DirectoryNotFoundException("No publicized assemblies found.");
} }
// no directory found, use the other one // no directory found, use the other one
catch (DirectoryNotFoundException dne) catch (DirectoryNotFoundException)
{ {
if (_luaCsSetup.Config.PreferToUseWorkshopLuaSetup) if (_luaCsSetup.Config.PreferToUseWorkshopLuaSetup)
{ {
@@ -454,8 +455,7 @@ public sealed class CsPackageManager : IDisposable
if (reliableMap && OrderAndFilterPackagesByDependencies( if (reliableMap && OrderAndFilterPackagesByDependencies(
_packagesDependencies, _packagesDependencies,
out var readyToLoad, out var readyToLoad,
out var cannotLoadPackages, out var cannotLoadPackages))
null))
{ {
packagesToLoadInOrder.AddRange(readyToLoad); packagesToLoadInOrder.AddRange(readyToLoad);
if (cannotLoadPackages is not null) if (cannotLoadPackages is not null)
@@ -611,21 +611,19 @@ public sealed class CsPackageManager : IDisposable
bool ShouldRunPackage(ContentPackage package, RunConfig config) bool ShouldRunPackage(ContentPackage package, RunConfig config)
{ {
if (config.AutoGenerated)
return false;
return (!_luaCsSetup.Config.TreatForcedModsAsNormal && config.IsForced()) return (!_luaCsSetup.Config.TreatForcedModsAsNormal && config.IsForced())
|| (ContentPackageManager.EnabledPackages.All.Contains(package) && config.IsForcedOrStandard()); || (ContentPackageManager.EnabledPackages.All.Contains(package) && config.IsForcedOrStandard());
} }
void UpdatePackagesToDisable(ref HashSet<ContentPackage> list, void UpdatePackagesToDisable(ref HashSet<ContentPackage> set,
ContentPackage newDisabledPackage, ContentPackage newDisabledPackage,
IEnumerable<KeyValuePair<ContentPackage, ImmutableList<ContentPackage>>> dependenciesMap) IEnumerable<KeyValuePair<ContentPackage, ImmutableList<ContentPackage>>> dependenciesMap)
{ {
list.Add(newDisabledPackage); set.Add(newDisabledPackage);
foreach (var package in dependenciesMap) foreach (var package in dependenciesMap)
{ {
if (package.Value.Contains(newDisabledPackage)) if (package.Value.Contains(newDisabledPackage))
list.Add(newDisabledPackage); set.Add(newDisabledPackage);
} }
} }
} }
@@ -655,7 +653,7 @@ public sealed class CsPackageManager : IDisposable
// init // init
foreach (var plugin in contentPlugins.Value) foreach (var plugin in contentPlugins.Value)
{ {
TryRun(() => plugin.Initialize(), $"{nameof(IAssemblyPlugin.Initialize)}", plugin.GetType().Name); TryRun(() => plugin.Initialize(), $"{nameof(IAssemblyPlugin.Initialize)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
} }
} }
@@ -664,7 +662,7 @@ public sealed class CsPackageManager : IDisposable
// load complete // load complete
foreach (var plugin in contentPlugins.Value) foreach (var plugin in contentPlugins.Value)
{ {
TryRun(() => plugin.OnLoadCompleted(), $"{nameof(IAssemblyPlugin.OnLoadCompleted)}", plugin.GetType().Name); TryRun(() => plugin.OnLoadCompleted(), $"{nameof(IAssemblyPlugin.OnLoadCompleted)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
} }
} }
@@ -698,7 +696,7 @@ public sealed class CsPackageManager : IDisposable
// init // init
foreach (var plugin in contentPlugins.Value) foreach (var plugin in contentPlugins.Value)
{ {
TryRun(() => plugin.PreInitPatching(), $"{nameof(IAssemblyPlugin.PreInitPatching)}", plugin.GetType().Name); TryRun(() => plugin.PreInitPatching(), $"{nameof(IAssemblyPlugin.PreInitPatching)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
} }
} }
@@ -741,21 +739,20 @@ public sealed class CsPackageManager : IDisposable
try try
{ {
plugin = (IAssemblyPlugin)Activator.CreateInstance(type); plugin = (IAssemblyPlugin)Activator.CreateInstance(type);
_loadedPlugins[pair.Key].Add(plugin);
} }
catch (Exception e) catch (Exception e)
{ {
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}. Now disposing..."); ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}. Now disposing...");
#if DEBUG
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}"); ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}");
#endif
TryRun(() => plugin?.Dispose(), "Dispose", type.FullName ?? type.Name);
plugin = null; if (plugin is not null)
{
// ReSharper disable once AccessToModifiedClosure
TryRun(() => plugin?.Dispose(), nameof(IAssemblyPlugin.Dispose), type.FullName ?? type.Name);
plugin = null;
}
} }
if (plugin is not null)
_loadedPlugins[pair.Key].Add(plugin);
else
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}");
} }
} }
@@ -772,7 +769,7 @@ public sealed class CsPackageManager : IDisposable
{ {
foreach (var plugin in contentPlugins.Value) foreach (var plugin in contentPlugins.Value)
{ {
TryRun(() => plugin.Dispose(), $"{nameof(IAssemblyPlugin.Dispose)}", plugin.GetType().Name); TryRun(() => plugin.Dispose(), $"{nameof(IAssemblyPlugin.Dispose)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
} }
contentPlugins.Value.Clear(); contentPlugins.Value.Clear();
} }
@@ -816,9 +813,7 @@ public sealed class CsPackageManager : IDisposable
catch (Exception e) catch (Exception e)
{ {
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while running {messageMethodName}() on plugin of type {messageTypeName}"); ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while running {messageMethodName}() on plugin of type {messageTypeName}");
#if DEBUG
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}"); ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}");
#endif
} }
} }
@@ -5,13 +5,10 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader; using System.Runtime.Loader;
using System.Threading;
using Barotrauma;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit; // ReSharper disable ConditionIsAlwaysTrueOrFalse
namespace Barotrauma; namespace Barotrauma;
@@ -23,16 +20,16 @@ namespace Barotrauma;
public class MemoryFileAssemblyContextLoader : AssemblyLoadContext public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
{ {
// public // public
public string FriendlyName { get; set; } = null; public string FriendlyName { get; set; }
// ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBePrivate.Global
public Assembly CompiledAssembly { get; private set; } = null; public Assembly CompiledAssembly { get; private set; }
public byte[] CompiledAssemblyImage { get; private set; } = null; public byte[] CompiledAssemblyImage { get; private set; }
// ReSharper restore MemberCanBePrivate.Global // ReSharper restore MemberCanBePrivate.Global
// internal // internal
private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new(); // path-folder, resolver private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new(); // path-folder, resolver
protected bool IsResolving; //this is to avoid circular dependency lookup. protected bool IsResolving; //this is to avoid circular dependency lookup.
private AssemblyManager _assemblyManager; private AssemblyManager _assemblyManager;
public bool IsTemplateMode { get; set; } = false; public bool IsTemplateMode { get; set; }
public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true) public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true)
{ {
@@ -73,23 +70,23 @@ public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
LoadFromAssemblyPath(sanitizedFilePath); LoadFromAssemblyPath(sanitizedFilePath);
} }
// on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected. // on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected.
catch (ArgumentNullException ane) catch (ArgumentNullException)
{ {
return AssemblyLoadingSuccessState.BadFilePath; return AssemblyLoadingSuccessState.BadFilePath;
} }
catch (ArgumentException ae) catch (ArgumentException)
{ {
return AssemblyLoadingSuccessState.BadFilePath; return AssemblyLoadingSuccessState.BadFilePath;
} }
catch (FileLoadException fle) catch (FileLoadException)
{ {
return AssemblyLoadingSuccessState.CannotLoadFile; return AssemblyLoadingSuccessState.CannotLoadFile;
} }
catch (FileNotFoundException fne) catch (FileNotFoundException)
{ {
return AssemblyLoadingSuccessState.NoAssemblyFound; return AssemblyLoadingSuccessState.NoAssemblyFound;
} }
catch (BadImageFormatException bfe) catch (BadImageFormatException)
{ {
return AssemblyLoadingSuccessState.InvalidAssembly; return AssemblyLoadingSuccessState.InvalidAssembly;
} }
@@ -281,7 +278,7 @@ public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
} }
private new void Unload() public new void Unload()
{ {
CompiledAssembly = null; CompiledAssembly = null;
CompiledAssemblyImage = null; CompiledAssemblyImage = null;
@@ -32,7 +32,7 @@ public sealed class RunConfig
this.AutoGenerated = autoGenerated; this.AutoGenerated = autoGenerated;
if (autoGenerated) if (autoGenerated)
{ {
(Client, Server) = ("None", "None"); (Client, Server) = ("Standard", "Standard");
} }
} }
@@ -60,18 +60,18 @@ public sealed class RunConfig
{ {
Client = SanitizeRunSetting(Client); Client = SanitizeRunSetting(Client);
} }
catch (Exception e) catch (Exception)
{ {
Client = "None"; Client = "Standard";
} }
try try
{ {
Server = SanitizeRunSetting(Server); Server = SanitizeRunSetting(Server);
} }
catch (Exception e) catch (Exception)
{ {
Server = "None"; Server = "Standard";
} }
Dependencies ??= new RunConfig.Dependency[] { }; Dependencies ??= new RunConfig.Dependency[] { };
@@ -79,9 +79,9 @@ public sealed class RunConfig
static string SanitizeRunSetting(string str) => static string SanitizeRunSetting(string str) =>
str switch str switch
{ {
null => "None", null => "Standard",
"" => "None", "" => "Standard",
" " => "None", " " => "Standard",
_ => str[0].ToString().ToUpper() + str.Substring(1).ToLower() _ => str[0].ToString().ToUpper() + str.Substring(1).ToLower()
}; };