1
1
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2
2
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
4
- #nullable enable
5
-
6
4
using System ;
7
5
using System . Collections . Concurrent ;
8
6
using System . Collections . Generic ;
9
7
using System . Linq ;
10
8
using System . Reflection ;
11
- using System . Reflection . Metadata ;
12
9
using Microsoft . DotNet . Watcher . Tools ;
13
10
14
11
namespace Microsoft . Extensions . HotReload
@@ -19,7 +16,7 @@ internal class HotReloadAgent : IDisposable
19
16
private readonly AssemblyLoadEventHandler _assemblyLoad ;
20
17
private readonly ConcurrentDictionary < Guid , IReadOnlyList < UpdateDelta > > _deltas = new ( ) ;
21
18
private readonly ConcurrentDictionary < Assembly , Assembly > _appliedAssemblies = new ( ) ;
22
- private volatile UpdateHandlerActions ? _beforeAfterUpdates ;
19
+ private volatile UpdateHandlerActions ? _handlerActions ;
23
20
24
21
public HotReloadAgent ( Action < string > log )
25
22
{
@@ -30,7 +27,7 @@ public HotReloadAgent(Action<string> log)
30
27
31
28
private void OnAssemblyLoad ( object ? _ , AssemblyLoadEventArgs eventArgs )
32
29
{
33
- _beforeAfterUpdates = null ;
30
+ _handlerActions = null ;
34
31
var loadedAssembly = eventArgs . LoadedAssembly ;
35
32
var moduleId = loadedAssembly . Modules . FirstOrDefault ( ) ? . ModuleVersionId ;
36
33
if ( moduleId is null )
@@ -45,109 +42,183 @@ private void OnAssemblyLoad(object? _, AssemblyLoadEventArgs eventArgs)
45
42
}
46
43
}
47
44
48
- private sealed class UpdateHandlerActions
45
+ internal sealed class UpdateHandlerActions
49
46
{
50
- public UpdateHandlerActions ( List < Action < Type [ ] ? > > before , List < Action < Type [ ] ? > > after )
51
- {
52
- Before = before ;
53
- After = after ;
54
- }
55
-
56
- public List < Action < Type [ ] ? > > Before { get ; }
57
- public List < Action < Type [ ] ? > > After { get ; }
47
+ public List < Action < Type [ ] ? > > Before { get ; } = new ( ) ;
48
+ public List < Action < Type [ ] ? > > After { get ; } = new ( ) ;
49
+ public List < Action < Type [ ] ? > > ClearCache { get ; } = new ( ) ;
50
+ public List < Action < Type [ ] ? > > UpdateApplication { get ; } = new ( ) ;
58
51
}
59
52
60
53
private UpdateHandlerActions GetMetadataUpdateHandlerActions ( )
61
54
{
62
- var before = new List < Action < Type [ ] ? > > ( ) ;
63
- var after = new List < Action < Type [ ] ? > > ( ) ;
64
-
65
- foreach ( var assembly in AppDomain . CurrentDomain . GetAssemblies ( ) )
55
+ // We need to execute MetadataUpdateHandlers in a well-defined order. For v1, the strategy that is used is to topologically
56
+ // sort assemblies so that handlers in a dependency are executed before the dependent (e.g. the reflection cache action
57
+ // in System.Private.CoreLib is executed before System.Text.Json clears it's own cache.)
58
+ // This would ensure that caches and updates more lower in the application stack are up to date
59
+ // before ones higher in the stack are recomputed.
60
+ var sortedAssemblies = TopologicalSort ( AppDomain . CurrentDomain . GetAssemblies ( ) ) ;
61
+ var handlerActions = new UpdateHandlerActions ( ) ;
62
+ foreach ( var assembly in sortedAssemblies )
66
63
{
67
- foreach ( var attr in assembly . GetCustomAttributes < MetadataUpdateHandlerAttribute > ( ) )
64
+ foreach ( var attr in assembly . GetCustomAttributesData ( ) )
68
65
{
69
- bool methodFound = false ;
70
- var handlerType = attr . HandlerType ;
71
-
72
- if ( GetUpdateMethod ( handlerType , "BeforeUpdate" ) is MethodInfo beforeUpdate )
66
+ // Look up the attribute by name rather than by type. This would allow netstandard targeting libraries to
67
+ // define their own copy without having to cross-compile.
68
+ if ( attr . AttributeType . FullName != "System.Reflection.Metadata.MetadataUpdateHandlerAttribute" )
73
69
{
74
- before . Add ( CreateAction ( beforeUpdate ) ) ;
75
- methodFound = true ;
70
+ continue ;
76
71
}
77
72
78
- if ( GetUpdateMethod ( handlerType , "AfterUpdate" ) is MethodInfo afterUpdate )
73
+ IList < CustomAttributeTypedArgument > ctorArgs = attr . ConstructorArguments ;
74
+ if ( ctorArgs . Count != 1 ||
75
+ ctorArgs [ 0 ] . Value is not Type handlerType )
79
76
{
80
- after . Add ( CreateAction ( afterUpdate ) ) ;
81
- methodFound = true ;
77
+ _log ( $ "' { attr } ' found with invalid arguments." ) ;
78
+ continue ;
82
79
}
83
80
84
- if ( ! methodFound )
81
+ GetHandlerActions ( handlerActions , handlerType ) ;
82
+ }
83
+ }
84
+
85
+ return handlerActions ;
86
+ }
87
+
88
+ internal void GetHandlerActions ( UpdateHandlerActions handlerActions , Type handlerType )
89
+ {
90
+ bool methodFound = false ;
91
+
92
+ // Temporarily allow BeforeUpdate and AfterUpdate to be invoked until
93
+ // everything is updated to use the new names.
94
+ if ( GetUpdateMethod ( handlerType , "BeforeUpdate" ) is MethodInfo beforeUpdate )
95
+ {
96
+ handlerActions . Before . Add ( CreateAction ( beforeUpdate ) ) ;
97
+ methodFound = true ;
98
+ }
99
+
100
+ if ( GetUpdateMethod ( handlerType , "AfterUpdate" ) is MethodInfo afterUpdate )
101
+ {
102
+ handlerActions . After . Add ( CreateAction ( afterUpdate ) ) ;
103
+ methodFound = true ;
104
+ }
105
+
106
+ if ( GetUpdateMethod ( handlerType , "ClearCache" ) is MethodInfo clearCache )
107
+ {
108
+ handlerActions . ClearCache . Add ( CreateAction ( clearCache ) ) ;
109
+ methodFound = true ;
110
+ }
111
+
112
+ if ( GetUpdateMethod ( handlerType , "UpdateApplication" ) is MethodInfo updateApplication )
113
+ {
114
+ handlerActions . UpdateApplication . Add ( CreateAction ( updateApplication ) ) ;
115
+ methodFound = true ;
116
+ }
117
+
118
+ if ( ! methodFound )
119
+ {
120
+ _log ( $ "No invokable methods found on metadata handler type '{ handlerType } '. " +
121
+ $ "Allowed methods are ClearCache, UpdateApplication") ;
122
+ }
123
+
124
+ Action < Type [ ] ? > CreateAction ( MethodInfo update )
125
+ {
126
+ Action < Type [ ] ? > action = update . CreateDelegate < Action < Type [ ] ? > > ( ) ;
127
+ return types =>
128
+ {
129
+ try
85
130
{
86
- _log ( $ "No BeforeUpdate or AfterUpdate method found on ' { handlerType } '." ) ;
131
+ action ( types ) ;
87
132
}
133
+ catch ( Exception ex )
134
+ {
135
+ _log ( $ "Exception from '{ action } ': { ex } ") ;
136
+ }
137
+ } ;
138
+ }
139
+
140
+ MethodInfo ? GetUpdateMethod ( Type handlerType , string name )
141
+ {
142
+ if ( handlerType . GetMethod ( name , BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static , new [ ] { typeof ( Type [ ] ) } ) is MethodInfo updateMethod &&
143
+ updateMethod . ReturnType == typeof ( void ) )
144
+ {
145
+ return updateMethod ;
146
+ }
88
147
89
- Action < Type [ ] ? > CreateAction ( MethodInfo update )
148
+ foreach ( MethodInfo method in handlerType . GetMethods ( BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static | BindingFlags . Instance ) )
149
+ {
150
+ if ( method . Name == name )
90
151
{
91
- Action < Type [ ] ? > action = update . CreateDelegate < Action < Type [ ] ? > > ( ) ;
92
- return types =>
93
- {
94
- try
95
- {
96
- action ( types ) ;
97
- }
98
- catch ( Exception ex )
99
- {
100
- _log ( $ "Exception from '{ action } ': { ex } ") ;
101
- }
102
- } ;
152
+ _log ( $ "Type '{ handlerType } ' has method '{ method } ' that does not match the required signature.") ;
153
+ break ;
103
154
}
155
+ }
156
+
157
+ return null ;
158
+ }
159
+ }
160
+
161
+ internal static List < Assembly > TopologicalSort ( Assembly [ ] assemblies )
162
+ {
163
+ var sortedAssemblies = new List < Assembly > ( assemblies . Length ) ;
164
+
165
+ var visited = new HashSet < string > ( StringComparer . Ordinal ) ;
104
166
105
- MethodInfo ? GetUpdateMethod ( Type handlerType , string name )
167
+ foreach ( var assembly in assemblies )
168
+ {
169
+ Visit ( assemblies , assembly , sortedAssemblies , visited ) ;
170
+ }
171
+
172
+ static void Visit ( Assembly [ ] assemblies , Assembly assembly , List < Assembly > sortedAssemblies , HashSet < string > visited )
173
+ {
174
+ var assemblyIdentifier = assembly . GetName ( ) . Name ! ;
175
+ if ( ! visited . Add ( assemblyIdentifier ) )
176
+ {
177
+ return ;
178
+ }
179
+
180
+ foreach ( var dependencyName in assembly . GetReferencedAssemblies ( ) )
181
+ {
182
+ var dependency = Array . Find ( assemblies , a => a . GetName ( ) . Name == dependencyName . Name ) ;
183
+ if ( dependency is not null )
106
184
{
107
- if ( handlerType . GetMethod ( name , BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static , new [ ] { typeof ( Type [ ] ) } ) is MethodInfo updateMethod &&
108
- updateMethod . ReturnType == typeof ( void ) )
109
- {
110
- return updateMethod ;
111
- }
112
-
113
- foreach ( MethodInfo method in handlerType . GetMethods ( BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static | BindingFlags . Instance ) )
114
- {
115
- if ( method . Name == name )
116
- {
117
- _log ( $ "Type '{ handlerType } ' has method '{ method } ' that does not match the required signature.") ;
118
- break ;
119
- }
120
- }
121
-
122
- return null ;
185
+ Visit ( assemblies , dependency , sortedAssemblies , visited ) ;
123
186
}
124
187
}
188
+
189
+ sortedAssemblies . Add ( assembly ) ;
125
190
}
126
191
127
- return new UpdateHandlerActions ( before , after ) ;
192
+ return sortedAssemblies ;
128
193
}
129
194
130
195
public void ApplyDeltas ( IReadOnlyList < UpdateDelta > deltas )
131
196
{
132
197
try
133
198
{
134
- UpdateHandlerActions beforeAfterUpdates = _beforeAfterUpdates ??= GetMetadataUpdateHandlerActions ( ) ;
199
+ // Defer discovering the receiving deltas until the first hot reload delta.
200
+ // This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
201
+ _handlerActions ??= GetMetadataUpdateHandlerActions ( ) ;
202
+ var handlerActions = _handlerActions ;
203
+
204
+ // TODO: Get types to pass in
205
+ Type [ ] ? updatedTypes = null ;
135
206
136
- beforeAfterUpdates . Before . ForEach ( b => b ( null ) ) ; // TODO: Get types to pass in
207
+ handlerActions . Before . ForEach ( b => b ( updatedTypes ) ) ;
137
208
138
- foreach ( var item in deltas )
209
+ for ( var i = 0 ; i < deltas . Count ; i ++ )
139
210
{
211
+ var item = deltas [ i ] ;
140
212
var assembly = AppDomain . CurrentDomain . GetAssemblies ( ) . FirstOrDefault ( a => a . Modules . FirstOrDefault ( ) is Module m && m . ModuleVersionId == item . ModuleId ) ;
141
213
if ( assembly is not null )
142
214
{
143
215
System . Reflection . Metadata . AssemblyExtensions . ApplyUpdate ( assembly , item . MetadataDelta , item . ILDelta , ReadOnlySpan < byte > . Empty ) ;
144
216
}
145
217
}
146
218
147
- // Defer discovering the receiving deltas until the first hot reload delta.
148
- // This should give enough opportunity for AppDomain.GetAssemblies() to be sufficiently populated.
149
-
150
- beforeAfterUpdates . After . ForEach ( a => a ( null ) ) ; // TODO: Get types to pass in
219
+ handlerActions . ClearCache . ForEach ( a => a ( updatedTypes ) ) ;
220
+ handlerActions . After . ForEach ( c => c ( updatedTypes ) ) ;
221
+ handlerActions . UpdateApplication . ForEach ( a => a ( updatedTypes ) ) ;
151
222
152
223
_log ( "Deltas applied." ) ;
153
224
}
0 commit comments