MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Buffers;5using System.Collections;6using System.Collections.Concurrent;7using System.Collections.Generic;8using System.Diagnostics;9using System.Globalization;10using System.IO;11using System.Management.Automation.Internal;12using System.Management.Automation.Language;13using System.Management.Automation.Runspaces;14using System.Text;15using System.Threading;16using System.Threading.Tasks;17 18using Microsoft.PowerShell.Commands;19 20namespace System.Management.Automation21{22 /// <summary>23 /// Class to manage the caching of analysis data.24 /// For performance, module command caching is flattened after discovery. Many modules have nested25 /// modules that can only be resolved at runtime - for example,26 /// script modules that declare: $env:PATH += "; $psScriptRoot". When27 /// doing initial analysis, we include these in 'ExportedCommands'.28 /// Changes to these type of modules will not be re-analyzed, unless the user re-imports the module,29 /// or runs Get-Module -List.30 /// </summary>31 internal static class AnalysisCache32 {33 private static readonly AnalysisCacheData s_cacheData = AnalysisCacheData.Get();34 35 // This dictionary shouldn't see much use, so low concurrency and capacity36 private static readonly ConcurrentDictionary<string, string> s_modulesBeingAnalyzed =37 new(concurrencyLevel: 1, capacity: 2, StringComparer.OrdinalIgnoreCase);38 39 internal static readonly SearchValues<char> InvalidCommandNameCharacters = SearchValues.Create("#,(){}[]&/\\$^;:\"'<>|?@`*%+=~");40 41 internal static bool ContainsInvalidCommandNameCharacters(ReadOnlySpan<char> text)42 => text.ContainsAny(InvalidCommandNameCharacters);43 44 internal static ConcurrentDictionary<string, CommandTypes> GetExportedCommands(string modulePath, bool testOnly, ExecutionContext context)45 {46 bool etwEnabled = CommandDiscoveryEventSource.Log.IsEnabled();47 if (etwEnabled) CommandDiscoveryEventSource.Log.GetModuleExportedCommandsStart(modulePath);48 49 DateTime lastWriteTime;50 ModuleCacheEntry moduleCacheEntry;51 if (GetModuleEntryFromCache(modulePath, out lastWriteTime, out moduleCacheEntry))52 {53 if (etwEnabled) CommandDiscoveryEventSource.Log.GetModuleExportedCommandsStop(modulePath);54 return moduleCacheEntry.Commands;55 }56 57 ConcurrentDictionary<string, CommandTypes> result = null;58 59 if (!testOnly)60 {61 var extension = Path.GetExtension(modulePath);62 if (extension.Equals(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase))63 {64 result = AnalyzeManifestModule(modulePath, context, lastWriteTime, etwEnabled);65 }66 else if (extension.Equals(StringLiterals.PowerShellModuleFileExtension, StringComparison.OrdinalIgnoreCase))67 {68 result = AnalyzeScriptModule(modulePath, context, lastWriteTime);69 }70 else if (extension.Equals(StringLiterals.PowerShellCmdletizationFileExtension, StringComparison.OrdinalIgnoreCase))71 {72 result = AnalyzeCdxmlModule(modulePath, context, lastWriteTime);73 }74 else if (extension.Equals(StringLiterals.PowerShellILAssemblyExtension, StringComparison.OrdinalIgnoreCase))75 {76 result = AnalyzeDllModule(modulePath, context, lastWriteTime);77 }78 else if (extension.Equals(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase))79 {80 result = AnalyzeDllModule(modulePath, context, lastWriteTime);81 }82 }83 84 if (result != null)85 {86 s_cacheData.QueueSerialization();87 ModuleIntrinsics.Tracer.WriteLine("Returning {0} exported commands.", result.Count);88 }89 else90 {91 ModuleIntrinsics.Tracer.WriteLine("Returning NULL for exported commands.");92 }93 94 if (etwEnabled) CommandDiscoveryEventSource.Log.GetModuleExportedCommandsStop(modulePath);95 return result;96 }97 98 private static ConcurrentDictionary<string, CommandTypes> AnalyzeManifestModule(string modulePath, ExecutionContext context, DateTime lastWriteTime, bool etwEnabled)99 {100 ConcurrentDictionary<string, CommandTypes> result = null;101 try102 {103 var moduleManifestProperties = PsUtils.GetModuleManifestProperties(modulePath, PsUtils.FastModuleManifestAnalysisPropertyNames);104 if (moduleManifestProperties != null)105 {106 if (!Configuration.PowerShellConfig.Instance.IsImplicitWinCompatEnabled() && ModuleIsEditionIncompatible(modulePath, moduleManifestProperties))107 {108 ModuleIntrinsics.Tracer.WriteLine($"Module lies on the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}");109 return null;110 }111 112 Version version;113 if (ModuleUtils.IsModuleInVersionSubdirectory(modulePath, out version))114 {115 var versionInManifest = LanguagePrimitives.ConvertTo<Version>(moduleManifestProperties["ModuleVersion"]);116 if (version != versionInManifest)117 {118 ModuleIntrinsics.Tracer.WriteLine("ModuleVersion in manifest does not match versioned module directory, skipping module: {0}", modulePath);119 return null;120 }121 }122 123 result = new ConcurrentDictionary<string, CommandTypes>(3, moduleManifestProperties.Count, StringComparer.OrdinalIgnoreCase);124 125 var sawWildcard = false;126 var hadCmdlets = AddPsd1EntryToResult(result, moduleManifestProperties["CmdletsToExport"], CommandTypes.Cmdlet, ref sawWildcard);127 var hadFunctions = AddPsd1EntryToResult(result, moduleManifestProperties["FunctionsToExport"], CommandTypes.Function, ref sawWildcard);128 var hadAliases = AddPsd1EntryToResult(result, moduleManifestProperties["AliasesToExport"], CommandTypes.Alias, ref sawWildcard);129 130 var analysisSucceeded = hadCmdlets && hadFunctions && hadAliases;131 132 if (!analysisSucceeded && !sawWildcard && (hadCmdlets || hadFunctions))133 {134 // If we're missing CmdletsToExport, that might still be OK, but only if we have a script module.135 // Likewise, if we're missing FunctionsToExport, that might be OK, but only if we have a binary module.136 137 analysisSucceeded = !CheckModulesTypesInManifestAgainstExportedCommands(moduleManifestProperties, hadCmdlets, hadFunctions, hadAliases);138 }139 140 if (analysisSucceeded)141 {142 var moduleCacheEntry = new ModuleCacheEntry143 {144 ModulePath = modulePath,145 LastWriteTime = lastWriteTime,146 Commands = result,147 TypesAnalyzed = false,148 Types = new ConcurrentDictionary<string, TypeAttributes>(1, 8, StringComparer.OrdinalIgnoreCase)149 };150 s_cacheData.Entries[modulePath] = moduleCacheEntry;151 }152 else153 {154 result = null;155 }156 }157 }158 catch (Exception e)159 {160 if (etwEnabled) CommandDiscoveryEventSource.Log.ModuleManifestAnalysisException(modulePath, e.Message);161 // Ignore the errors, proceed with the usual module analysis162 ModuleIntrinsics.Tracer.WriteLine("Exception on fast-path analysis of module {0}", modulePath);163 }164 165 if (etwEnabled) CommandDiscoveryEventSource.Log.ModuleManifestAnalysisResult(modulePath, result != null);166 167 return result ?? AnalyzeTheOldWay(modulePath, context, lastWriteTime);168 }169 170 /// <summary>171 /// Check if a module is compatible with the current PSEdition given its path and its manifest properties.172 /// </summary>173 /// <param name="modulePath">The path to the module.</param>174 /// <param name="moduleManifestProperties">The properties of the module's manifest.</param>175 /// <returns></returns>176 internal static bool ModuleIsEditionIncompatible(string modulePath, Hashtable moduleManifestProperties)177 {178#if UNIX179 return false;180#else181 if (!ModuleUtils.IsOnSystem32ModulePath(modulePath))182 {183 return false;184 }185 186 if (!moduleManifestProperties.ContainsKey("CompatiblePSEditions"))187 {188 return true;189 }190 191 return !Utils.IsPSEditionSupported(LanguagePrimitives.ConvertTo<string[]>(moduleManifestProperties["CompatiblePSEditions"]));192#endif193 }194 195 internal static bool ModuleAnalysisViaGetModuleRequired(object modulePathObj, bool hadCmdlets, bool hadFunctions, bool hadAliases)196 {197 if (modulePathObj is not string modulePath)198 return true;199 200 if (modulePath.EndsWith(StringLiterals.PowerShellModuleFileExtension, StringComparison.OrdinalIgnoreCase))201 {202 // A script module can't exactly define cmdlets, but it can import a binary module (as nested), so203 // it can indirectly define cmdlets. And obviously a script module can define functions and aliases.204 // If we got here, one of those is missing, so analysis is required.205 return true;206 }207 208 if (modulePath.EndsWith(StringLiterals.PowerShellCmdletizationFileExtension, StringComparison.OrdinalIgnoreCase))209 {210 // A cdxml module can only define functions and aliases, so if we have both, no more analysis is required.211 return !hadFunctions || !hadAliases;212 }213 214 if (modulePath.EndsWith(StringLiterals.PowerShellILAssemblyExtension, StringComparison.OrdinalIgnoreCase))215 {216 // A dll just exports cmdlets, so if the manifest doesn't explicitly export any cmdlets,217 // more analysis is required. If the module exports aliases, we can't discover that analyzing218 // the binary, so aliases are always required to be explicit (no wildcards) in the manifest.219 return !hadCmdlets;220 }221 222 if (modulePath.EndsWith(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase))223 {224 // A dll just exports cmdlets, so if the manifest doesn't explicitly export any cmdlets,225 // more analysis is required. If the module exports aliases, we can't discover that analyzing226 // the binary, so aliases are always required to be explicit (no wildcards) in the manifest.227 return !hadCmdlets;228 }229 230 // Any other extension (or no extension), just assume the worst and analyze the module231 return true;232 }233 234 // Returns true if we need to analyze the manifest module in Get-Module because235 // our quick and dirty module manifest analysis is missing something not easily236 // discovered.237 //238 // TODO - psm1 modules are actually easily handled, so if we only saw a psm1 here,239 // we should just analyze it and not fall back on Get-Module -List.240 private static bool CheckModulesTypesInManifestAgainstExportedCommands(Hashtable moduleManifestProperties, bool hadCmdlets, bool hadFunctions, bool hadAliases)241 {242 var rootModule = moduleManifestProperties["RootModule"];243 if (rootModule != null && ModuleAnalysisViaGetModuleRequired(rootModule, hadCmdlets, hadFunctions, hadAliases))244 return true;245 246 var moduleToProcess = moduleManifestProperties["ModuleToProcess"];247 if (moduleToProcess != null && ModuleAnalysisViaGetModuleRequired(moduleToProcess, hadCmdlets, hadFunctions, hadAliases))248 return true;249 250 var nestedModules = moduleManifestProperties["NestedModules"];251 if (nestedModules != null)252 {253 var nestedModule = nestedModules as string;254 if (nestedModule != null)255 {256 return ModuleAnalysisViaGetModuleRequired(nestedModule, hadCmdlets, hadFunctions, hadAliases);257 }258 259 if (nestedModules is not object[] nestedModuleArray)260 return true;261 262 foreach (var element in nestedModuleArray)263 {264 if (ModuleAnalysisViaGetModuleRequired(element, hadCmdlets, hadFunctions, hadAliases))265 return true;266 }267 }268 269 return false;270 }271 272 private static bool AddPsd1EntryToResult(ConcurrentDictionary<string, CommandTypes> result, string command, CommandTypes commandTypeToAdd, ref bool sawWildcard)273 {274 if (WildcardPattern.ContainsWildcardCharacters(command))275 {276 sawWildcard = true;277 return false;278 }279 280 // An empty string is one way of saying "no exported commands".281 if (command.Length != 0)282 {283 CommandTypes commandTypes;284 if (result.TryGetValue(command, out commandTypes))285 {286 commandTypes |= commandTypeToAdd;287 }288 else289 {290 commandTypes = commandTypeToAdd;291 }292 293 result[command] = commandTypes;294 }295 296 return true;297 }298 299 private static bool AddPsd1EntryToResult(ConcurrentDictionary<string, CommandTypes> result, object value, CommandTypes commandTypeToAdd, ref bool sawWildcard)300 {301 string command = value as string;302 if (command != null)303 {304 return AddPsd1EntryToResult(result, command, commandTypeToAdd, ref sawWildcard);305 }306 307 object[] commands = value as object[];308 if (commands != null)309 {310 foreach (var o in commands)311 {312 if (!AddPsd1EntryToResult(result, o, commandTypeToAdd, ref sawWildcard))313 return false;314 }315 316 // An empty array is still success, that's how a manifest declares that317 // no entries are exported (unlike the lack of an entry, or $null).318 return true;319 }320 321 // Unknown type, let Get-Module -List deal with this manifest322 return false;323 }324 325 private static ConcurrentDictionary<string, CommandTypes> AnalyzeScriptModule(string modulePath, ExecutionContext context, DateTime lastWriteTime)326 {327 var scriptAnalysis = ScriptAnalysis.Analyze(modulePath, context);328 if (scriptAnalysis == null)329 {330 return null;331 }332 333 List<WildcardPattern> scriptAnalysisPatterns = new List<WildcardPattern>();334 foreach (string discoveredCommandFilter in scriptAnalysis.DiscoveredCommandFilters)335 {336 scriptAnalysisPatterns.Add(new WildcardPattern(discoveredCommandFilter));337 }338 339 var result = new ConcurrentDictionary<string, CommandTypes>(3,340 scriptAnalysis.DiscoveredExports.Count + scriptAnalysis.DiscoveredAliases.Count,341 StringComparer.OrdinalIgnoreCase);342 343 // Add any directly discovered exports344 foreach (var command in scriptAnalysis.DiscoveredExports)345 {346 if (SessionStateUtilities.MatchesAnyWildcardPattern(command, scriptAnalysisPatterns, true))347 {348 if (!ContainsInvalidCommandNameCharacters(command))349 {350 result[command] = CommandTypes.Function;351 }352 }353 }354 355 // Add the discovered aliases356 foreach (var pair in scriptAnalysis.DiscoveredAliases)357 {358 var commandName = pair.Key;359 // These are already filtered360 if (!ContainsInvalidCommandNameCharacters(commandName))361 {362 result.AddOrUpdate(commandName, CommandTypes.Alias,363 static (_, existingCommandType) => existingCommandType | CommandTypes.Alias);364 }365 }366 367 // Add any files in PsScriptRoot if it added itself to the path368 if (scriptAnalysis.AddsSelfToPath)369 {370 string baseDirectory = Path.GetDirectoryName(modulePath);371 372 try373 {374 foreach (string item in Directory.EnumerateFiles(baseDirectory, "*.ps1"))375 {376 var command = Path.GetFileNameWithoutExtension(item);377 result.AddOrUpdate(command, CommandTypes.ExternalScript,378 static (_, existingCommandType) => existingCommandType | CommandTypes.ExternalScript);379 }380 }381 catch (UnauthorizedAccessException)382 {383 // Consume this exception here384 }385 }386 387 ConcurrentDictionary<string, TypeAttributes> exportedClasses = new(388 concurrencyLevel: 1,389 capacity: scriptAnalysis.DiscoveredClasses.Count,390 StringComparer.OrdinalIgnoreCase);391 foreach (var exportedClass in scriptAnalysis.DiscoveredClasses)392 {393 exportedClasses[exportedClass.Name] = exportedClass.TypeAttributes;394 }395 396 var moduleCacheEntry = new ModuleCacheEntry397 {398 ModulePath = modulePath,399 LastWriteTime = lastWriteTime,400 Commands = result,401 TypesAnalyzed = true,402 Types = exportedClasses403 };404 s_cacheData.Entries[modulePath] = moduleCacheEntry;405 406 return result;407 }408 409 private static ConcurrentDictionary<string, CommandTypes> AnalyzeCdxmlModule(string modulePath, ExecutionContext context, DateTime lastWriteTime)410 {411 return AnalyzeTheOldWay(modulePath, context, lastWriteTime);412 }413 414 private static ConcurrentDictionary<string, CommandTypes> AnalyzeDllModule(string modulePath, ExecutionContext context, DateTime lastWriteTime)415 {416 return AnalyzeTheOldWay(modulePath, context, lastWriteTime);417 }418 419 private static ConcurrentDictionary<string, CommandTypes> AnalyzeTheOldWay(string modulePath, ExecutionContext context, DateTime lastWriteTime)420 {421 try422 {423 // If we're already analyzing this module, let the recursion bottom out.424 if (!s_modulesBeingAnalyzed.TryAdd(modulePath, modulePath))425 {426 ModuleIntrinsics.Tracer.WriteLine("{0} is already being analyzed. Exiting.", modulePath);427 return null;428 }429 430 // Record that we're analyzing this specific module so that we don't get stuck in recursion431 ModuleIntrinsics.Tracer.WriteLine("Started analysis: {0}", modulePath);432 CallGetModuleDashList(context, modulePath);433 434 ModuleCacheEntry moduleCacheEntry;435 if (GetModuleEntryFromCache(modulePath, out lastWriteTime, out moduleCacheEntry))436 {437 return moduleCacheEntry.Commands;438 }439 }440 catch (Exception e)441 {442 ModuleIntrinsics.Tracer.WriteLine("Module analysis generated an exception: {0}", e);443 444 // Catch-all OK, third-party call-out.445 }446 finally447 {448 ModuleIntrinsics.Tracer.WriteLine("Finished analysis: {0}", modulePath);449 s_modulesBeingAnalyzed.TryRemove(modulePath, out modulePath);450 }451 452 return null;453 }454 455 /// <summary>456 /// Return the exported types for a specific module.457 /// If the module is already cache, return from cache, else cache the module.458 /// Also re-cache the module if the cached item is stale.459 /// </summary>460 /// <param name="modulePath">Path to the module to get exported types from.</param>461 /// <param name="context">Current Context.</param>462 /// <returns></returns>463 internal static ConcurrentDictionary<string, TypeAttributes> GetExportedClasses(string modulePath, ExecutionContext context)464 {465 DateTime lastWriteTime;466 ModuleCacheEntry moduleCacheEntry;467 if (GetModuleEntryFromCache(modulePath, out lastWriteTime, out moduleCacheEntry) && moduleCacheEntry.TypesAnalyzed)468 {469 return moduleCacheEntry.Types;470 }471 472 try473 {474 CallGetModuleDashList(context, modulePath);475 if (GetModuleEntryFromCache(modulePath, out lastWriteTime, out moduleCacheEntry))476 {477 return moduleCacheEntry.Types;478 }479 }480 catch (Exception e)481 {482 ModuleIntrinsics.Tracer.WriteLine("Module analysis generated an exception: {0}", e);483 484 // Catch-all OK, third-party call-out.485 }486 487 return null;488 }489 490 internal static void CacheModuleExports(PSModuleInfo module, ExecutionContext context)491 {492 ModuleIntrinsics.Tracer.WriteLine("Requested caching for {0}", module.Name);493 494 // Don't cache incompatible modules on the system32 module path even if loaded with495 // -SkipEditionCheck, since it will break subsequent sessions496 if (!Configuration.PowerShellConfig.Instance.IsImplicitWinCompatEnabled() && !module.IsConsideredEditionCompatible)497 {498 ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached.");499 return;500 }501 502 DateTime lastWriteTime;503 ModuleCacheEntry moduleCacheEntry;504 GetModuleEntryFromCache(module.Path, out lastWriteTime, out moduleCacheEntry);505 506 var realExportedCommands = module.ExportedCommands;507 var realExportedClasses = module.GetExportedTypeDefinitions();508 ConcurrentDictionary<string, CommandTypes> exportedCommands;509 ConcurrentDictionary<string, TypeAttributes> exportedClasses;510 511 // First see if the existing module info is sufficient. GetModuleEntryFromCache does LastWriteTime512 // verification, so this will also return nothing if the cache is out of date or corrupt.513 if (moduleCacheEntry != null)514 {515 bool needToUpdate = false;516 517 // We need to iterate and check as exportedCommands will have more item as it can have aliases as well.518 exportedCommands = moduleCacheEntry.Commands;519 foreach (var pair in realExportedCommands)520 {521 var commandName = pair.Key;522 var realCommandType = pair.Value.CommandType;523 CommandTypes commandType;524 if (!exportedCommands.TryGetValue(commandName, out commandType) || commandType != realCommandType)525 {526 needToUpdate = true;527 break;528 }529 }530 531 exportedClasses = moduleCacheEntry.Types;532 foreach (var pair in realExportedClasses)533 {534 var className = pair.Key;535 var realTypeAttributes = pair.Value.TypeAttributes;536 TypeAttributes typeAttributes;537 if (!exportedClasses.TryGetValue(className, out typeAttributes) ||538 typeAttributes != realTypeAttributes)539 {540 needToUpdate = true;541 break;542 }543 }544 545 // Update or not, we've analyzed commands and types now.546 moduleCacheEntry.TypesAnalyzed = true;547 548 if (!needToUpdate)549 {550 ModuleIntrinsics.Tracer.WriteLine("Existing cached info up-to-date. Skipping.");551 return;552 }553 554 exportedCommands.Clear();555 exportedClasses.Clear();556 }557 else558 {559 exportedCommands = new ConcurrentDictionary<string, CommandTypes>(3, realExportedCommands.Count, StringComparer.OrdinalIgnoreCase);560 exportedClasses = new ConcurrentDictionary<string, TypeAttributes>(1, realExportedClasses.Count, StringComparer.OrdinalIgnoreCase);561 moduleCacheEntry = new ModuleCacheEntry562 {563 ModulePath = module.Path,564 LastWriteTime = lastWriteTime,565 Commands = exportedCommands,566 TypesAnalyzed = true,567 Types = exportedClasses568 };569 moduleCacheEntry = s_cacheData.Entries.GetOrAdd(module.Path, moduleCacheEntry);570 }571 572 // We need to update the cache573 foreach (var exportedCommand in realExportedCommands.Values)574 {575 ModuleIntrinsics.Tracer.WriteLine("Caching command: {0}", exportedCommand.Name);576 exportedCommands.GetOrAdd(exportedCommand.Name, exportedCommand.CommandType);577 }578 579 foreach (var pair in realExportedClasses)580 {581 var className = pair.Key;582 ModuleIntrinsics.Tracer.WriteLine("Caching command: {0}", className);583 moduleCacheEntry.Types.AddOrUpdate(className, pair.Value.TypeAttributes, (k, t) => t);584 }585 586 s_cacheData.QueueSerialization();587 }588 589 private static void CallGetModuleDashList(ExecutionContext context, string modulePath)590 {591 CommandInfo commandInfo = new CmdletInfo("Get-Module", typeof(GetModuleCommand), null, null, context);592 Command getModuleCommand = new Command(commandInfo);593 594 try595 {596 PowerShell.Create(RunspaceMode.CurrentRunspace)597 .AddCommand(getModuleCommand)598 .AddParameter("List", true)599 .AddParameter("ErrorAction", ActionPreference.Ignore)600 .AddParameter("WarningAction", ActionPreference.Ignore)601 .AddParameter("InformationAction", ActionPreference.Ignore)602 .AddParameter("Verbose", false)603 .AddParameter("Debug", false)604 .AddParameter("Name", modulePath)605 .Invoke();606 }607 catch (Exception e)608 {609 ModuleIntrinsics.Tracer.WriteLine("Module analysis generated an exception: {0}", e);610 611 // Catch-all OK, third-party call-out.612 }613 }614 615 private static bool GetModuleEntryFromCache(string modulePath, out DateTime lastWriteTime, out ModuleCacheEntry moduleCacheEntry)616 {617 try618 {619 lastWriteTime = new FileInfo(modulePath).LastWriteTime;620 }621 catch (Exception e)622 {623 ModuleIntrinsics.Tracer.WriteLine("Exception checking LastWriteTime on module {0}: {1}", modulePath, e.Message);624 lastWriteTime = DateTime.MinValue;625 }626 627 if (s_cacheData.Entries.TryGetValue(modulePath, out moduleCacheEntry))628 {629 if (lastWriteTime == moduleCacheEntry.LastWriteTime)630 {631 return true;632 }633 634 ModuleIntrinsics.Tracer.WriteLine("{0}: cache entry out of date, cached on {1}, last updated on {2}",635 modulePath, moduleCacheEntry.LastWriteTime, lastWriteTime);636 637 s_cacheData.Entries.TryRemove(modulePath, out moduleCacheEntry);638 }639 640 moduleCacheEntry = null;641 return false;642 }643 }644 645 internal sealed class AnalysisCacheData646 {647 private static byte[] GetHeader()648 {649 return new byte[]650 {651 0x50, 0x53, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x43, 0x41, 0x43, 0x48, 0x45, // PSMODULECACHE652 0x01 // version #653 };654 }655 656 // The last time the index was maintained.657 public DateTime LastReadTime { get; set; }658 659 public ConcurrentDictionary<string, ModuleCacheEntry> Entries { get; set; }660 661 private int _saveCacheToDiskQueued;662 663 private bool _saveCacheToDisk = true;664 665 public void QueueSerialization()666 {667 if (string.IsNullOrEmpty(s_cacheStoreLocation))668 {669 return;670 }671 672 // We expect many modules to rapidly call for serialization.673 // Instead of doing it right away, we'll queue a task that starts writing674 // after it seems like we've stopped adding stuff to write out. This is675 // avoids blocking the pipeline thread waiting for the write to finish.676 // We want to make sure we only queue one task.677 if (_saveCacheToDisk && Interlocked.Increment(ref _saveCacheToDiskQueued) == 1)678 {679 Task.Run(async delegate680 {681 // Wait a while before assuming we've finished the updates,682 // writing the cache out in a timely matter isn't too important683 // now anyway.684 await Task.Delay(10000).ConfigureAwait(false);685 int counter1, counter2;686 do687 {688 // Check the counter a couple times with a delay,689 // if it's stable, then proceed with writing.690 counter1 = _saveCacheToDiskQueued;691 await Task.Delay(3000).ConfigureAwait(false);692 counter2 = _saveCacheToDiskQueued;693 } while (counter1 != counter2);694 Serialize(s_cacheStoreLocation);695 });696 }697 }698 699 // Remove entries that are not needed anymore, e.g. if a module was removed.700 // If anything is removed, save the cache.701 private void Cleanup()702 {703 Diagnostics.Assert(Environment.GetEnvironmentVariable("PSDisableModuleAnalysisCacheCleanup") == null,704 "Caller to check environment variable before calling");705 706 bool removedSomething = false;707 var keys = Entries.Keys;708 foreach (var key in keys)709 {710 if (!File.Exists(key))711 {712 removedSomething |= Entries.TryRemove(key, out ModuleCacheEntry _);713 }714 }715 716 if (removedSomething)717 {718 QueueSerialization();719 }720 }721 722 private static unsafe void Write(int val, byte[] bytes, FileStream stream)723 {724 Diagnostics.Assert(bytes.Length >= 4, "Must pass a large enough byte array");725 fixed (byte* b = bytes) *((int*)b) = val;726 stream.Write(bytes, 0, 4);727 }728 729 private static unsafe void Write(long val, byte[] bytes, FileStream stream)730 {731 Diagnostics.Assert(bytes.Length >= 8, "Must pass a large enough byte array");732 fixed (byte* b = bytes) *((long*)b) = val;733 stream.Write(bytes, 0, 8);734 }735 736 private static void Write(string val, byte[] bytes, FileStream stream)737 {738 Write(val.Length, bytes, stream);739 bytes = Encoding.UTF8.GetBytes(val);740 stream.Write(bytes, 0, bytes.Length);741 }742 743 private void Serialize(string filename)744 {745 AnalysisCacheData fromOtherProcess = null;746 Diagnostics.Assert(_saveCacheToDisk, "Serialize should never be called without going through QueueSerialization which has a check");747 748 try749 {750 if (File.Exists(filename))751 {752 var fileLastWriteTime = new FileInfo(filename).LastWriteTime;753 if (fileLastWriteTime > this.LastReadTime)754 {755 fromOtherProcess = Deserialize(filename);756 }757 }758 else759 {760 // Make sure the folder exists761 var folder = Path.GetDirectoryName(filename);762 if (!Directory.Exists(folder))763 {764 try765 {766 Directory.CreateDirectory(folder);767 }768 catch (UnauthorizedAccessException)769 {770 // service accounts won't be able to create directory771 _saveCacheToDisk = false;772 return;773 }774 }775 }776 }777 catch (Exception e)778 {779 ModuleIntrinsics.Tracer.WriteLine("Exception checking module analysis cache {0}: {1} ", filename, e.Message);780 }781 782 if (fromOtherProcess != null)783 {784 // We should merge with what another process wrote so we don't clobber useful analysis785 foreach (var otherEntryPair in fromOtherProcess.Entries)786 {787 var otherModuleName = otherEntryPair.Key;788 var otherEntry = otherEntryPair.Value;789 ModuleCacheEntry thisEntry;790 if (Entries.TryGetValue(otherModuleName, out thisEntry))791 {792 if (otherEntry.LastWriteTime > thisEntry.LastWriteTime)793 {794 // The other entry is newer, take it over ours795 Entries[otherModuleName] = otherEntry;796 }797 }798 else799 {800 Entries[otherModuleName] = otherEntry;801 }802 }803 }804 805 // "PSMODULECACHE" -> 13 bytes806 // byte ( 1 byte) -> version807 // int ( 4 bytes) -> count of entries808 // entries (?? bytes) -> all entries809 //810 // each entry is811 // DateTime ( 8 bytes) -> last write time for module file812 // int ( 4 bytes) -> path length813 // string (?? bytes) -> utf8 encoded path814 // int ( 4 bytes) -> count of commands815 // commands (?? bytes) -> all commands816 // int ( 4 bytes) -> count of types, -1 means unanalyzed (and 0 items serialized)817 // types (?? bytes) -> all types818 //819 // each command is820 // int ( 4 bytes) -> command name length821 // string (?? bytes) -> utf8 encoded command name822 // int ( 4 bytes) -> CommandTypes enum823 //824 // each type is825 // int ( 4 bytes) -> type name length826 // string (?? bytes) -> utf8 encoded type name827 // int ( 4 bytes) -> type attributes828 try829 {830 var bytes = new byte[8];831 832 using (var stream = File.Create(filename))833 {834 var headerBytes = GetHeader();835 stream.Write(headerBytes, 0, headerBytes.Length);836 837 // Count of entries838 Write(Entries.Count, bytes, stream);839 840 foreach (var pair in Entries.ToArray())841 {842 var path = pair.Key;843 var entry = pair.Value;844 845 // Module last write time846 Write(entry.LastWriteTime.Ticks, bytes, stream);847 848 // Module path849 Write(path, bytes, stream);850 851 // Commands852 var commandPairs = entry.Commands.ToArray();853 Write(commandPairs.Length, bytes, stream);854 855 foreach (var command in commandPairs)856 {857 Write(command.Key, bytes, stream);858 Write((int)command.Value, bytes, stream);859 }860 861 // Types862 var typePairs = entry.Types.ToArray();863 Write(entry.TypesAnalyzed ? typePairs.Length : -1, bytes, stream);864 865 foreach (var type in typePairs)866 {867 Write(type.Key, bytes, stream);868 Write((int)type.Value, bytes, stream);869 }870 }871 }872 // We just wrote the file, note this so we can detect writes from another process873 LastReadTime = new FileInfo(filename).LastWriteTime;874 }875 catch (Exception e)876 {877 ModuleIntrinsics.Tracer.WriteLine("Exception writing module analysis cache {0}: {1} ", filename, e.Message);878 }879 880 // Reset our counter so we can write again if asked.881 Interlocked.Exchange(ref _saveCacheToDiskQueued, 0);882 }883 884 private const string TruncatedErrorMessage = "module cache file appears truncated";885 private const string InvalidSignatureErrorMessage = "module cache signature not valid";886 private const string PossibleCorruptionErrorMessage = "possible corruption in module cache";887 888 private static unsafe long ReadLong(FileStream stream, byte[] bytes)889 {890 Diagnostics.Assert(bytes.Length >= 8, "Must pass a large enough byte array");891 if (stream.Read(bytes, 0, 8) != 8)892 throw new Exception(TruncatedErrorMessage);893 fixed (byte* b = bytes)894 return *(long*)b;895 }896 897 private static unsafe int ReadInt(FileStream stream, byte[] bytes)898 {899 Diagnostics.Assert(bytes.Length >= 4, "Must pass a large enough byte array");900 if (stream.Read(bytes, 0, 4) != 4)901 throw new Exception(TruncatedErrorMessage);902 fixed (byte* b = bytes)903 return *(int*)b;904 }905 906 private static string ReadString(FileStream stream, ref byte[] bytes)907 {908 int length = ReadInt(stream, bytes);909 if (length > 10 * 1024)910 throw new Exception(PossibleCorruptionErrorMessage);911 if (length > bytes.Length)912 bytes = new byte[length];913 if (stream.Read(bytes, 0, length) != length)914 throw new Exception(TruncatedErrorMessage);915 return Encoding.UTF8.GetString(bytes, 0, length);916 }917 918 private static void ReadHeader(FileStream stream, byte[] bytes)919 {920 var headerBytes = GetHeader();921 var length = headerBytes.Length;922 Diagnostics.Assert(bytes.Length >= length, "must pass a large enough byte array");923 if (stream.Read(bytes, 0, length) != length)924 throw new Exception(TruncatedErrorMessage);925 926 for (int i = 0; i < length; i++)927 {928 if (bytes[i] != headerBytes[i])929 {930 throw new Exception(InvalidSignatureErrorMessage);931 }932 }933 // No need to return - we don't use it other than to detect the correct file format934 }935 936 public static AnalysisCacheData Deserialize(string filename)937 {938 using (var stream = File.OpenRead(filename))939 {940 var result = new AnalysisCacheData { LastReadTime = DateTime.Now };941 942 var bytes = new byte[1024];943 944 // Header945 // "PSMODULECACHE" -> 13 bytes946 // byte ( 1 byte) -> version947 ReadHeader(stream, bytes);948 949 // int ( 4 bytes) -> count of entries950 int entries = ReadInt(stream, bytes);951 if (entries > 20 * 1024)952 throw new Exception(PossibleCorruptionErrorMessage);953 954 result.Entries = new ConcurrentDictionary<string, ModuleCacheEntry>(/*concurrency*/3, entries, StringComparer.OrdinalIgnoreCase);955 956 // entries (?? bytes) -> all entries957 while (entries > 0)958 {959 // DateTime ( 8 bytes) -> last write time for module file960 var lastWriteTime = new DateTime(ReadLong(stream, bytes));961 962 // int ( 4 bytes) -> path length963 // string (?? bytes) -> utf8 encoded path964 var path = ReadString(stream, ref bytes);965 966 // int ( 4 bytes) -> count of commands967 var countItems = ReadInt(stream, bytes);968 if (countItems > 20 * 1024)969 throw new Exception(PossibleCorruptionErrorMessage);970 971 var commands = new ConcurrentDictionary<string, CommandTypes>(/*concurrency*/3, countItems, StringComparer.OrdinalIgnoreCase);972 973 // commands (?? bytes) -> all commands974 while (countItems > 0)975 {976 // int ( 4 bytes) -> command name length977 // string (?? bytes) -> utf8 encoded command name978 var commandName = ReadString(stream, ref bytes);979 980 // int ( 4 bytes) -> CommandTypes enum981 var commandTypes = (CommandTypes)ReadInt(stream, bytes);982 983 // Ignore empty entries (possible corruption in the cache or bug?)984 if (!string.IsNullOrWhiteSpace(commandName))985 commands[commandName] = commandTypes;986 987 countItems -= 1;988 }989 990 // int ( 4 bytes) -> count of types991 countItems = ReadInt(stream, bytes);992 993 bool typesAnalyzed = countItems != -1;994 if (!typesAnalyzed)995 countItems = 0;996 if (countItems > 20 * 1024)997 throw new Exception(PossibleCorruptionErrorMessage);998 999 var types = new ConcurrentDictionary<string, TypeAttributes>(1, countItems, StringComparer.OrdinalIgnoreCase);1000 1001 // types (?? bytes) -> all types1002 while (countItems > 0)1003 {1004 // int ( 4 bytes) -> type name length1005 // string (?? bytes) -> utf8 encoded type name1006 var typeName = ReadString(stream, ref bytes);1007 1008 // int ( 4 bytes) -> type attributes1009 var typeAttributes = (TypeAttributes)ReadInt(stream, bytes);1010 1011 // Ignore empty entries (possible corruption in the cache or bug?)1012 if (!string.IsNullOrWhiteSpace(typeName))1013 types[typeName] = typeAttributes;1014 1015 countItems -= 1;1016 }1017 1018 var entry = new ModuleCacheEntry1019 {1020 ModulePath = path,1021 LastWriteTime = lastWriteTime,1022 Commands = commands,1023 TypesAnalyzed = typesAnalyzed,1024 Types = types1025 };1026 result.Entries[path] = entry;1027 1028 entries -= 1;1029 }1030 1031 if (Environment.GetEnvironmentVariable("PSDisableModuleAnalysisCacheCleanup") == null)1032 {1033 Task.Delay(10000).ContinueWith(_ => result.Cleanup());1034 }1035 1036 return result;1037 }1038 }1039 1040 internal static AnalysisCacheData Get()1041 {1042 int retryCount = 3;1043 1044 do1045 {1046 try1047 {1048 if (File.Exists(s_cacheStoreLocation))1049 {1050 return Deserialize(s_cacheStoreLocation);1051 }1052 }1053 catch (Exception e)1054 {1055 ModuleIntrinsics.Tracer.WriteLine("Exception checking module analysis cache: " + e.Message);1056 if ((object)e.Message == (object)TruncatedErrorMessage1057 || (object)e.Message == (object)InvalidSignatureErrorMessage1058 || (object)e.Message == (object)PossibleCorruptionErrorMessage)1059 {1060 // Don't retry if we detected something is wrong with the file1061 // (as opposed to the file being locked or something else)1062 break;1063 }1064 }1065 1066 retryCount -= 1;1067 Thread.Sleep(25); // Sleep a bit to give time for another process to finish writing the cache1068 } while (retryCount > 0);1069 1070 return new AnalysisCacheData1071 {1072 LastReadTime = DateTime.Now,1073 // Capacity set to 100 - a bit bigger than the # of modules on a default Win10 client machine1074 // Concurrency=3 to not create too many locks, contention is unclear, but the old code had a single lock1075 Entries = new ConcurrentDictionary<string, ModuleCacheEntry>(/*concurrency*/3, /*capacity*/100, StringComparer.OrdinalIgnoreCase)1076 };1077 }1078 1079 private AnalysisCacheData()1080 {1081 }1082 1083 private static readonly string s_cacheStoreLocation;1084 1085 static AnalysisCacheData()1086 {1087 // If user defines a custom cache path, then use that.1088 string userDefinedCachePath = Environment.GetEnvironmentVariable("PSModuleAnalysisCachePath");1089 if (!string.IsNullOrEmpty(userDefinedCachePath))1090 {1091 s_cacheStoreLocation = userDefinedCachePath;1092 return;1093 }1094 1095 string cacheFileName = "ModuleAnalysisCache";1096 1097 // When multiple copies of pwsh are on the system, they should use their own copy of the cache.1098 // Append hash of `$PSHOME` to cacheFileName.1099 string hashString = CRC32Hash.ComputeHash(Utils.DefaultPowerShellAppBase);1100 cacheFileName = string.Create(CultureInfo.InvariantCulture, $"{cacheFileName}-{hashString}");1101 1102 if (ExperimentalFeature.EnabledExperimentalFeatureNames.Count > 0)1103 {1104 // If any experimental features are enabled, we cannot use the default cache file because those1105 // features may expose commands that are not available in a regular powershell session, and we1106 // should not cache those commands in the default cache file because that will result in wrong1107 // auto-completion suggestions when the default cache file is used in another powershell session.1108 //1109 // Here we will generate a cache file name that represent the combination of enabled feature names.1110 // We first convert enabled feature names to lower case, then we sort the feature names, and then1111 // compute an CRC32 hash from the sorted feature names. We will use the CRC32 hash to generate the1112 // cache file name.1113 int index = 0;1114 string[] featureNames = new string[ExperimentalFeature.EnabledExperimentalFeatureNames.Count];1115 foreach (string featureName in ExperimentalFeature.EnabledExperimentalFeatureNames)1116 {1117 featureNames[index++] = featureName.ToLowerInvariant();1118 }1119 1120 Array.Sort(featureNames);1121 string allNames = string.Join(Environment.NewLine, featureNames);1122 1123 // Use CRC32 because it's faster.1124 // It's very unlikely to get collision from hashing the combinations of enabled features names.1125 hashString = CRC32Hash.ComputeHash(allNames);1126 cacheFileName = string.Create(CultureInfo.InvariantCulture, $"{cacheFileName}-{hashString}");1127 }1128 1129 Platform.TryDeriveFromCache(cacheFileName, out s_cacheStoreLocation);1130 }1131 }1132 1133 [DebuggerDisplay("ModulePath = {ModulePath}")]1134 internal class ModuleCacheEntry1135 {1136 public DateTime LastWriteTime;1137 public string ModulePath;1138 public bool TypesAnalyzed;1139 public ConcurrentDictionary<string, CommandTypes> Commands;1140 public ConcurrentDictionary<string, TypeAttributes> Types;1141 }1142}1143 