Skip to content

Conversation

@daxian-dbw
Copy link
Member

@daxian-dbw daxian-dbw commented Nov 16, 2019

PR Summary

In .NET Core 2.1, [System.AppDomain]::CurrentDomain.GetAssemblies() only returns the assemblies loaded in the default load context.
In .NET Core 3.0, [System.AppDomain]::CurrentDomain.GetAssemblies() is changed to return all assemblies loaded in the AppDomain, including the assemblies loaded into other AssemblyLoadContext (ALC) instances.
This results in our type resolution to behave inconsistently:

  • for assembly-name-fully-qualified type name, powershell cannot resolve a type when the assembly is loaded in a separate ALC.
  • for fully-qualified type name (without assembly name part), when 2 versions of the same assembly get loaded in two ALC, powershell will resolve the type to the first assembly that got loaded. This behavior is close to undefined because sometimes you cannot control the order of loading.

This PR changes to use AssemblyLoadContext.Default.Assemblies (new API added in .NET Core 3) to make sure assemblies loaded in other ALC are invisible to PowerShell type resolution.

PR Context

PR Checklist

@daxian-dbw daxian-dbw requested a review from rjmholt November 16, 2019 00:48
@daxian-dbw daxian-dbw added the CL-Engine Indicates that a PR should be marked as an engine change in the Change Log label Nov 16, 2019
@daxian-dbw daxian-dbw added this to the 7.0.0-rc.1 milestone Nov 26, 2019
@daxian-dbw daxian-dbw merged commit d46dfc2 into PowerShell:master Nov 26, 2019
@daxian-dbw daxian-dbw deleted the type-resolution branch November 26, 2019 20:40
@SteveL-MSFT SteveL-MSFT removed this from the 7.0.0-rc.1 milestone Dec 4, 2019
@daxian-dbw daxian-dbw added this to the 7.0-Consider milestone Dec 14, 2019
TravisEz13 pushed a commit that referenced this pull request Feb 8, 2020
@TravisEz13 TravisEz13 modified the milestones: GA-approved, 7.0.0 Feb 8, 2020
@ghost
Copy link

ghost commented Feb 21, 2020

🎉v7.0.0-rc.3 has been released which incorporates this pull request.:tada:

Handy links:

@stevenebutler
Copy link
Contributor

This PR prevents PowerShell from finding types in assemblies loaded from memory using

[System.Reflection.Assembly]::Load($bytes)

This has worked in PowerShell 5.1, and PowerShell 7 up to rc2 but doesn't work in rc3.

A workaround for PowerShell 7 rc3 is to use

# $ms is a MemoryStream containing the assembly bytes above
[System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($ms)

However the above code is not compatible with PowerShell 5.1 as the type doesn't exist.

@rjmholt
Copy link
Collaborator

rjmholt commented Mar 3, 2020

Are you able to expand on your scenario for using that API? I haven't seen any use cases in .NET Framework for loading an assembly from bytes other than to get around assembly resolution issues, which .NET Core has effectively resolved with the addition of assembly load contexts.

Really this change is us catching up to deeper changes in .NET assembly handling, and given that these changes have occurred in the platform and something fundamentally different is happening (i.e. the loaded assembly really isn't in the current load context), it's probably appropriate to handle the loading differently on different platforms:

if ($PSVersionTable.PSVersion.Major -ge 7)
{
    $ms = ...
    [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($ms)
}
else
{
    [System.Reflection.Assembly]::Load($bytes)
}

@stevenebutler
Copy link
Contributor

stevenebutler commented Mar 3, 2020

The core use case is when you don't want the DLL to be locked on the file system in Windows, preventing rebuilds/reinstalls etc.

I typically load my more complex DLLs using add-type, but I have a native interop DLL that checks file locking status on startup and handles upgrading of assemblies and native DLLs by inspecting the install folder for locked files. I don't want loading this DLL to also cause file locking so I load it from memory - it has no external dependencies so this works fine.

I used something like the above code, but I tested for the presence of the AssemblyLoadContext type, rather than tying it to the PSVersion.

@rjmholt
Copy link
Collaborator

rjmholt commented Mar 3, 2020

The core use case is when you don't want the DLL to be locked on the file system in Windows

Yes, this is something an ALC can solve today, since they can be collectible/unloadable. Meaning the strategy for handling the problem will probably change significantly moving into the future.

We've been discussing how best to support this going forward and there's no simple solution available by default in PowerShell yet, but it's certainly an area of interest.

@stevenebutler
Copy link
Contributor

stevenebutler commented Mar 3, 2020

I don't think being able to unload from a specific PowerShell will solve my issue, which is users may have multiple PowerShell windows open, each of which would have locked the DLLs when they upgrade. I currently resolve it by checking for locked files and creating a new sequence number folder for the new version of DLLs on upgrade, which is what the in-memory DLL helps with.

Most of my users are on PS 5.1 as well, but I'm on both.

@iSazonov
Copy link
Collaborator

iSazonov commented Mar 4, 2020

@stevenebutler Please open new issue with repo steps.

[System.Reflection.Assembly]::Load($bytes)

It is not PowerShell, it is .Net Core method. You could create simple C# application and check with different .Net Core 3 versions.

@rjmholt
Copy link
Collaborator

rjmholt commented Mar 4, 2020

It is not PowerShell, it is .Net Core method.

The preliminary issue is that PowerShell stopped resolving types outside the default ALC with the changes in this PR.

However, Assembly.Load effectively made a breaking change first, since it loads byte assemblies into an anonymous ALC.

Given that ALCs are designed partially to replace AppDomains, I think not resolving from them is the right way to go (and I should note that fully qualified names still work).

@iSazonov
Copy link
Collaborator

iSazonov commented Mar 4, 2020

@rjmholt Thanks! In the case I see another additional problem https://source.dot.net/#System.Private.CoreLib/Assembly.cs,242

@stevenebutler
Copy link
Contributor

stevenebutler commented Mar 5, 2020

I'm not sure I want to create an issue for this as I understand this area is changing and PowerShell has to adapt. It's obviously a change in behaviour that may break existing code, so I wanted to let people know how they could adapt. I am happy with the change I have made and don't have any issues with it now.

In case anyone does feel strongly about this and wants to raise an issue with a repro, the below pester test will repro the issue (fail) on PowerShell 7.0.0 rc3 (and release), but will pass on versions prior.

For completeness, I have included a second test that shows how to load types from memory in PowerShell, which passes on PowerShell 7.0.0rc3 and above..

import-module Pester

Describe "PowerShell C# type visibility" {
    It "[Assembly]::Load(`$bytes) types are visible to PowerShell" {
    
        $testdll = "$env:TEMP/visibility.dll"
        if (test-path $testdll) {
            remove-item $testdll -force
        }
        Add-Type @"
public class Visibility {
    public static string Test = "Found";
}
"@ -Language CSharp -OutputAssembly $testdll
        $bytes = [System.IO.File]::ReadAllBytes($testdll)
        [System.Reflection.Assembly]::Load($bytes)

        [Visibility]::Test | should be "Found"

    }
    It "System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream(`$memoryStream) types are visible to PowerShell" {
    
        $testdll = "$env:TEMP/visibility2.dll"
        if (test-path $testdll) {
            remove-item $testdll -force
        }
        Add-Type @"
public class Visibility2 {
    public static string Test = "Found";
}
"@ -Language CSharp -OutputAssembly $testdll
        $bytes = [System.IO.File]::ReadAllBytes($testdll)

        $ms = new-object System.IO.MemoryStream
        try {
            $ms.Write($bytes, 0, $bytes.Length)
            $ms.Seek(0, [System.IO.SeekOrigin]::Begin)


            # This will fail on Windows PowerShell 5.1
            [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($ms)
        }
        finally {
            $ms.Dispose()
        }
        [Visibility2]::Test | should be "Found"

    }
}

Output on 7.0.0 rc3:

Describing PowerShell C# type visibilty
 [-] [Assembly]::Load($bytes) types are visible to PowerShell 119ms
   RuntimeException: Unable to find type [Visibility].
   at <ScriptBlock>, C:\dev\repro-AssemblyLoad.ps1: line 18
 [+] System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($memoryStream) types are visible to PowerShell 147ms 

@daxian-dbw
Copy link
Member Author

@stevenebutler In cast you don't know yet, #12052 is tracking the issue you reported, and the PR #12203 was submitted to fix it.

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

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants