33using System . IO ;
44using System . Linq ;
55using System . Management . Automation ;
6+ using System . Management . Automation . Language ;
67
78namespace DSCParser . PSDSC
89{
@@ -14,14 +15,57 @@ namespace DSCParser.PSDSC
1415 /// PowerShell rebuilds this registration from scratch every time it parses a configuration that
1516 /// contains an Import-DSCResource statement, probing the file system for a schema file per
1617 /// resource. On a module the size of Microsoft365DSC that dominates the cost of parsing. Keeping
17- /// the registration for the lifetime of the process lets callers strip the Import-DSCResource
18- /// statement and skip that work entirely.
18+ /// the registration alive lets callers strip the Import-DSCResource statement and skip that
19+ /// work entirely.
1920 /// </remarks>
2021 public static class DscKeywordRegistry
2122 {
22- private static readonly HashSet < string > ImportedModules = new ( StringComparer . OrdinalIgnoreCase ) ;
23+ [ ThreadStatic ]
24+ private static HashSet < string > ? t_importedModules ;
2325
24- private static bool _defaultKeywordsLoaded ;
26+ private static HashSet < string > ImportedModules => t_importedModules ??= new ( StringComparer . OrdinalIgnoreCase ) ;
27+
28+ [ ThreadStatic ]
29+ private static bool t_defaultKeywordsLoaded ;
30+
31+ [ ThreadStatic ]
32+ private static bool t_engineUnsupported ;
33+
34+ [ ThreadStatic ]
35+ private static List < DynamicKeyword > ? t_defaultTableKeywords ;
36+
37+ [ ThreadStatic ]
38+ private static bool t_staleWarningIssued ;
39+
40+ [ ThreadStatic ]
41+ private static int t_expectedCachedKeywordCount ;
42+
43+ // Registered into the class cache by LoadDefaultCimKeywords and dies with it, so its
44+ // absence while the bookkeeping claims otherwise means the engine wiped the cache.
45+ private const string ClassCacheSentinel = "OMI_ConfigurationDocument" ;
46+
47+ // Lives in the DynamicKeyword table (not the class cache) and is re-added by
48+ // LoadDefaultCimKeywords, so it tells whether the table currently holds the defaults.
49+ private const string NodeKeyword = "Node" ;
50+
51+ private static bool EngineStateIsFresh
52+ {
53+ get
54+ {
55+ if ( ! DscClassCacheReflection . HasCachedClass ( ClassCacheSentinel ) )
56+ {
57+ return false ;
58+ }
59+
60+ return t_expectedCachedKeywordCount == 0
61+ || CurrentCachedKeywordCount ( ) >= t_expectedCachedKeywordCount ;
62+ }
63+ }
64+
65+ private static int CurrentCachedKeywordCount ( )
66+ {
67+ return DscClassCacheReflection . GetCachedKeywords ( ) ? . Count ( ) ?? 0 ;
68+ }
2569
2670 /// <summary>
2771 /// Registers the resources of the supplied modules, skipping modules already registered.
@@ -47,6 +91,8 @@ public static void ImportModules(IEnumerable<PSModuleInfo> modules)
4791 // A later request that names no version is satisfied by any registered version.
4892 _ = ImportedModules . Add ( GetModuleKey ( module . Name , null ) ) ;
4993 }
94+
95+ t_expectedCachedKeywordCount = CurrentCachedKeywordCount ( ) ;
5096 }
5197
5298 /// <summary>
@@ -62,6 +108,9 @@ public static bool EnsureRegistered(string moduleName, Version? version)
62108 return false ;
63109 }
64110
111+ // Heals a wiped engine cache before the fast path below can return a stale answer.
112+ EnsureDefaultKeywordsLoaded ( ) ;
113+
65114 if ( ImportedModules . Contains ( GetModuleKey ( moduleName , version ) ) )
66115 {
67116 return true ;
@@ -79,25 +128,132 @@ public static bool EnsureRegistered(string moduleName, Version? version)
79128 }
80129
81130 /// <summary>
82- /// Drops every registered keyword and the underlying class cache.
131+ /// Drops every registered keyword and the underlying class cache on the current thread .
83132 /// </summary>
84133 public static void Reset ( )
85134 {
86135 ImportedModules . Clear ( ) ;
87- _defaultKeywordsLoaded = false ;
136+ t_defaultKeywordsLoaded = false ;
137+ t_engineUnsupported = false ;
138+ t_defaultTableKeywords = null ;
139+ t_expectedCachedKeywordCount = 0 ;
88140 DscClassCacheReflection . ResetDynamicKeywords ( ) ;
89141 DscClassCacheReflection . ClearCache ( ) ;
90142 }
91143
92144 internal static void EnsureDefaultKeywordsLoaded ( )
93145 {
94- if ( _defaultKeywordsLoaded )
146+ _ = HandleExternalCacheReset ( ) ;
147+
148+ if ( t_defaultKeywordsLoaded )
95149 {
96150 return ;
97151 }
98152
99153 DscClassCacheReflection . LoadDefaultCimKeywords ( ) ;
100- _defaultKeywordsLoaded = true ;
154+ t_defaultKeywordsLoaded = true ;
155+ t_engineUnsupported = ! EngineStateIsFresh ;
156+
157+ List < DynamicKeyword > snapshot = [ ] ;
158+ IEnumerable < string > defaultNames =
159+ ( DscClassCacheReflection . GetCachedKeywords ( ) ? . Select ( k => k . Keyword ) ?? [ ] )
160+ . Concat ( [ NodeKeyword , "Import-DscResource" ] ) ;
161+ foreach ( string name in defaultNames . Distinct ( StringComparer . OrdinalIgnoreCase ) )
162+ {
163+ if ( DynamicKeyword . GetKeyword ( name ) is { } keyword )
164+ {
165+ snapshot . Add ( keyword ) ;
166+ }
167+ }
168+
169+ t_defaultTableKeywords = snapshot ;
170+ t_expectedCachedKeywordCount = CurrentCachedKeywordCount ( ) ;
171+ }
172+
173+ /// <summary>
174+ /// The DSC engine clears its internal class cache and DynamicKeyword table whenever a
175+ /// Configuration block is compiled to MOF in this process. This means that this class
176+ /// then no longer matches engine state, resulting in no resource results although
177+ /// shown as imported.
178+ /// </summary>
179+ public static bool HandleExternalCacheReset ( )
180+ {
181+ if ( t_engineUnsupported || ! DscClassCacheReflection . IsDscClassCacheAvailable )
182+ {
183+ return false ;
184+ }
185+
186+ if ( ImportedModules . Count == 0 && ! t_defaultKeywordsLoaded )
187+ {
188+ return false ;
189+ }
190+
191+ if ( EngineStateIsFresh )
192+ {
193+ return false ;
194+ }
195+
196+ if ( ! t_staleWarningIssued )
197+ {
198+ t_staleWarningIssued = true ;
199+ DscResourceService . ReportWarning (
200+ "The PowerShell engine cleared its internal DSC caches (typically caused by compiling a "
201+ + "Configuration to MOF). DSCParser re-imported its DSC resource keywords automatically." ) ;
202+ }
203+
204+ Reset ( ) ;
205+ return true ;
206+ }
207+
208+ /// <summary>
209+ /// Fills the engine's DynamicKeyword table with the default CIM keywords (including Node)
210+ /// and every keyword the class cache holds, so a configuration can be parsed without any
211+ /// Import-DscResource statement. Pair with <see cref="ClearKeywordTable"/> once parsing is
212+ /// done.
213+ /// </summary>
214+ public static void MaterializeKeywordTable ( )
215+ {
216+ EnsureDefaultKeywordsLoaded ( ) ;
217+
218+ List < DynamicKeyword > ? cachedKeywords = DscClassCacheReflection . GetCachedKeywords ( ) ? . ToList ( ) ;
219+
220+ if ( ! DynamicKeyword . ContainsKeyword ( NodeKeyword ) )
221+ {
222+ if ( t_defaultTableKeywords is { Count : > 0 } defaults )
223+ {
224+ foreach ( DynamicKeyword keyword in defaults )
225+ {
226+ if ( ! DynamicKeyword . ContainsKeyword ( keyword . Keyword ) )
227+ {
228+ DynamicKeyword . AddKeyword ( keyword ) ;
229+ }
230+ }
231+ }
232+ else if ( ImportedModules . Count == 0 )
233+ {
234+ DscClassCacheReflection . LoadDefaultCimKeywords ( ) ;
235+ }
236+ }
237+
238+ if ( cachedKeywords is not null )
239+ {
240+ foreach ( DynamicKeyword keyword in cachedKeywords )
241+ {
242+ if ( ! DynamicKeyword . ContainsKeyword ( keyword . Keyword ) )
243+ {
244+ DynamicKeyword . AddKeyword ( keyword ) ;
245+ }
246+ }
247+ }
248+ }
249+
250+ /// <summary>
251+ /// Empties the engine's DynamicKeyword table while keeping the class cache. Every public
252+ /// operation must end with this.
253+ /// </summary>
254+ public static void ClearKeywordTable ( )
255+ {
256+ DscClassCacheReflection . ResetDynamicKeywords ( ) ;
101257 }
102258
103259 private static string GetModuleKey ( string moduleName , Version ? version )
0 commit comments