12

Got a problem here...

I have got a Powershell CmdLet that works when running in 32-bit mode and fails in 64-bit mode. Question is what the cause is and how it can be fixed.

Situation

Powershell CmdLet that references 'OutlookHelper.Common.dll'. Newest version is 2.0.0.0 The CmdLet also uses logging and references 'Logging.dll'.
Logging.dll also references 'OutlookHelper.Common.dll', only was compiled against version 1.0.0.0.

How I made it work, this works partially

Using an assembly binding redirect in the application configuration file of Powershell:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true"> 
    <supportedRuntime version="v4.0.30319"/> 
    <supportedRuntime version="v2.0.50727"/> 
  </startup> 
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="OutlookHelper.Common" publicKeyToken="5e4553dc0df45306"/>
        <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Powershell 32-bits works just fine

When running on a 64-bits machine, using 'Windows Powershell (x86)' it works. The assembly manager finds an assembly binding redirect:

The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
Running under executable  C:\Windows\syswow64\Windowspowershell\v1.0\powershell.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: User = MYDOMAIN\testuser
LOG: DisplayName = OutlookHelper.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
 (Fully-specified)
LOG: Appbase = file:///C:/Windows/syswow64/Windowspowershell/v1.0/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = powershell.exe
Calling assembly : OutlookHelper.Data.Common, Version=1.0.5295.26925, Culture=neutral, PublicKeyToken=null.
===
LOG: This bind starts in LoadFrom load context.
WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().
LOG: Using application configuration file: C:\Windows\syswow64\Windowspowershell\v1.0\powershell.exe.Config
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.
LOG: Redirect found in application configuration file: 1.0.0.0 redirected to 2.0.0.0.
LOG: Post-policy reference: OutlookHelper.Common, Version=2.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
LOG: GAC Lookup was unsuccessful.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common.DLL.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common/OutlookHelper.Common.DLL.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common.EXE.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common/OutlookHelper.Common.EXE.
LOG: Attempting download of new URL file:///D:/SampleApps/_Common/Bin/Outlook.Extensions.Sample/OutlookHelper.Common.DLL.
LOG: Assembly download was successful. Attempting setup of file: D:\SampleApps\_Common\Bin\Outlook.Extensions.Sample\OutlookHelper.Common.dll
LOG: Entering run-from-source setup phase.
LOG: Assembly Name is: OutlookHelper.Common, Version=2.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
LOG: Where-ref bind Codebase does not match what is found in default context. Keep the result in LoadFrom context.
LOG: Binding succeeds. Returns assembly from D:\SampleApps\_Common\Bin\Outlook.Extensions.Sample\OutlookHelper.Common.dll.
LOG: Assembly is loaded in LoadFrom load context.

Here is what Powershell says about the assembly identity:

Windows PowerShell (x86)
Copyright (C) 2009 Microsoft Corporation. All rights reserved.

PS C:\Users\testuser> ([xml](gc $([System.AppDomain]::CurrentDomain.SetupInformation.ConfigurationFile))).configuratio
n.runtime.assemblyBinding.dependentAssembly.assemblyIdentity

name                                                        publicKeyToken
----                                                        --------------
OutlookHelper.Common                                        5e4553dc0df45306

PS C:\Users\testuser>

This is where the trouble starts...

When running on a 64-bits machine, using 'Windows Powershell' it does not work. The assembly manager does NOT find the assembly binding redirect:

The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
Running under executable  C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: User = MYDOMAIN\testuser
LOG: DisplayName = OutlookHelper.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
 (Fully-specified)
LOG: Appbase = file:///C:/WINDOWS/system32/WindowsPowerShell/v1.0/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = powershell.exe
Calling assembly : OutlookHelper.Data.Common, Version=1.0.5295.26925, Culture=neutral, PublicKeyToken=null.
===
LOG: This bind starts in LoadFrom load context.
WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().
LOG: Using application configuration file: C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe.Config
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Post-policy reference: OutlookHelper.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
LOG: The same bind was seen before, and was failed with hr = 0x80070002.
ERR: Unrecoverable error occurred during pre-download check (hr = 0x80070002).

Here is what Powershell says about the assembly identity:

Windows PowerShell
Copyright (C) 2009 Microsoft Corporation. All rights reserved.

PS C:\Users\testuser> ([xml](gc $([System.AppDomain]::CurrentDomain.SetupInformation.ConfigurationFile))).configuratio
n.runtime.assemblyBinding.dependentAssembly.assemblyIdentity
PS C:\Users\ccontent01>

When I let Powershell get the contents of its own application configuration file I get the following output:

Windows PowerShell
Copyright (C) 2009 Microsoft Corporation. All rights reserved.

PS C:\Users\testuser> gc C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe.Config
<?xml version="1.0"?>
<configuration>
    <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v4.0.30319"/>
        <supportedRuntime version="v2.0.50727"/>
    </startup>
</configuration>
PS C:\Users\testuser>

What I tried...

  • Check whether 'Specific Version' is set to 'false' -> this is the case.
  • Deleted and re-added the application configuration file -> did not fix it.
  • Started with a new application configuration file in the SysWOW64 folder -> did not fix it.
  • Double checked the contents of the files that are loaded (powershell.exe.Config and machine.config) -> they are the same.

My guess

  • Assembly manager cannot find the assembly redirect binding.

Any solutions?

  • Why does the Fusion log for the 64-bits instance not mention something like 'Redirect found in application configuration file: 1.0.0.0 redirected to 2.0.0.0.'?
  • What can be the cause of all this?
  • Can you think of any solutions?
mathijsuitmegen
  • 2,270
  • 1
  • 34
  • 36
  • Where are the dlls on the disk? Are they present in `C:/WINDOWS/system32/WindowsPowerShell/v1.0/` or somewhere else? – Vikas Gupta Nov 03 '14 at 21:31
  • ddls like PSEvents.dll, pspluginwkr.dll, pspluginwkr-v3.dll, pwrshmsg.dll, pwrshsip.dll are present in the system32 AND in the syswow64 folder. The file contents are not the same though. – mathijsuitmegen Nov 04 '14 at 09:01

3 Answers3

11

Not 100% related to 32/64-bit issue, however if someone is interested in working assembly redirect solution please have a look here Powershell config assembly redirect.

You can do custom assembly redirect using PowerShell code like

$FSharpCore = [reflection.assembly]::LoadFrom($PSScriptRoot + "\bin\LIBRARY\FSharp.Core.dll") 

$OnAssemblyResolve = [System.ResolveEventHandler] {
  param($sender, $e)

  # from:FSharp.Core, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  # to:  FSharp.Core, Version=4.4.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  if ($e.Name -eq "FSharp.Core, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") { return $FSharpCore }

  foreach($a in [System.AppDomain]::CurrentDomain.GetAssemblies())
  {
    if ($a.FullName -eq $e.Name)
    {
      return $a
    }
  }
  return $null
}

[System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve)

I am first loading the correct version of FSharp.Core from somewhere as the version in the GAC is old (I guess this might be your case too)

davidpodhola
  • 1,030
  • 10
  • 17
7

Based on @davidpodhola's extremely helpful answer, I started putting something like this in my psm1 module files. If your newer assemblies are already loaded (by Import-Module for instance), this should work:

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

#exclude all powershell related stuff, not sure this strictly necessary
$redirectExcludes = 
    @(
        "System.Management.Automation", 
        "Microsoft.PowerShell.Commands.Utility",
        "Microsoft.PowerShell.Commands.Management",
        "Microsoft.PowerShell.Security",
        "Microsoft.WSMan.Management",    
        "Microsoft.PowerShell.ConsoleHost",
        "Microsoft.Management.Infrastructure",
        "Microsoft.Powershell.PSReadline",
        "Microsoft.PowerShell.GraphicalHost"
        "System.Management.Automation.HostUtilities",

        "System.Management.Automation.resources",
        "Microsoft.PowerShell.Commands.Management.resources",
        "Microsoft.PowerShell.Commands.Utility.resources",
        "Microsoft.PowerShell.Security.resources",
        "Microsoft.WSMan.Management.resources",
        "Microsoft.PowerShell.ConsoleHost.resources",
        "Microsoft.Management.Infrastructure.resources",
        "Microsoft.Powershell.PSReadline.resources",
        "Microsoft.PowerShell.GraphicalHost.resources",
        "System.Management.Automation.HostUtilities.resources"
    )
try
{
    $redirector = [Redirector]::new($redirectExcludes)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    write-warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

Update: Powershell appears to have a bug where simply registering an assembly resolve scriptblock can cause a StackOverflowException when calling some commands like Out-GridView. I updated the code to use a version compiled with Add-Type that seems to resolve the issue.

Tim
  • 412
  • 7
  • 18
  • 1
    That works great, but how do you avoid stack overflows when exiting? If I leave the AssemblyResolve handler in, then powershell crashes on exit. The following prevents it `[System.AppDomain]::CurrentDomain.remove_AssemblyResolve($OnAssemblyResolve)` but this breaks the encapsulation, and means the user of the assembly has to know to unregister the event handler. I've tried doing it in the AppDomain unload and process exit handlers, but without success. – ben Mar 19 '17 at 18:45
  • You know I was occasionally getting that exception and I assumed it was something buried in all the interop code I was writing because it would crash the process. Unfortunately, I think the problem is deeper than just removing the resolver because on my machine I can repro it 100% by simply registering a resolver that does nothing and a call to out-gridview. The docs for AssemblyResolve indicate that if a resolve handler leads to AppDomain.Load or Assembly.Load getting called you can get a StackOverflowException but I am not sure how that can happen with this little bit of code: – Tim Mar 29 '17 at 20:51
  • $OnAssemblyResolve = [System.ResolveEventHandler] { param($sender, $resolveEventArgs) Write-Host -fore Green "trying to resolve something" return null; } [System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve); #this will crash powershell with a StackOverflowException try { 1..10 | out-gridview } catch { #this will not get hit } – Tim Mar 29 '17 at 20:51
  • 1
    @ben I had a thread going with someone internally at MS and they suggested that I use an Add-Type compiled resolver to avoid the stack overflow. I can't say I'm exactly sure why it makes a difference but it does seem to work. I will update my example... – Tim May 03 '17 at 22:42
3

On a 64-bit machine there are two configuration files:

C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe.Config
C:\Windows\syswow64\Windowspowershell\v1.0\powershell.exe.Config

Have you edited both of them on the 64-bit machine?

On 64-bit versions of Windows. 32 bit processes (like notepad++) are transparently redirected from C:\WINDOWS\System32 to C:\WINDOWS\SysWOW64 by the OS.

You will need to make sure you edit both files using a 64-bit text editor like the builtin notepad.exe. This will guarantee you do not suffer from this subtle issue, which can cause confusion.

mathijsuitmegen
  • 2,270
  • 1
  • 34
  • 36
DanL
  • 1,974
  • 14
  • 13
  • Yes I did, tried editing one of the two, then the Operating System changes/synchronizes the changes to the other file. So I tried changing the one in system32 -> didn't work. Tried changing the one in syswow64. Tried to delete both and then test 2 scenario's where I would add the file in the system32 in the first scenario and add secondly add the file in the syswow64 folder -> didn't work. – mathijsuitmegen Nov 04 '14 at 08:56
  • Can you run the following in Powershell and Powershell (x86) and tell me the output in each: ([xml](gc $([System.AppDomain]::CurrentDomain.SetupInformation.ConfigurationFile))).configuration.runtime.assemblyBinding.dependentAssembly.assemblyIdentity – DanL Nov 04 '14 at 09:13
  • Thanks for your comment Dan! I have updated my question with the information you requested. Seems like the 64 bits is missing something. – mathijsuitmegen Nov 04 '14 at 10:58
  • Hi mathijsuitmegen, Can you run gc C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe.Config in a 64 bit powershell console and post the output. Thanks – DanL Nov 04 '14 at 11:07
  • You are on to something Dan! Powershell doesn't read the whole file. When I fire up a cmd.exe and do a 'type' on the same file the whole contents of the file are displayed (including the element 'runtime'). – mathijsuitmegen Nov 04 '14 at 11:40
  • Hi mathijsuitmegen, Can you open C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe.Config in notepad [Not any other text editor and make sure it is C:\Windows\System32\notepad.exe (32-bit programs can't "really access" files inside C:\WINDOWS\System32 even though they appear to be able to do so)]. Does the file appear as you expect? – DanL Nov 04 '14 at 11:48
  • I opened the file in notepad.exe as you mentioned, it is lacking the 'runtime' settings. – mathijsuitmegen Nov 04 '14 at 12:38
  • Have you tried editing the file in notepad to add the runtime settings then retesting. – DanL Nov 04 '14 at 12:39
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/64239/discussion-between-mathijsuitmegen-and-danl). – mathijsuitmegen Nov 04 '14 at 13:05