2020-03-24 11:08:37 -07:00
// Copyright (c) Microsoft Corporation.
2018-02-13 09:23:53 -08:00
// Licensed under the MIT License.
2016-03-30 14:20:52 -07:00
using System.Collections ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.Diagnostics ;
2018-07-02 18:05:37 -07:00
using System.Globalization ;
2016-03-30 14:20:52 -07:00
using System.IO ;
2016-04-28 13:41:25 -07:00
using System.Management.Automation.Internal ;
2016-03-30 14:20:52 -07:00
using System.Management.Automation.Language ;
using System.Management.Automation.Runspaces ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
2019-04-30 10:25:11 +05:00
2016-03-30 14:20:52 -07:00
using Microsoft.PowerShell.Commands ;
namespace System.Management.Automation
{
/// <summary>
/// Class to manage the caching of analysis data.
/// For performance, module command caching is flattened after discovery. Many modules have nested
/// modules that can only be resolved at runtime - for example,
/// script modules that declare: $env:PATH += "; $psScriptRoot". When
/// doing initial analysis, we include these in 'ExportedCommands'.
/// Changes to these type of modules will not be re-analyzed, unless the user re-imports the module,
/// or runs Get-Module -List.
/// </summary>
internal class AnalysisCache
{
2016-07-29 13:02:49 -07:00
private static AnalysisCacheData s_cacheData = AnalysisCacheData . Get ();
2016-03-30 14:20:52 -07:00
// This dictionary shouldn't see much use, so low concurrency and capacity
2016-07-29 13:02:49 -07:00
private static ConcurrentDictionary < string , string > s_modulesBeingAnalyzed =
2016-03-30 14:20:52 -07:00
new ConcurrentDictionary < string , string >( /*concurrency*/ 1 , /*capacity*/ 2 , StringComparer . OrdinalIgnoreCase );
2020-05-31 06:10:22 +01:00
internal static readonly char [] InvalidCommandNameCharacters = new []
2016-03-30 14:20:52 -07:00
{
'#' , ',' , '(' , ')' , '{' , '}' , '[' , ']' , '&' , '/' , '\\' , '$' , '^' , ';' , ':' ,
'"' , '\'' , '<' , '>' , '|' , '?' , '@' , '`' , '*' , '%' , '+' , '=' , '~'
};
internal static ConcurrentDictionary < string , CommandTypes > GetExportedCommands ( string modulePath , bool testOnly , ExecutionContext context )
{
bool etwEnabled = CommandDiscoveryEventSource . Log . IsEnabled ();
if ( etwEnabled ) CommandDiscoveryEventSource . Log . GetModuleExportedCommandsStart ( modulePath );
DateTime lastWriteTime ;
ModuleCacheEntry moduleCacheEntry ;
if ( GetModuleEntryFromCache ( modulePath , out lastWriteTime , out moduleCacheEntry ))
{
if ( etwEnabled ) CommandDiscoveryEventSource . Log . GetModuleExportedCommandsStop ( modulePath );
return moduleCacheEntry . Commands ;
}
ConcurrentDictionary < string , CommandTypes > result = null ;
if (! testOnly )
{
var extension = Path . GetExtension ( modulePath );
if ( extension . Equals ( StringLiterals . PowerShellDataFileExtension , StringComparison . OrdinalIgnoreCase ))
{
result = AnalyzeManifestModule ( modulePath , context , lastWriteTime , etwEnabled );
}
else if ( extension . Equals ( StringLiterals . PowerShellModuleFileExtension , StringComparison . OrdinalIgnoreCase ))
{
result = AnalyzeScriptModule ( modulePath , context , lastWriteTime );
}
else if ( extension . Equals ( StringLiterals . PowerShellCmdletizationFileExtension , StringComparison . OrdinalIgnoreCase ))
{
result = AnalyzeCdxmlModule ( modulePath , context , lastWriteTime );
}
2019-02-07 11:24:06 -08:00
else if ( extension . Equals ( StringLiterals . PowerShellILAssemblyExtension , StringComparison . OrdinalIgnoreCase ))
{
result = AnalyzeDllModule ( modulePath , context , lastWriteTime );
}
else if ( extension . Equals ( StringLiterals . PowerShellILExecutableExtension , StringComparison . OrdinalIgnoreCase ))
2016-03-30 14:20:52 -07:00
{
result = AnalyzeDllModule ( modulePath , context , lastWriteTime );
}
}
if ( result != null )
{
2016-07-29 13:02:49 -07:00
s_cacheData . QueueSerialization ();
2016-03-30 14:20:52 -07:00
ModuleIntrinsics . Tracer . WriteLine ( "Returning {0} exported commands." , result . Count );
}
else
{
ModuleIntrinsics . Tracer . WriteLine ( "Returning NULL for exported commands." );
}
if ( etwEnabled ) CommandDiscoveryEventSource . Log . GetModuleExportedCommandsStop ( modulePath );
return result ;
}
private static ConcurrentDictionary < string , CommandTypes > AnalyzeManifestModule ( string modulePath , ExecutionContext context , DateTime lastWriteTime , bool etwEnabled )
{
ConcurrentDictionary < string , CommandTypes > result = null ;
try
{
var moduleManifestProperties = PsUtils . GetModuleManifestProperties ( modulePath , PsUtils . FastModuleManifestAnalysisPropertyNames );
if ( moduleManifestProperties != null )
{
2020-01-03 10:32:00 -08:00
if (! Configuration . PowerShellConfig . Instance . IsImplicitWinCompatEnabled () && ModuleIsEditionIncompatible ( modulePath , moduleManifestProperties ))
{
ModuleIntrinsics . Tracer . WriteLine ( $"Module lies on the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}" );
return null ;
}
2016-04-28 13:41:25 -07:00
Version version ;
if ( ModuleUtils . IsModuleInVersionSubdirectory ( modulePath , out version ))
{
var versionInManifest = LanguagePrimitives . ConvertTo < Version >( moduleManifestProperties [ "ModuleVersion" ]);
if ( version != versionInManifest )
{
ModuleIntrinsics . Tracer . WriteLine ( "ModuleVersion in manifest does not match versioned module directory, skipping module: {0}" , modulePath );
return null ;
}
}
2016-03-30 14:20:52 -07:00
result = new ConcurrentDictionary < string , CommandTypes >( 3 , moduleManifestProperties . Count , StringComparer . OrdinalIgnoreCase );
var sawWildcard = false ;
var hadCmdlets = AddPsd1EntryToResult ( result , moduleManifestProperties [ "CmdletsToExport" ], CommandTypes . Cmdlet , ref sawWildcard );
var hadFunctions = AddPsd1EntryToResult ( result , moduleManifestProperties [ "FunctionsToExport" ], CommandTypes . Function , ref sawWildcard );
var hadAliases = AddPsd1EntryToResult ( result , moduleManifestProperties [ "AliasesToExport" ], CommandTypes . Alias , ref sawWildcard );
2016-09-01 14:47:07 +00:00
var analysisSucceeded = hadCmdlets && hadFunctions && hadAliases ;
2016-03-30 14:20:52 -07:00
2016-09-01 14:47:07 +00:00
if (! analysisSucceeded && ! sawWildcard && ( hadCmdlets || hadFunctions ))
2016-03-30 14:20:52 -07:00
{
// If we're missing CmdletsToExport, that might still be OK, but only if we have a script module.
// Likewise, if we're missing FunctionsToExport, that might be OK, but only if we have a binary module.
2016-09-01 14:47:07 +00:00
analysisSucceeded = ! CheckModulesTypesInManifestAgainstExportedCommands ( moduleManifestProperties , hadCmdlets , hadFunctions , hadAliases );
2016-03-30 14:20:52 -07:00
}
2016-09-01 14:47:07 +00:00
if ( analysisSucceeded )
2016-03-30 14:20:52 -07:00
{
var moduleCacheEntry = new ModuleCacheEntry
{
ModulePath = modulePath ,
LastWriteTime = lastWriteTime ,
Commands = result ,
TypesAnalyzed = false ,
Types = new ConcurrentDictionary < string , TypeAttributes >( 1 , 8 , StringComparer . OrdinalIgnoreCase )
};
2016-07-29 13:02:49 -07:00
s_cacheData . Entries [ modulePath ] = moduleCacheEntry ;
2016-03-30 14:20:52 -07:00
}
else
{
result = null ;
}
}
}
catch ( Exception e )
{
if ( etwEnabled ) CommandDiscoveryEventSource . Log . ModuleManifestAnalysisException ( modulePath , e . Message );
// Ignore the errors, proceed with the usual module analysis
ModuleIntrinsics . Tracer . WriteLine ( "Exception on fast-path analysis of module {0}" , modulePath );
}
if ( etwEnabled ) CommandDiscoveryEventSource . Log . ModuleManifestAnalysisResult ( modulePath , result != null );
return result ?? AnalyzeTheOldWay ( modulePath , context , lastWriteTime );
}
2018-07-16 16:58:40 -07:00
/// <summary>
/// Check if a module is compatible with the current PSEdition given its path and its manifest properties.
/// </summary>
/// <param name="modulePath">The path to the module.</param>
/// <param name="moduleManifestProperties">The properties of the module's manifest.</param>
/// <returns></returns>
internal static bool ModuleIsEditionIncompatible ( string modulePath , Hashtable moduleManifestProperties )
{
#if UNIX
return false ;
#else
if (! ModuleUtils . IsOnSystem32ModulePath ( modulePath ))
{
return false ;
}
if (! moduleManifestProperties . ContainsKey ( "CompatiblePSEditions" ))
{
return true ;
}
return ! Utils . IsPSEditionSupported ( LanguagePrimitives . ConvertTo < string []>( moduleManifestProperties [ "CompatiblePSEditions" ]));
#endif
}
2016-03-30 14:20:52 -07:00
internal static bool ModuleAnalysisViaGetModuleRequired ( object modulePathObj , bool hadCmdlets , bool hadFunctions , bool hadAliases )
{
var modulePath = modulePathObj as string ;
if ( modulePath == null )
return true ;
if ( modulePath . EndsWith ( StringLiterals . PowerShellModuleFileExtension , StringComparison . OrdinalIgnoreCase ))
{
// A script module can't exactly define cmdlets, but it can import a binary module (as nested), so
// it can indirectly define cmdlets. And obviously a script module can define functions and aliases.
// If we got here, one of those is missing, so analysis is required.
return true ;
}
if ( modulePath . EndsWith ( StringLiterals . PowerShellCmdletizationFileExtension , StringComparison . OrdinalIgnoreCase ))
{
// A cdxml module can only define functions and aliases, so if we have both, no more analysis is required.
return ! hadFunctions || ! hadAliases ;
}
2019-02-07 11:24:06 -08:00
if ( modulePath . EndsWith ( StringLiterals . PowerShellILAssemblyExtension , StringComparison . OrdinalIgnoreCase ))
{
// A dll just exports cmdlets, so if the manifest doesn't explicitly export any cmdlets,
// more analysis is required. If the module exports aliases, we can't discover that analyzing
// the binary, so aliases are always required to be explicit (no wildcards) in the manifest.
return ! hadCmdlets ;
}
if ( modulePath . EndsWith ( StringLiterals . PowerShellILExecutableExtension , StringComparison . OrdinalIgnoreCase ))
2016-03-30 14:20:52 -07:00
{
// A dll just exports cmdlets, so if the manifest doesn't explicitly export any cmdlets,
// more analysis is required. If the module exports aliases, we can't discover that analyzing
// the binary, so aliases are always required to be explicit (no wildcards) in the manifest.
return ! hadCmdlets ;
}
// Any other extension (or no extension), just assume the worst and analyze the module
return true ;
}
// Returns true if we need to analyze the manifest module in Get-Module because
// our quick and dirty module manifest analysis is missing something not easily
// discovered.
//
// TODO - psm1 modules are actually easily handled, so if we only saw a psm1 here,
// we should just analyze it and not fall back on Get-Module -List.
private static bool CheckModulesTypesInManifestAgainstExportedCommands ( Hashtable moduleManifestProperties , bool hadCmdlets , bool hadFunctions , bool hadAliases )
{
var rootModule = moduleManifestProperties [ "RootModule" ];
if ( rootModule != null && ModuleAnalysisViaGetModuleRequired ( rootModule , hadCmdlets , hadFunctions , hadAliases ))
return true ;
var moduleToProcess = moduleManifestProperties [ "ModuleToProcess" ];
if ( moduleToProcess != null && ModuleAnalysisViaGetModuleRequired ( moduleToProcess , hadCmdlets , hadFunctions , hadAliases ))
return true ;
var nestedModules = moduleManifestProperties [ "NestedModules" ];
if ( nestedModules != null )
{
var nestedModule = nestedModules as string ;
if ( nestedModule != null )
{
return ModuleAnalysisViaGetModuleRequired ( nestedModule , hadCmdlets , hadFunctions , hadAliases );
}
var nestedModuleArray = nestedModules as object [];
if ( nestedModuleArray == null )
return true ;
foreach ( var element in nestedModuleArray )
{
if ( ModuleAnalysisViaGetModuleRequired ( element , hadCmdlets , hadFunctions , hadAliases ))
return true ;
}
}
return false ;
}
private static bool AddPsd1EntryToResult ( ConcurrentDictionary < string , CommandTypes > result , string command , CommandTypes commandTypeToAdd , ref bool sawWildcard )
{
if ( WildcardPattern . ContainsWildcardCharacters ( command ))
{
sawWildcard = true ;
return false ;
}
// An empty string is one way of saying "no exported commands".
if ( command . Length != 0 )
{
CommandTypes commandTypes ;
if ( result . TryGetValue ( command , out commandTypes ))
{
commandTypes |= commandTypeToAdd ;
}
else
{
commandTypes = commandTypeToAdd ;
}
2018-12-23 22:20:06 -08:00
2016-03-30 14:20:52 -07:00
result [ command ] = commandTypes ;
}
2018-12-23 22:20:06 -08:00
2016-03-30 14:20:52 -07:00
return true ;
}
private static bool AddPsd1EntryToResult ( ConcurrentDictionary < string , CommandTypes > result , object value , CommandTypes commandTypeToAdd , ref bool sawWildcard )
{
string command = value as string ;
if ( command != null )
{
return AddPsd1EntryToResult ( result , command , commandTypeToAdd , ref sawWildcard );
}
object [] commands = value as object [];
if ( commands != null )
{
foreach ( var o in commands )
{
if (! AddPsd1EntryToResult ( result , o , commandTypeToAdd , ref sawWildcard ))
return false ;
}
// An empty array is still success, that's how a manifest declares that
// no entries are exported (unlike the lack of an entry, or $null).
return true ;
}
// Unknown type, let Get-Module -List deal with this manifest
return false ;
}
private static ConcurrentDictionary < string , CommandTypes > AnalyzeScriptModule ( string modulePath , ExecutionContext context , DateTime lastWriteTime )
{
var scriptAnalysis = ScriptAnalysis . Analyze ( modulePath , context );
if ( scriptAnalysis == null )
{
return null ;
}
List < WildcardPattern > scriptAnalysisPatterns = new List < WildcardPattern >();
foreach ( string discoveredCommandFilter in scriptAnalysis . DiscoveredCommandFilters )
{
scriptAnalysisPatterns . Add ( new WildcardPattern ( discoveredCommandFilter ));
}
var result = new ConcurrentDictionary < string , CommandTypes >( 3 ,
scriptAnalysis . DiscoveredExports . Count + scriptAnalysis . DiscoveredAliases . Count ,
StringComparer . OrdinalIgnoreCase );
// Add any directly discovered exports
2016-07-29 13:02:49 -07:00
foreach ( var command in scriptAnalysis . DiscoveredExports )
2016-03-30 14:20:52 -07:00
{
if ( SessionStateUtilities . MatchesAnyWildcardPattern ( command , scriptAnalysisPatterns , true ))
{
if ( command . IndexOfAny ( InvalidCommandNameCharacters ) < 0 )
{
result [ command ] = CommandTypes . Function ;
}
}
}
// Add the discovered aliases
2016-07-29 13:02:49 -07:00
foreach ( var pair in scriptAnalysis . DiscoveredAliases )
2016-03-30 14:20:52 -07:00
{
var commandName = pair . Key ;
// These are already filtered
if ( commandName . IndexOfAny ( InvalidCommandNameCharacters ) < 0 )
{
result . AddOrUpdate ( commandName , CommandTypes . Alias ,
2016-07-29 13:02:49 -07:00
( _ , existingCommandType ) => existingCommandType | CommandTypes . Alias );
2016-03-30 14:20:52 -07:00
}
}
// Add any files in PsScriptRoot if it added itself to the path
if ( scriptAnalysis . AddsSelfToPath )
{
string baseDirectory = Path . GetDirectoryName ( modulePath );
try
{
foreach ( string item in Directory . GetFiles ( baseDirectory , "*.ps1" ))
{
var command = Path . GetFileNameWithoutExtension ( item );
result . AddOrUpdate ( command , CommandTypes . ExternalScript ,
2016-07-29 13:02:49 -07:00
( _ , existingCommandType ) => existingCommandType | CommandTypes . ExternalScript );
2016-03-30 14:20:52 -07:00
}
}
catch ( UnauthorizedAccessException )
{
// Consume this exception here
}
}
var exportedClasses = new ConcurrentDictionary < string , TypeAttributes >( /*concurrency*/
1 , scriptAnalysis . DiscoveredClasses . Count , StringComparer . OrdinalIgnoreCase );
foreach ( var exportedClass in scriptAnalysis . DiscoveredClasses )
{
exportedClasses [ exportedClass . Name ] = exportedClass . TypeAttributes ;
}
var moduleCacheEntry = new ModuleCacheEntry
{
ModulePath = modulePath ,
LastWriteTime = lastWriteTime ,
Commands = result ,
TypesAnalyzed = true ,
Types = exportedClasses
};
2016-07-29 13:02:49 -07:00
s_cacheData . Entries [ modulePath ] = moduleCacheEntry ;
2016-03-30 14:20:52 -07:00
return result ;
}
private static ConcurrentDictionary < string , CommandTypes > AnalyzeCdxmlModule ( string modulePath , ExecutionContext context , DateTime lastWriteTime )
{
return AnalyzeTheOldWay ( modulePath , context , lastWriteTime );
}
private static ConcurrentDictionary < string , CommandTypes > AnalyzeDllModule ( string modulePath , ExecutionContext context , DateTime lastWriteTime )
{
return AnalyzeTheOldWay ( modulePath , context , lastWriteTime );
}
private static ConcurrentDictionary < string , CommandTypes > AnalyzeTheOldWay ( string modulePath , ExecutionContext context , DateTime lastWriteTime )
{
try
{
// If we're already analyzing this module, let the recursion bottom out.
2016-07-29 13:02:49 -07:00
if (! s_modulesBeingAnalyzed . TryAdd ( modulePath , modulePath ))
2016-03-30 14:20:52 -07:00
{
ModuleIntrinsics . Tracer . WriteLine ( "{0} is already being analyzed. Exiting." , modulePath );
return null ;
}
// Record that we're analyzing this specific module so that we don't get stuck in recursion
ModuleIntrinsics . Tracer . WriteLine ( "Started analysis: {0}" , modulePath );
CallGetModuleDashList ( context , modulePath );
ModuleCacheEntry moduleCacheEntry ;
if ( GetModuleEntryFromCache ( modulePath , out lastWriteTime , out moduleCacheEntry ))
{
return moduleCacheEntry . Commands ;
}
}
catch ( Exception e )
{
ModuleIntrinsics . Tracer . WriteLine ( "Module analysis generated an exception: {0}" , e );
// Catch-all OK, third-party call-out.
}
finally
{
ModuleIntrinsics . Tracer . WriteLine ( "Finished analysis: {0}" , modulePath );
2016-07-29 13:02:49 -07:00
s_modulesBeingAnalyzed . TryRemove ( modulePath , out modulePath );
2016-03-30 14:20:52 -07:00
}
return null ;
}
/// <summary>
/// Return the exported types for a specific module.
/// If the module is already cache, return from cache, else cache the module.
/// Also re-cache the module if the cached item is stale.
/// </summary>
/// <param name="modulePath">Path to the module to get exported types from.</param>
2018-12-28 16:39:06 +05:00
/// <param name="context">Current Context.</param>
2016-03-30 14:20:52 -07:00
/// <returns></returns>
internal static ConcurrentDictionary < string , TypeAttributes > GetExportedClasses ( string modulePath , ExecutionContext context )
{
DateTime lastWriteTime ;
ModuleCacheEntry moduleCacheEntry ;
if ( GetModuleEntryFromCache ( modulePath , out lastWriteTime , out moduleCacheEntry ) && moduleCacheEntry . TypesAnalyzed )
{
return moduleCacheEntry . Types ;
}
try
{
CallGetModuleDashList ( context , modulePath );
if ( GetModuleEntryFromCache ( modulePath , out lastWriteTime , out moduleCacheEntry ))
{
return moduleCacheEntry . Types ;
}
}
catch ( Exception e )
{
ModuleIntrinsics . Tracer . WriteLine ( "Module analysis generated an exception: {0}" , e );
// Catch-all OK, third-party call-out.
}
return null ;
}
2016-07-29 13:02:49 -07:00
2016-03-30 14:20:52 -07:00
internal static void CacheModuleExports ( PSModuleInfo module , ExecutionContext context )
{
ModuleIntrinsics . Tracer . WriteLine ( "Requested caching for {0}" , module . Name );
2020-01-03 10:32:00 -08:00
// Don't cache incompatible modules on the system32 module path even if loaded with
// -SkipEditionCheck, since it will break subsequent sessions
if (! Configuration . PowerShellConfig . Instance . IsImplicitWinCompatEnabled () && ! module . IsConsideredEditionCompatible )
{
ModuleIntrinsics . Tracer . WriteLine ( $"Module '{module.Name}' not edition compatible and not cached." );
return ;
}
2016-03-30 14:20:52 -07:00
DateTime lastWriteTime ;
ModuleCacheEntry moduleCacheEntry ;
GetModuleEntryFromCache ( module . Path , out lastWriteTime , out moduleCacheEntry );
var realExportedCommands = module . ExportedCommands ;
var realExportedClasses = module . GetExportedTypeDefinitions ();
ConcurrentDictionary < string , CommandTypes > exportedCommands ;
ConcurrentDictionary < string , TypeAttributes > exportedClasses ;
// First see if the existing module info is sufficient. GetModuleEntryFromCache does LastWriteTime
// verification, so this will also return nothing if the cache is out of date or corrupt.
if ( moduleCacheEntry != null )
{
bool needToUpdate = false ;
2017-01-16 13:31:14 -08:00
// We need to iterate and check as exportedCommands will have more item as it can have aliases as well.
2016-03-30 14:20:52 -07:00
exportedCommands = moduleCacheEntry . Commands ;
foreach ( var pair in realExportedCommands )
{
var commandName = pair . Key ;
var realCommandType = pair . Value . CommandType ;
CommandTypes commandType ;
if (! exportedCommands . TryGetValue ( commandName , out commandType ) || commandType != realCommandType )
{
needToUpdate = true ;
break ;
}
}
exportedClasses = moduleCacheEntry . Types ;
foreach ( var pair in realExportedClasses )
{
var className = pair . Key ;
var realTypeAttributes = pair . Value . TypeAttributes ;
TypeAttributes typeAttributes ;
if (! exportedClasses . TryGetValue ( className , out typeAttributes ) ||
typeAttributes != realTypeAttributes )
{
needToUpdate = true ;
break ;
}
}
// Update or not, we've analyzed commands and types now.
moduleCacheEntry . TypesAnalyzed = true ;
if (! needToUpdate )
{
ModuleIntrinsics . Tracer . WriteLine ( "Existing cached info up-to-date. Skipping." );
return ;
}
exportedCommands . Clear ();
exportedClasses . Clear ();
}
else
{
exportedCommands = new ConcurrentDictionary < string , CommandTypes >( 3 , realExportedCommands . Count , StringComparer . OrdinalIgnoreCase );
exportedClasses = new ConcurrentDictionary < string , TypeAttributes >( 1 , realExportedClasses . Count , StringComparer . OrdinalIgnoreCase );
moduleCacheEntry = new ModuleCacheEntry
{
ModulePath = module . Path ,
LastWriteTime = lastWriteTime ,
Commands = exportedCommands ,
TypesAnalyzed = true ,
Types = exportedClasses
};
2016-07-29 13:02:49 -07:00
moduleCacheEntry = s_cacheData . Entries . GetOrAdd ( module . Path , moduleCacheEntry );
2016-03-30 14:20:52 -07:00
}
// We need to update the cache
foreach ( var exportedCommand in realExportedCommands . Values )
{
ModuleIntrinsics . Tracer . WriteLine ( "Caching command: {0}" , exportedCommand . Name );
exportedCommands . GetOrAdd ( exportedCommand . Name , exportedCommand . CommandType );
}
foreach ( var pair in realExportedClasses )
{
var className = pair . Key ;
ModuleIntrinsics . Tracer . WriteLine ( "Caching command: {0}" , className );
moduleCacheEntry . Types . AddOrUpdate ( className , pair . Value . TypeAttributes , ( k , t ) => t );
}
2016-07-29 13:02:49 -07:00
s_cacheData . QueueSerialization ();
2016-03-30 14:20:52 -07:00
}
private static void CallGetModuleDashList ( ExecutionContext context , string modulePath )
{
CommandInfo commandInfo = new CmdletInfo ( "Get-Module" , typeof ( GetModuleCommand ), null , null , context );
Command getModuleCommand = new Command ( commandInfo );
try
{
PowerShell . Create ( RunspaceMode . CurrentRunspace )
. AddCommand ( getModuleCommand )
. AddParameter ( "List" , true )
. AddParameter ( "ErrorAction" , ActionPreference . Ignore )
. AddParameter ( "WarningAction" , ActionPreference . Ignore )
. AddParameter ( "InformationAction" , ActionPreference . Ignore )
. AddParameter ( "Verbose" , false )
. AddParameter ( "Debug" , false )
. AddParameter ( "Name" , modulePath )
. Invoke ();
}
catch ( Exception e )
{
ModuleIntrinsics . Tracer . WriteLine ( "Module analysis generated an exception: {0}" , e );
// Catch-all OK, third-party call-out.
}
}
private static bool GetModuleEntryFromCache ( string modulePath , out DateTime lastWriteTime , out ModuleCacheEntry moduleCacheEntry )
{
try
{
lastWriteTime = new FileInfo ( modulePath ). LastWriteTime ;
}
catch ( Exception e )
{
ModuleIntrinsics . Tracer . WriteLine ( "Exception checking LastWriteTime on module {0}: {1}" , modulePath , e . Message );
lastWriteTime = DateTime . MinValue ;
}
2016-07-29 13:02:49 -07:00
if ( s_cacheData . Entries . TryGetValue ( modulePath , out moduleCacheEntry ))
2016-03-30 14:20:52 -07:00
{
if ( lastWriteTime == moduleCacheEntry . LastWriteTime )
{
return true ;
}
ModuleIntrinsics . Tracer . WriteLine ( "{0}: cache entry out of date, cached on {1}, last updated on {2}" ,
modulePath , moduleCacheEntry . LastWriteTime , lastWriteTime );
2016-07-29 13:02:49 -07:00
s_cacheData . Entries . TryRemove ( modulePath , out moduleCacheEntry );
2016-03-30 14:20:52 -07:00
}
moduleCacheEntry = null ;
return false ;
}
}
internal class AnalysisCacheData
{
2016-07-29 13:02:49 -07:00
private static byte [] GetHeader ()
2016-03-30 14:20:52 -07:00
{
return new byte []
{
0x50 , 0x53 , 0x4d , 0x4f , 0x44 , 0x55 , 0x4c , 0x45 , 0x43 , 0x41 , 0x43 , 0x48 , 0x45 , // PSMODULECACHE
0x01 // version #
};
}
// The last time the index was maintained.
public DateTime LastReadTime { get ; set ; }
2016-07-29 13:02:49 -07:00
2016-03-30 14:20:52 -07:00
public ConcurrentDictionary < string , ModuleCacheEntry > Entries { get ; set ; }
2016-07-29 13:02:49 -07:00
private int _saveCacheToDiskQueued ;
2016-03-30 14:20:52 -07:00
2017-03-05 22:05:10 -08:00
private bool _saveCacheToDisk = true ;
2016-03-30 14:20:52 -07:00
public void QueueSerialization ()
{
// We expect many modules to rapidly call for serialization.
// Instead of doing it right away, we'll queue a task that starts writing
// after it seems like we've stopped adding stuff to write out. This is
// avoids blocking the pipeline thread waiting for the write to finish.
// We want to make sure we only queue one task.
2017-03-05 22:05:10 -08:00
if ( _saveCacheToDisk && Interlocked . Increment ( ref _saveCacheToDiskQueued ) == 1 )
2016-03-30 14:20:52 -07:00
{
Task . Run ( async delegate
{
// Wait a while before assuming we've finished the updates,
// writing the cache out in a timely matter isn't too important
// now anyway.
2017-11-10 20:03:34 +03:00
await Task . Delay ( 10000 ). ConfigureAwait ( false );
2016-03-30 14:20:52 -07:00
int counter1 , counter2 ;
do
{
// Check the counter a couple times with a delay,
// if it's stable, then proceed with writing.
counter1 = _saveCacheToDiskQueued ;
2017-11-10 20:03:34 +03:00
await Task . Delay ( 3000 ). ConfigureAwait ( false );
2016-03-30 14:20:52 -07:00
counter2 = _saveCacheToDiskQueued ;
} while ( counter1 != counter2 );
2016-07-29 13:02:49 -07:00
Serialize ( s_cacheStoreLocation );
2016-03-30 14:20:52 -07:00
});
}
}
// Remove entries that are not needed anymore, e.g. if a module was removed.
// If anything is removed, save the cache.
private void Cleanup ()
{
2016-04-28 13:41:25 -07:00
Diagnostics . Assert ( Environment . GetEnvironmentVariable ( "PSDisableModuleAnalysisCacheCleanup" ) == null ,
"Caller to check environment variable before calling" );
2016-03-30 14:20:52 -07:00
bool removedSomething = false ;
var keys = Entries . Keys ;
foreach ( var key in keys )
{
2018-07-25 22:02:09 +05:00
if (! File . Exists ( key ))
2016-03-30 14:20:52 -07:00
{
2018-07-25 22:02:09 +05:00
removedSomething |= Entries . TryRemove ( key , out ModuleCacheEntry _ );
2016-03-30 14:20:52 -07:00
}
}
if ( removedSomething )
{
QueueSerialization ();
}
}
private static unsafe void Write ( int val , byte [] bytes , FileStream stream )
{
Diagnostics . Assert ( bytes . Length >= 4 , "Must pass a large enough byte array" );
2016-07-29 13:02:49 -07:00
fixed ( byte * b = bytes ) *(( int *) b ) = val ;
2016-03-30 14:20:52 -07:00
stream . Write ( bytes , 0 , 4 );
}
2018-12-23 22:20:06 -08:00
2016-03-30 14:20:52 -07:00
private static unsafe void Write ( long val , byte [] bytes , FileStream stream )
{
Diagnostics . Assert ( bytes . Length >= 8 , "Must pass a large enough byte array" );
2016-07-29 13:02:49 -07:00
fixed ( byte * b = bytes ) *(( long *) b ) = val ;
2016-03-30 14:20:52 -07:00
stream . Write ( bytes , 0 , 8 );
}
private static void Write ( string val , byte [] bytes , FileStream stream )
{
Write ( val . Length , bytes , stream );
bytes = Encoding . UTF8 . GetBytes ( val );
stream . Write ( bytes , 0 , bytes . Length );
}
private void Serialize ( string filename )
{
AnalysisCacheData fromOtherProcess = null ;
2020-06-11 07:25:35 +01:00
Diagnostics . Assert ( _saveCacheToDisk , "Serialize should never be called without going through QueueSerialization which has a check" );
2017-03-05 22:05:10 -08:00
2016-03-30 14:20:52 -07:00
try
{
2018-07-25 22:02:09 +05:00
if ( File . Exists ( filename ))
2016-03-30 14:20:52 -07:00
{
var fileLastWriteTime = new FileInfo ( filename ). LastWriteTime ;
if ( fileLastWriteTime > this . LastReadTime )
{
fromOtherProcess = Deserialize ( filename );
}
}
else
{
// Make sure the folder exists
var folder = Path . GetDirectoryName ( filename );
if (! Directory . Exists ( folder ))
{
2017-03-05 22:05:10 -08:00
try
{
Directory . CreateDirectory ( folder );
}
catch ( UnauthorizedAccessException )
{
// service accounts won't be able to create directory
_saveCacheToDisk = false ;
return ;
}
2016-03-30 14:20:52 -07:00
}
}
}
catch ( Exception e )
{
ModuleIntrinsics . Tracer . WriteLine ( "Exception checking module analysis cache {0}: {1} " , filename , e . Message );
}
if ( fromOtherProcess != null )
{
// We should merge with what another process wrote so we don't clobber useful analysis
foreach ( var otherEntryPair in fromOtherProcess . Entries )
{
var otherModuleName = otherEntryPair . Key ;
var otherEntry = otherEntryPair . Value ;
ModuleCacheEntry thisEntry ;
if ( Entries . TryGetValue ( otherModuleName , out thisEntry ))
{
if ( otherEntry . LastWriteTime > thisEntry . LastWriteTime )
{
// The other entry is newer, take it over ours
Entries [ otherModuleName ] = otherEntry ;
}
}
else
{
Entries [ otherModuleName ] = otherEntry ;
}
}
}
// "PSMODULECACHE" -> 13 bytes
// byte ( 1 byte) -> version
// int ( 4 bytes) -> count of entries
// entries (?? bytes) -> all entries
//
// each entry is
// DateTime ( 8 bytes) -> last write time for module file
// int ( 4 bytes) -> path length
// string (?? bytes) -> utf8 encoded path
// int ( 4 bytes) -> count of commands
// commands (?? bytes) -> all commands
// int ( 4 bytes) -> count of types, -1 means unanalyzed (and 0 items serialized)
// types (?? bytes) -> all types
//
// each command is
// int ( 4 bytes) -> command name length
// string (?? bytes) -> utf8 encoded command name
// int ( 4 bytes) -> CommandTypes enum
//
// each type is
// int ( 4 bytes) -> type name length
// string (?? bytes) -> utf8 encoded type name
// int ( 4 bytes) -> type attributes
2016-04-28 13:41:25 -07:00
try
2016-03-30 14:20:52 -07:00
{
2016-04-28 13:41:25 -07:00
var bytes = new byte [ 8 ];
2016-03-30 14:20:52 -07:00
2016-04-28 13:41:25 -07:00
using ( var stream = File . Create ( filename ))
2016-03-30 14:20:52 -07:00
{
2016-04-28 13:41:25 -07:00
var headerBytes = GetHeader ();
stream . Write ( headerBytes , 0 , headerBytes . Length );
2016-03-30 14:20:52 -07:00
2016-04-28 13:41:25 -07:00
// Count of entries
Write ( Entries . Count , bytes , stream );
2016-03-30 14:20:52 -07:00
2016-04-28 13:41:25 -07:00
foreach ( var pair in Entries . ToArray ())
2016-03-30 14:20:52 -07:00
{
2016-04-28 13:41:25 -07:00
var path = pair . Key ;
var entry = pair . Value ;
2016-03-30 14:20:52 -07:00
2016-04-28 13:41:25 -07:00
// Module last write time
Write ( entry . LastWriteTime . Ticks , bytes , stream );
2016-03-30 14:20:52 -07:00
2016-04-28 13:41:25 -07:00
// Module path
Write ( path , bytes , stream );
// Commands
var commandPairs = entry . Commands . ToArray ();
Write ( commandPairs . Length , bytes , stream );
foreach ( var command in commandPairs )
{
Write ( command . Key , bytes , stream );
2016-07-29 13:02:49 -07:00
Write (( int ) command . Value , bytes , stream );
2016-04-28 13:41:25 -07:00
}
// Types
var typePairs = entry . Types . ToArray ();
Write ( entry . TypesAnalyzed ? typePairs . Length : - 1 , bytes , stream );
foreach ( var type in typePairs )
{
Write ( type . Key , bytes , stream );
2016-07-29 13:02:49 -07:00
Write (( int ) type . Value , bytes , stream );
2016-04-28 13:41:25 -07:00
}
2016-03-30 14:20:52 -07:00
}
}
2016-04-28 13:41:25 -07:00
// We just wrote the file, note this so we can detect writes from another process
LastReadTime = new FileInfo ( filename ). LastWriteTime ;
}
catch ( Exception e )
{
ModuleIntrinsics . Tracer . WriteLine ( "Exception writing module analysis cache {0}: {1} " , filename , e . Message );
2016-03-30 14:20:52 -07:00
}
// Reset our counter so we can write again if asked.
Interlocked . Exchange ( ref _saveCacheToDiskQueued , 0 );
}
private const string TruncatedErrorMessage = "module cache file appears truncated" ;
private const string InvalidSignatureErrorMessage = "module cache signature not valid" ;
private const string PossibleCorruptionErrorMessage = "possible corruption in module cache" ;
private static unsafe long ReadLong ( FileStream stream , byte [] bytes )
{
Diagnostics . Assert ( bytes . Length >= 8 , "Must pass a large enough byte array" );
if ( stream . Read ( bytes , 0 , 8 ) != 8 )
throw new Exception ( TruncatedErrorMessage );
2016-07-29 13:02:49 -07:00
fixed ( byte * b = bytes )
return *( long *) b ;
2016-03-30 14:20:52 -07:00
}
private static unsafe int ReadInt ( FileStream stream , byte [] bytes )
{
Diagnostics . Assert ( bytes . Length >= 4 , "Must pass a large enough byte array" );
if ( stream . Read ( bytes , 0 , 4 ) != 4 )
throw new Exception ( TruncatedErrorMessage );
2016-07-29 13:02:49 -07:00
fixed ( byte * b = bytes )
return *( int *) b ;
2016-03-30 14:20:52 -07:00
}
private static string ReadString ( FileStream stream , ref byte [] bytes )
{
int length = ReadInt ( stream , bytes );
2016-07-29 13:02:49 -07:00
if ( length > 10 * 1024 )
2016-03-30 14:20:52 -07:00
throw new Exception ( PossibleCorruptionErrorMessage );
if ( length > bytes . Length )
bytes = new byte [ length ];
if ( stream . Read ( bytes , 0 , length ) != length )
throw new Exception ( TruncatedErrorMessage );
return Encoding . UTF8 . GetString ( bytes , 0 , length );
}
private static void ReadHeader ( FileStream stream , byte [] bytes )
{
var headerBytes = GetHeader ();
var length = headerBytes . Length ;
Diagnostics . Assert ( bytes . Length >= length , "must pass a large enough byte array" );
if ( stream . Read ( bytes , 0 , length ) != length )
throw new Exception ( TruncatedErrorMessage );
for ( int i = 0 ; i < length ; i ++)
{
if ( bytes [ i ] != headerBytes [ i ])
{
throw new Exception ( InvalidSignatureErrorMessage );
}
}
// No need to return - we don't use it other than to detect the correct file format
}
public static AnalysisCacheData Deserialize ( string filename )
{
using ( var stream = File . OpenRead ( filename ))
{
2016-07-29 13:02:49 -07:00
var result = new AnalysisCacheData { LastReadTime = DateTime . Now };
2016-03-30 14:20:52 -07:00
var bytes = new byte [ 1024 ];
// Header
// "PSMODULECACHE" -> 13 bytes
// byte ( 1 byte) -> version
ReadHeader ( stream , bytes );
// int ( 4 bytes) -> count of entries
int entries = ReadInt ( stream , bytes );
2016-07-29 13:02:49 -07:00
if ( entries > 20 * 1024 )
2016-03-30 14:20:52 -07:00
throw new Exception ( PossibleCorruptionErrorMessage );
result . Entries = new ConcurrentDictionary < string , ModuleCacheEntry >( /*concurrency*/ 3 , entries , StringComparer . OrdinalIgnoreCase );
// entries (?? bytes) -> all entries
while ( entries > 0 )
{
// DateTime ( 8 bytes) -> last write time for module file
var lastWriteTime = new DateTime ( ReadLong ( stream , bytes ));
// int ( 4 bytes) -> path length
// string (?? bytes) -> utf8 encoded path
var path = ReadString ( stream , ref bytes );
// int ( 4 bytes) -> count of commands
var countItems = ReadInt ( stream , bytes );
2016-07-29 13:02:49 -07:00
if ( countItems > 20 * 1024 )
2016-03-30 14:20:52 -07:00
throw new Exception ( PossibleCorruptionErrorMessage );
var commands = new ConcurrentDictionary < string , CommandTypes >( /*concurrency*/ 3 , countItems , StringComparer . OrdinalIgnoreCase );
// commands (?? bytes) -> all commands
while ( countItems > 0 )
{
// int ( 4 bytes) -> command name length
// string (?? bytes) -> utf8 encoded command name
var commandName = ReadString ( stream , ref bytes );
// int ( 4 bytes) -> CommandTypes enum
var commandTypes = ( CommandTypes ) ReadInt ( stream , bytes );
// Ignore empty entries (possible corruption in the cache or bug?)
if (! string . IsNullOrWhiteSpace ( commandName ))
commands [ commandName ] = commandTypes ;
countItems -= 1 ;
}
// int ( 4 bytes) -> count of types
countItems = ReadInt ( stream , bytes );
bool typesAnalyzed = countItems != - 1 ;
if (! typesAnalyzed )
countItems = 0 ;
2016-07-29 13:02:49 -07:00
if ( countItems > 20 * 1024 )
2016-03-30 14:20:52 -07:00
throw new Exception ( PossibleCorruptionErrorMessage );
var types = new ConcurrentDictionary < string , TypeAttributes >( 1 , countItems , StringComparer . OrdinalIgnoreCase );
// types (?? bytes) -> all types
while ( countItems > 0 )
{
// int ( 4 bytes) -> type name length
// string (?? bytes) -> utf8 encoded type name
var typeName = ReadString ( stream , ref bytes );
// int ( 4 bytes) -> type attributes
var typeAttributes = ( TypeAttributes ) ReadInt ( stream , bytes );
// Ignore empty entries (possible corruption in the cache or bug?)
if (! string . IsNullOrWhiteSpace ( typeName ))
types [ typeName ] = typeAttributes ;
countItems -= 1 ;
}
var entry = new ModuleCacheEntry
{
ModulePath = path ,
LastWriteTime = lastWriteTime ,
Commands = commands ,
TypesAnalyzed = typesAnalyzed ,
Types = types
};
result . Entries [ path ] = entry ;
entries -= 1 ;
}
2016-04-28 13:41:25 -07:00
if ( Environment . GetEnvironmentVariable ( "PSDisableModuleAnalysisCacheCleanup" ) == null )
{
Task . Delay ( 10000 ). ContinueWith ( _ => result . Cleanup ());
}
2018-12-23 22:20:06 -08:00
2016-03-30 14:20:52 -07:00
return result ;
}
}
internal static AnalysisCacheData Get ()
{
int retryCount = 3 ;
do
{
try
{
2018-07-25 22:02:09 +05:00
if ( File . Exists ( s_cacheStoreLocation ))
2016-03-30 14:20:52 -07:00
{
2016-07-29 13:02:49 -07:00
return Deserialize ( s_cacheStoreLocation );
2016-03-30 14:20:52 -07:00
}
}
catch ( Exception e )
{
ModuleIntrinsics . Tracer . WriteLine ( "Exception checking module analysis cache: " + e . Message );
2016-07-29 13:02:49 -07:00
if (( object ) e . Message == ( object ) TruncatedErrorMessage
2016-03-30 14:20:52 -07:00
|| ( object ) e . Message == ( object ) InvalidSignatureErrorMessage
|| ( object ) e . Message == ( object ) PossibleCorruptionErrorMessage )
{
// Don't retry if we detected something is wrong with the file
// (as opposed to the file being locked or something else)
break ;
}
}
2018-12-23 22:20:06 -08:00
2016-03-30 14:20:52 -07:00
retryCount -= 1 ;
Thread . Sleep ( 25 ); // Sleep a bit to give time for another process to finish writing the cache
} while ( retryCount > 0 );
return new AnalysisCacheData
{
LastReadTime = DateTime . Now ,
// Capacity set to 100 - a bit bigger than the # of modules on a default Win10 client machine
// Concurrency=3 to not create too many locks, contention is unclear, but the old code had a single lock
Entries = new ConcurrentDictionary < string , ModuleCacheEntry >( /*concurrency*/ 3 , /*capacity*/ 100 , StringComparer . OrdinalIgnoreCase )
};
}
private AnalysisCacheData ()
{
}
2016-04-28 13:41:25 -07:00
2016-07-29 13:02:49 -07:00
private static readonly string s_cacheStoreLocation ;
2016-04-28 13:41:25 -07:00
static AnalysisCacheData ()
{
2018-07-02 18:05:37 -07:00
// If user defines a custom cache path, then use that.
string userDefinedCachePath = Environment . GetEnvironmentVariable ( "PSModuleAnalysisCachePath" );
2018-07-10 22:53:39 -07:00
if (! string . IsNullOrEmpty ( userDefinedCachePath ))
2018-07-02 18:05:37 -07:00
{
s_cacheStoreLocation = userDefinedCachePath ;
2018-07-10 22:53:39 -07:00
return ;
2018-07-02 18:05:37 -07:00
}
string cacheFileName = "ModuleAnalysisCache" ;
2018-12-02 12:52:39 -08:00
2018-11-13 11:08:05 -08:00
// When multiple copies of pwsh are on the system, they should use their own copy of the cache.
// Append hash of `$PSHOME` to cacheFileName.
2018-12-02 12:52:39 -08:00
string hashString = CRC32Hash . ComputeHash ( Utils . DefaultPowerShellAppBase );
cacheFileName = string . Format ( CultureInfo . InvariantCulture , "{0}-{1}" , cacheFileName , hashString );
if ( ExperimentalFeature . EnabledExperimentalFeatureNames . Count > 0 )
2018-07-02 18:05:37 -07:00
{
2018-12-02 12:52:39 -08:00
// If any experimental features are enabled, we cannot use the default cache file because those
// features may expose commands that are not available in a regular powershell session, and we
// should not cache those commands in the default cache file because that will result in wrong
// auto-completion suggestions when the default cache file is used in another powershell session.
//
// Here we will generate a cache file name that represent the combination of enabled feature names.
// We first convert enabled feature names to lower case, then we sort the feature names, and then
// compute an CRC32 hash from the sorted feature names. We will use the CRC32 hash to generate the
// cache file name.
int index = 0 ;
string [] featureNames = new string [ ExperimentalFeature . EnabledExperimentalFeatureNames . Count ];
foreach ( string featureName in ExperimentalFeature . EnabledExperimentalFeatureNames )
2018-07-02 18:05:37 -07:00
{
2018-12-02 12:52:39 -08:00
featureNames [ index ++] = featureName . ToLowerInvariant ();
2018-11-13 11:08:05 -08:00
}
2018-12-02 12:52:39 -08:00
Array . Sort ( featureNames );
string allNames = string . Join ( Environment . NewLine , featureNames );
// Use CRC32 because it's faster.
// It's very unlikely to get collision from hashing the combinations of enabled features names.
hashString = CRC32Hash . ComputeHash ( allNames );
cacheFileName = string . Format ( CultureInfo . InvariantCulture , "{0}-{1}" , cacheFileName , hashString );
2018-07-02 18:05:37 -07:00
}
2019-10-07 02:15:01 -07:00
s_cacheStoreLocation = Path . Combine ( Platform . CacheDirectory , cacheFileName );
2016-04-28 13:41:25 -07:00
}
2016-03-30 14:20:52 -07:00
}
[DebuggerDisplay("ModulePath = {ModulePath}")]
internal class ModuleCacheEntry
{
public DateTime LastWriteTime ;
public string ModulePath ;
public bool TypesAnalyzed ;
public ConcurrentDictionary < string , CommandTypes > Commands ;
public ConcurrentDictionary < string , TypeAttributes > Types ;
}
2018-07-02 18:05:37 -07:00
}