Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate away from MEF for loading DLLs in Windows PowerShell #1154

Open
rjmholt opened this issue Feb 28, 2019 · 1 comment
Open

Migrate away from MEF for loading DLLs in Windows PowerShell #1154

rjmholt opened this issue Feb 28, 2019 · 1 comment

Comments

@rjmholt
Copy link
Contributor

rjmholt commented Feb 28, 2019

PSScriptAnalyzer's .NET Framework builds depend on a lesser known technology called the Managed Extensibility Framework (namespaced to System.ComponentModel.Composition) to do a kind of lazy loading of assemblies for rules, presumably for custom rule sets.

This seems to be a kind of inversion of control framework, although we use it in a less typical way than a web server might:

// An aggregate catalog that combines multiple catalogs.
using (AggregateCatalog catalog = new AggregateCatalog())
{
// Adds all the parts found in the same directory.
string dirName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
catalog.Catalogs.Add(
new SafeDirectoryCatalog(
dirName,
this.outputWriter));
// Adds user specified directory
paths = result.ContainsKey("ValidDllPaths") ? result["ValidDllPaths"] : result["ValidPaths"];
foreach (string path in paths)
{
if (String.Equals(Path.GetExtension(path), ".dll", StringComparison.OrdinalIgnoreCase))
{
catalog.Catalogs.Add(new AssemblyCatalog(path));
}
else
{
catalog.Catalogs.Add(
new SafeDirectoryCatalog(
path,
this.outputWriter));
}
}
// Creates the CompositionContainer with the parts in the catalog.
container = new CompositionContainer(catalog);
// Fills the imports of this object.
try
{
container.ComposeParts(this);
}
catch (CompositionException compositionException)
{
this.outputWriter.WriteWarning(compositionException.ToString());
}
}

It also means that for rules to be discoverable they need to be decorated with attributes like here:

#if !CORECLR
[Export(typeof(IScriptRule))]
#endif

All of that is fine, and it may be that PSScriptAnalyzer users are having success with it, but I'm not really sure if anyone is using it.

There are two problems:

  • We aren't using (and don't seem to need) MEF in PowerShell Core (it apparently has been ported, but it's not clear to what extent).
  • MEF loads assemblies differently to PowerShell

This problem raised itself in #1133, where a rule that depended on an external assembly, which in turn depended on a third assembly failed to load only in .NET Framework (and behaved differently across PowerShell versions).

The error looked like this:

System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.
   at System.Reflection.RuntimeModule.GetTypes(RuntimeModule module)
   at System.Reflection.Assembly.GetTypes()
   at System.ComponentModel.Composition.Hosting.AssemblyCatalog.get_InnerCatalog()
   at System.ComponentModel.Composition.Hosting.AssemblyCatalog.GetEnumerator()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Microsoft.Windows.PowerShell.ScriptAnalyzer.SafeDirectoryCatalog..ctor(String folderLocation, IOutputWriter outputWriter)
System.ComponentModel.Composition.CompositionException: The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.
1) Could not load file or assembly 'Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
Resulting in: An exception occurred while trying to create an instance of type 'Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands'.
Resulting in: Cannot activate part 'Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands'.
Element: Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands -->  Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands -->  AssemblyCatalog (Assembly="Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules, Version=1.17.1.0, Culture=neutral, PublicKeyToken=null")
Resulting in: Cannot get export 'Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands (ContractName="Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.IScriptRule")' from part 'Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands'.
Element: Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands (ContractName="Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.IScriptRule") -->  Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseCompatibleCommands -->  AssemblyCatalog (Assembly="Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules, Version=1.17.1.0, Culture=neutral, PublicKeyToken=null")
Resulting in: Cannot set import 'Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer.ScriptRules (ContractName="Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.IScriptRule")' on part 'Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer'.
Element: Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer.ScriptRules (ContractName="Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.IScriptRule") -->  Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer
   at System.ComponentModel.Composition.CompositionResult.ThrowOnErrors(AtomicComposition atomicComposition)
   at System.ComponentModel.Composition.Hosting.ComposablePartExportProvider.Compose(CompositionBatch batch)
   at Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer.LoadRules(Dictionary`2 result, CommandInvocationIntrinsics invokeCommand, Boolean loadBuiltInRules)

At some point, the compatibility rule would depend on Microsoft.PowerShell.CrossCompatibility.dll, which in turn depended on Newtonsoft.Json.dll.

The first load would succeed, but the second would fail because it would only look for Newtonsoft.Json.dll in the directory of powershell.exe rather than in the same directory as Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.dll.

This was resolved by adding Add-Type $newtonsoftDllPath to ScriptAnalyzer.psm1.

This may not actually be due to MEF, but is worth investigating in any case. The DLL loading differences between Windows PS and PS Core are here:

// An aggregate catalog that combines multiple catalogs.
using (AggregateCatalog catalog = new AggregateCatalog())
{
// Adds all the parts found in the same directory.
string dirName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
catalog.Catalogs.Add(
new SafeDirectoryCatalog(
dirName,
this.outputWriter));
// Adds user specified directory
paths = result.ContainsKey("ValidDllPaths") ? result["ValidDllPaths"] : result["ValidPaths"];
foreach (string path in paths)
{
if (String.Equals(Path.GetExtension(path), ".dll", StringComparison.OrdinalIgnoreCase))
{
catalog.Catalogs.Add(new AssemblyCatalog(path));
}
else
{
catalog.Catalogs.Add(
new SafeDirectoryCatalog(
path,
this.outputWriter));
}
}
// Creates the CompositionContainer with the parts in the catalog.
container = new CompositionContainer(catalog);
// Fills the imports of this object.
try
{
container.ComposeParts(this);
}
catch (CompositionException compositionException)
{
this.outputWriter.WriteWarning(compositionException.ToString());
}
}

@bergmeister
Copy link
Collaborator

Thanks for providing those details. Very helpful, especially since I did not write the base of PSSA and didn't know some of the details you described :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants