Files
sbox-public/engine/Sandbox.Hotload/UpdateReferences.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

943 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Sandbox.Upgraders;
using Sentry;
namespace Sandbox
{
public partial class Hotload
{
private readonly Dictionary<Type, Type> SubstituteTypeCache = new Dictionary<Type, Type>();
private readonly Dictionary<Type, InstanceTimingEntry> TypeTimings = new Dictionary<Type, InstanceTimingEntry>();
private readonly Dictionary<IInstanceProcessor, InstanceTimingEntry> ProcessorTimings = new Dictionary<IInstanceProcessor, InstanceTimingEntry>();
private readonly Queue<InstanceTask> DefaultInstanceTaskQueue = new Queue<InstanceTask>();
private readonly Queue<InstanceTask> LateInstanceTaskQueue = new Queue<InstanceTask>();
private readonly Dictionary<Assembly, (Type Type, FieldInfo[] Fields)[]> StaticFieldCache = new();
private HotloadResult CurrentResult;
private ReferencePath CurrentPath;
private FieldInfo CurrentSrcField;
private FieldInfo CurrentDstField;
/// <summary>
/// Merge chains of swaps. For example, if A is swapped with B, and B is swapped with C, simplify to A swapping with C.
/// </summary>
private void SimplifySwaps()
{
foreach ( var oldAsm in Swaps.Keys.ToArray() )
{
var newAsm = Swaps[oldAsm];
var count = Swaps.Count;
while ( newAsm != null && Swaps.TryGetValue( newAsm, out var nextAsm ) )
{
if ( count-- < 0 )
{
Log( HotloadEntryType.Error, $"Assembly swap cycle detected involving {FormatAssemblyName( oldAsm )}." );
newAsm = oldAsm;
break;
}
newAsm = nextAsm;
}
Swaps[oldAsm] = newAsm;
}
}
/// <summary>
/// Cycle though all types in all watched assemblies.
/// Find statics, iterate over all their fields recursively.
/// Replace any instances of classes that are defined in the assemblies added using ReplacingAssembly
/// </summary>
public HotloadResult UpdateReferences()
{
// If there's nothing to do, early out
if ( Swaps.Count == 0 )
return HotloadResult.NoActionSingleton;
CurrentResult = new HotloadResult();
TypeTimings.Clear();
ProcessorTimings.Clear();
var timer = new Stopwatch();
timer.Start();
SimplifySwaps();
// Remove cached static fields for assemblies that aren't watched any more
foreach ( var cachedAsm in StaticFieldCache.Keys.ToArray() )
{
if ( Swaps.ContainsKey( cachedAsm ) || !_watchedAssemblies.ContainsKey( cachedAsm ) )
{
StaticFieldCache.Remove( cachedAsm );
}
}
// Make sure instance upgraders are set up
foreach ( var upgrader in AllUpgraders.Values )
{
if ( !upgrader.IsInitialized )
upgrader.Initialize( this );
}
var defaultUpgrader = GetUpgrader<DefaultUpgrader>();
try
{
RootUpgraderGroup.HotloadStart();
var staticFieldStartTime = timer.Elapsed;
var watchedAssemblies = _watchedAssemblies.Keys
.Union( Swaps.Keys )
.Union( New.Where( x => !Swaps.ContainsValue( x ) ) );
foreach ( var (type, fields) in watchedAssemblies.SelectMany( GetWatchedFields ) )
{
UpdateReferencesInType( type, fields );
}
CurrentResult.StaticFieldTime = (timer.Elapsed - staticFieldStartTime).TotalMilliseconds;
var watchedInstanceStartTime = timer.Elapsed;
foreach ( var instance in WatchedInstances )
{
CurrentPath = ReferencePath.GetRoot( instance.GetType() );
defaultUpgrader.ProcessObjectFields( instance );
CurrentPath = null;
}
CurrentResult.WatchedInstanceTime = (timer.Elapsed - watchedInstanceStartTime).TotalMilliseconds;
var instanceQueueStartTime = timer.Elapsed;
ProcessInstanceQueue();
CurrentResult.InstanceQueueTime = (timer.Elapsed - instanceQueueStartTime).TotalMilliseconds;
}
finally
{
RootUpgraderGroup.HotloadComplete();
if ( TryGetUpgrader<AutoSkipUpgrader>( out var upgrader ) )
{
CurrentResult.AutoSkippedTypes.AddRange( upgrader
.SkippedTypes
.Select( x => x.ToSimpleString() ) );
}
ClearFieldDefaults();
DefaultInstanceTaskQueue.Clear();
LateInstanceTaskQueue.Clear();
SubstituteTypeCache.Clear();
ScopeMethodOrdinals.Clear();
AnonymousTypes.Clear();
RootUpgraderGroup.ClearCache();
// update the watched assemblies if we swapped those
foreach ( var swap in Swaps )
{
if ( swap.Key is null ) continue;
if ( !_watchedAssemblies.TryGetValue( swap.Key, out var filter ) ) continue;
UnwatchAssembly( swap.Key );
if ( swap.Value is null ) continue;
WatchAssembly( swap.Value, filter );
}
foreach ( var asm in New )
{
WatchAssembly( asm );
}
Swaps.Clear();
New.Clear();
}
CurrentResult.ProcessingTime = timer.Elapsed.TotalMilliseconds;
return CurrentResult;
}
private void ScheduleInstanceTask( IInstanceProcessor handler, object oldInstance, object newInstance )
{
DefaultInstanceTaskQueue.Enqueue( new InstanceTask( handler, oldInstance, newInstance, CurrentSrcField, CurrentDstField, CurrentPath ) );
}
private void ScheduleLateInstanceTask( IInstanceProcessor handler, object oldInstance, object newInstance )
{
LateInstanceTaskQueue.Enqueue( new InstanceTask( handler, oldInstance, newInstance, CurrentSrcField, CurrentDstField, CurrentPath ) );
}
private void ProcessInstanceQueue()
{
var metaSw = new Stopwatch();
var sw = new Stopwatch();
while ( DefaultInstanceTaskQueue.TryDequeue( out var task ) || LateInstanceTaskQueue.TryDequeue( out task ) )
{
sw.Restart();
CurrentPath = task.Path;
CurrentSrcField = task.SrcField;
CurrentDstField = task.DstField;
var instanceCount = task.Handler.ProcessInstance( task.OldInstance, task.NewInstance );
var instanceType = task.OldInstance.GetType();
CurrentPath = null;
CurrentSrcField = null;
CurrentDstField = null;
var elapsed = sw.Elapsed;
metaSw.Start();
CurrentResult.InstancesProcessed += instanceCount;
//
// record timing information
// this should help diagnose potentially slow classes
// that maybe don't need hotloading, so you can disable with
// the Skip attribute
//
{
string rootString = null;
if ( IncludeTypeTimings )
{
if ( !TypeTimings.TryGetValue( instanceType, out var tt ) )
{
var typeName = instanceType.ToSimpleString();
// Check for type name collisions
if ( !CurrentResult.TypeTimings.TryGetValue( typeName, out tt ) )
{
CurrentResult.TypeTimings.Add( typeName,
tt = new InstanceTimingEntry( TraceRoots ) );
}
TypeTimings.Add( instanceType, tt );
}
AddTiming( tt, instanceCount, elapsed, TraceRoots ? task.Path : null, ref rootString );
}
if ( IncludeProcessorTimings )
{
if ( !ProcessorTimings.TryGetValue( task.Handler, out var pt ) )
{
var typeName = task.Handler.GetType().ToSimpleString();
// Check for type name collisions
if ( !CurrentResult.ProcessorTimings.TryGetValue( typeName, out pt ) )
{
CurrentResult.ProcessorTimings.Add( typeName, pt = new InstanceTimingEntry( TraceRoots ) );
}
ProcessorTimings.Add( task.Handler, pt );
}
AddTiming( pt, instanceCount, elapsed, TraceRoots ? task.Path : null, ref rootString );
}
}
metaSw.Stop();
}
CurrentResult.DiagnosticsTime = metaSw.Elapsed.TotalMilliseconds;
}
private void AddTiming( InstanceTimingEntry entry, int instanceCount, TimeSpan elapsed, ReferencePath path, ref string rootString )
{
entry.Instances += instanceCount;
entry.Milliseconds += elapsed.TotalMilliseconds;
if ( TraceRoots && path != null )
{
rootString ??= path.Root.ToString();
if ( entry.Roots.TryGetValue( rootString, out var rootTiming ) )
{
rootTiming.Instances += instanceCount;
rootTiming.Milliseconds += elapsed.TotalMilliseconds;
}
else
{
entry.Roots.Add( rootString, new TimingEntry( instanceCount, elapsed ) );
}
}
}
private static bool IsMatchingName( AssemblyName a, AssemblyName b )
{
return string.Equals( a.Name, b.Name, StringComparison.OrdinalIgnoreCase ) &&
(a.Version?.Equals( b.Version ) ?? false);
}
private IEnumerable<(Type Type, FieldInfo[] Fields)> GetWatchedFields( Assembly asm )
{
if ( Swaps.ContainsKey( asm ) )
{
return GetWatchedFieldsUncached( asm, true );
}
if ( !New.Contains( asm ) )
{
var references = asm.GetReferencedAssemblies();
// For assemblies that aren't getting swapped, check for references to swapped assemblies.
foreach ( var reference in references )
{
var matchingSwap = Swaps.SingleOrDefault( x => IsMatchingName( x.Key.GetName(), reference ) );
if ( matchingSwap.Key == null )
{
continue;
}
if ( matchingSwap.Value != null && references.Any( x => IsMatchingName( x, matchingSwap.Value.GetName() ) ) )
{
// Both the old and new versions of a swapped assembly are referenced by asm.
// This will probably only happen in Sandbox.Hotload.Test, and it's intentional.
Log( HotloadEntryType.Information, $"Both old and new versions of an assembly are referenced by a non-swapped assembly. ({FormatAssemblyName( asm )} references {FormatAssemblyName( matchingSwap.Key )} and {FormatAssemblyName( matchingSwap.Value )})" );
continue;
}
// A reference to an assembly that's getting swapped was found.
// This is technically a fault, but we'll let it slide with a message for now since tools relies on it.
Log( HotloadEntryType.Information, $"Skipping static fields from a non-swapped assembly that references a swapped assembly. ({FormatAssemblyName( asm )} references {FormatAssemblyName( matchingSwap.Key )})" );
return Enumerable.Empty<(Type, FieldInfo[])>();
}
}
if ( StaticFieldCache.TryGetValue( asm, out var cached ) )
{
return cached;
}
cached = GetWatchedFieldsUncached( asm, false ).ToArray();
StaticFieldCache.Add( asm, cached );
return cached;
}
private IEnumerable<(Type Type, FieldInfo[] Fields)> GetWatchedFieldsUncached( Assembly asm, bool isFromSwappedAsm )
{
if ( IsAssemblyIgnored( asm ) ) yield break;
var filter = _watchedAssemblies.GetValueOrDefault( asm );
foreach ( var type in asm.GetTypes() )
{
if ( filter?.Invoke( type ) is false ) continue;
var fields = GetWatchedFields( type, isFromSwappedAsm ).ToArray();
if ( fields.Length == 0 ) continue;
yield return (type, fields);
}
}
private IEnumerable<FieldInfo> GetWatchedFields( Type type, bool isFromSwappedAsm )
{
var typeInfo = type.GetTypeInfo();
var skipUpgrader = GetUpgrader<SkipUpgrader>();
var autoSkipUpgrader = TryGetUpgrader<AutoSkipUpgrader>( out var upgrader ) ? upgrader : null;
// Ignore anything in the hotload assembly
if ( IsAssemblyIgnored( typeInfo.Assembly ) ) yield break;
// TODO: Currently cannot find static fields in instances of generic types
if ( typeInfo.ContainsGenericParameters ) yield break;
// Type definition has a SkipAttribute
if ( typeInfo.GetCustomAttribute<SkipHotloadAttribute>() != null ) yield break;
// Attempting to change fields in <PrivateImplementationDetails> will fail
if ( type.Name == "<PrivateImplementationDetails>" ) yield break;
// Skip static compiler generated types (dynamic caches, etc?)
if ( typeInfo.IsSealed && isFromSwappedAsm && DelegateUpgrader.IsCompilerGenerated( type ) ) yield break;
const BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
// Iterate all statics
foreach ( var staticField in type.GetFields( flags ) )
{
// Ignore removed or const values
if ( staticField.IsLiteral ) continue;
// Ignore if marked with SkipAttribute
if ( staticField.HasAttribute<SkipHotloadAttribute>() ) continue;
if ( !isFromSwappedAsm && staticField.FieldType.IsSealed )
{
if ( skipUpgrader.ShouldProcessType( staticField.FieldType ) ) continue;
if ( autoSkipUpgrader?.ShouldProcessType( staticField.FieldType ) ?? false ) continue;
}
yield return staticField;
}
}
internal void UpdateReferencesInType( Type t, FieldInfo[] staticFields )
{
var subType = GetNewType( t );
// Ignore removed types
if ( subType == null ) return;
// Iterate all statics
foreach ( var staticField in staticFields )
{
var newField = subType != t
? subType.GetField( staticField.Name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly )
: staticField;
// Ignore removed fields
if ( newField == null ) continue;
// Ignore const values
if ( newField.IsLiteral ) continue;
// Ignore if marked with SkipAttribute
if ( newField.HasAttribute<SkipHotloadAttribute>() ) continue;
object curVal;
try
{
curVal = staticField.GetValue( null );
}
catch ( TargetInvocationException e )
{
if ( e.InnerException is TypeInitializationException )
{
Log( HotloadEntryType.Warning, $"{e.InnerException}", t );
break;
}
Log( e.InnerException, member: t );
continue;
}
if ( curVal is null ) continue;
if ( staticField.TryGetBackedProperty( out var staticProperty ) )
{
CurrentPath = ReferencePath.GetRoot( staticProperty );
}
else
{
CurrentPath = ReferencePath.GetRoot( staticField );
}
CurrentSrcField = staticField;
CurrentDstField = newField;
try
{
if ( newField.IsInitOnly )
{
// We can't assign a new value to this field, so fall
// back to upgrading the new instance in-place
var newVal = newField.GetValue( null );
if ( newVal is null ) continue;
RootUpgraderGroup.TryUpgradeInstance( curVal, newVal );
continue;
}
var curValType = curVal.GetType();
if ( staticField == newField && !curValType.IsValueType && !IsSwappedType( curValType ) )
{
// We're processing a static field in a non-swapped type, and the value type isn't
// swapped either. Try upgrading in-place.
if ( RootUpgraderGroup.TryUpgradeInstance( curVal, curVal ) )
{
continue;
}
}
// Default to creating a new instance to upgrade the old one to
newField.SetValue( null, GetNewInstance( curVal ) );
}
#if !HOTLOAD_NOCATCH
catch ( Exception e )
{
Log( e, member: newField );
}
#endif
finally
{
CurrentPath = null;
CurrentSrcField = null;
CurrentDstField = null;
}
}
}
private object GetNewInstance( object oldInstance )
{
if ( oldInstance == null )
return null;
// Check for specific InstanceUpgraders for handling special types
if ( RootUpgraderGroup.TryCreateNewInstance( oldInstance, out var newInstance ) )
{
return newInstance;
}
// We shouldn't get here, the DefaultUpgrader will always be able to create new instances
throw new NotImplementedException();
}
private void ReportSwappedOutAssemblyReference( Type type, Assembly oldAsm )
{
Log( HotloadEntryType.Warning, $"Unable to find a type substitution without referencing a swapped-out assembly ({FormatAssemblyName( oldAsm )}).", type );
}
/// <summary>
/// Make sure a candidate type substitution isn't at all defined in a swapped-out assembly.
/// This can go wrong if assembly B references assembly A, but only assembly A was swapped.
/// </summary>
private bool ValidateNewType( Type newType )
{
foreach ( var interfaceType in newType.GetInterfaces() )
{
if ( Swaps.ContainsKey( interfaceType.Assembly ) )
{
ReportSwappedOutAssemblyReference( newType, interfaceType.Assembly );
return false;
}
}
var baseType = newType;
while ( baseType != null )
{
if ( Swaps.ContainsKey( baseType.Assembly ) )
{
ReportSwappedOutAssemblyReference( newType, baseType.Assembly );
return false;
}
baseType = baseType.BaseType;
}
return true;
}
private Type GetSubstituteType( Assembly asm, Type oldType )
{
if ( asm == null )
{
// Assembly was unloaded in this swap
return null;
}
if ( GeneratedName.TryParse( oldType.Name, out var name ) )
{
switch ( name.Kind )
{
case GeneratedNameKind.LambdaDisplayClass:
case GeneratedNameKind.StateMachineType:
return GetNewNestedGeneratedType( oldType, name );
case GeneratedNameKind.ReadOnlyListType:
return GetNewReadOnlyListType( oldType );
case GeneratedNameKind.AnonymousType:
return GetNewAnonymousType( oldType );
default:
Log( HotloadEntryType.Error, $"Unhandled compiler-generated type", oldType );
return null;
}
}
if ( oldType.FullName == null )
{
if ( oldType.DeclaringType == null ) return null;
var newDeclType = GetSubstituteType( asm, oldType.DeclaringType );
if ( oldType.IsGenericTypeParameter )
{
Assert.NotNull( oldType.DeclaringType );
return newDeclType?.GetGenericArguments()[oldType.GenericParameterPosition];
}
if ( oldType.IsGenericMethodParameter )
{
Assert.NotNull( oldType.DeclaringMethod );
var newMethod = GetNewInstance( oldType.DeclaringMethod ) as MethodBase;
return newMethod?.GetGenericArguments()[oldType.GenericParameterPosition];
}
return newDeclType?.GetNestedType( oldType.Name, BindingFlags.Public | BindingFlags.NonPublic );
}
var sub = asm.GetType( oldType.FullName );
if ( sub != null )
{
if ( !ValidateNewType( sub ) )
{
return null;
}
return sub;
}
// TODO
return null;
}
/// <summary>
/// In a swapped assembly find a replacement type for this type.
/// Return null if no replacement is found.
/// </summary>
private Type GetNewType( Type oldType )
{
if ( SubstituteTypeCache.TryGetValue( oldType, out var newType ) ) return newType;
if ( IsSwappedType( oldType ) )
{
var typeInfo = oldType.GetTypeInfo();
var typeAssembly = typeInfo.Assembly;
if ( typeInfo.IsArray )
{
newType = GetNewArrayType( oldType );
}
else if ( typeInfo.IsByRef )
{
newType = GetNewByRefType( oldType );
}
else if ( typeInfo.IsConstructedGenericType )
{
newType = GetNewGenericType( oldType );
}
else
{
newType = Swaps.TryGetValue( typeAssembly, out var swapAssembly )
? GetSubstituteType( swapAssembly, oldType ) : oldType;
}
}
else if ( ValidateNewType( oldType ) )
{
newType = oldType;
}
else
{
newType = null;
}
SubstituteTypeCache.Add( oldType, newType );
return newType;
}
private Type GetNewArrayType( Type oldType )
{
var oldElemType = oldType.GetElementType();
var newElemType = GetNewType( oldElemType );
if ( newElemType == null )
{
return null;
}
if ( newElemType == oldElemType )
{
return oldType;
}
var rank = oldType.GetArrayRank();
return rank == 1 ? newElemType.MakeArrayType() : newElemType.MakeArrayType( rank );
}
private Type GetNewByRefType( Type oldType )
{
var oldElemType = oldType.GetElementType();
var newElemType = GetNewType( oldElemType );
if ( newElemType == null )
{
return null;
}
if ( newElemType == oldElemType )
{
return oldType;
}
return newElemType.MakeByRefType();
}
private Type GetNewGenericType( Type oldType )
{
var oldDefType = oldType.GetGenericTypeDefinition();
var newDefType = GetNewType( oldDefType );
if ( newDefType == null )
{
return null;
}
var madeSubstitution = newDefType != oldDefType;
var args = oldType.GetGenericArguments();
for ( var i = 0; i < args.Length; ++i )
{
var argSubType = GetNewType( args[i] );
if ( argSubType == null )
{
return null;
}
madeSubstitution |= argSubType != args[i];
args[i] = argSubType;
}
return madeSubstitution ? newDefType.MakeGenericType( args ) : oldType;
}
private bool IsSwappedType( Type type )
{
if ( type == null )
{
return false;
}
if ( type.IsArray || type.IsByRef )
{
return IsSwappedType( type.GetElementType() );
}
if ( type.IsConstructedGenericType )
{
if ( IsSwappedType( type.GetGenericTypeDefinition() ) )
{
return true;
}
return type.GetGenericArguments().Any( IsSwappedType );
}
return Swaps.ContainsKey( type.Assembly );
}
private bool AreEqualTypes( Type a, Type b )
{
if ( a == b ) return true;
if ( a == null || b == null ) return false;
if ( a.IsArray && b.IsArray )
{
if ( a.GetArrayRank() != b.GetArrayRank() )
{
return false;
}
return AreEqualTypes( a.GetElementType(), b.GetElementType() );
}
if ( a.IsByRef && b.IsByRef )
{
return AreEqualTypes( a.GetElementType(), b.GetElementType() );
}
if ( a.IsGenericType && b.IsGenericType )
{
if ( !AreEqualTypes( a.GetGenericTypeDefinition(), b.GetGenericTypeDefinition() ) )
{
return false;
}
var aArgs = a.GetGenericArguments();
var bArgs = b.GetGenericArguments();
if ( aArgs.Length != bArgs.Length )
{
return false;
}
for ( var i = 0; i < aArgs.Length; ++i )
{
if ( !AreEqualTypes( aArgs[i], bArgs[i] ) )
{
return false;
}
}
return true;
}
return false;
}
private bool AreEquivalentTypes( Type oldType, Type newType )
{
if ( oldType == newType ) return true;
if ( oldType == null || newType == null ) return false;
if ( oldType.IsArray != newType.IsArray ) return false;
if ( oldType.IsArray && newType.IsArray )
{
if ( oldType.GetArrayRank() != newType.GetArrayRank() )
{
return false;
}
return AreEquivalentTypes( oldType.GetElementType(), newType.GetElementType() );
}
if ( oldType.IsByRef != newType.IsByRef ) return false;
if ( oldType.IsByRef && newType.IsByRef )
{
return AreEquivalentTypes( oldType.GetElementType(), newType.GetElementType() );
}
if ( oldType.IsConstructedGenericType != newType.IsConstructedGenericType ) return false;
if ( oldType.IsConstructedGenericType && newType.IsConstructedGenericType )
{
if ( !AreEquivalentTypes( oldType.GetGenericTypeDefinition(), newType.GetGenericTypeDefinition() ) )
{
return false;
}
var aArgs = oldType.GetGenericArguments();
var bArgs = newType.GetGenericArguments();
if ( aArgs.Length != bArgs.Length )
{
return false;
}
for ( var i = 0; i < aArgs.Length; ++i )
{
if ( !AreEquivalentTypes( aArgs[i], bArgs[i] ) )
{
return false;
}
}
return true;
}
if ( oldType.IsGenericMethodParameter != newType.IsGenericMethodParameter ) return false;
if ( oldType.IsGenericMethodParameter && newType.IsGenericMethodParameter )
{
// TODO: constraints
return oldType.GenericParameterPosition == newType.GenericParameterPosition;
}
if ( oldType.IsGenericTypeParameter != newType.IsGenericTypeParameter ) return false;
if ( oldType.IsGenericTypeParameter && newType.IsGenericTypeParameter )
{
// TODO: constraints
return oldType.GenericParameterPosition == newType.GenericParameterPosition;
}
var subType = GetNewType( oldType );
return subType != oldType && AreEqualTypes( subType, newType );
}
private void AddResultEntry( HotloadResultEntry entry )
{
CurrentResult?.AddEntry( entry );
if ( _logger == null )
{
return;
}
using var scope = SentrySdk.PushScope();
SentrySdk.ConfigureScope( x =>
{
x.Contexts["Source Member"] = new
{
FullName = entry.MemberString,
Path = entry.Path?.ToString()
};
} );
var details = entry.Message;
if ( entry.Member != null )
{
details = $"{details}\n Member: {entry.MemberString}";
}
if ( entry.Path != null )
{
details = $"{details}\n Path: {entry.Path}";
}
switch ( entry.Type )
{
case HotloadEntryType.Trace:
_logger.Trace( details );
break;
case HotloadEntryType.Information:
_logger.Info( details );
break;
case HotloadEntryType.Warning:
_logger.Warning( entry.Exception, details );
break;
case HotloadEntryType.Error:
_logger.Error( entry.Exception, details );
break;
}
}
internal void Log( HotloadEntryType type, FormattableString message, MemberInfo member = null )
{
AddResultEntry( new HotloadResultEntry( type, message, member, TracePaths ? CurrentPath : null ) );
}
internal void Log( Exception exception, FormattableString message = null, MemberInfo member = null )
{
AddResultEntry( new HotloadResultEntry( exception, message, member, TracePaths ? CurrentPath : null ) );
}
}
}