PowerShell / PowerShell/PowerShell
Module Assembly Load Context has no ExportedCommands on 2nd import
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- C#
- Star
- 55.5k
- Fork
- 8.5k
- Merge trung bình
- 1 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 88
Mô tả
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
I tried to create a simpler reproducer without an ALC but I wasn't able to. I'm unsure if there's some combination of the ALC or how I'm loading it but here is a PowerShell script to create a module that is affected by this problem.
$moduleDir = 'temp:/MyModule'
if (Test-Path -Path $moduleDir) {
Remove-Item -Path $moduleDir -Recurse -Force
}
New-Item -Path $moduleDir -ItemType Directory | Out-Null
Add-Type -TypeDefinition @'
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
#nullable enable
namespace MyModule;
public class LoadContext : AssemblyLoadContext
{
private static LoadContext? _instance;
private Assembly _thisAssembly;
private AssemblyName _thisAssemblyName;
private Assembly _moduleAssembly;
private string _assemblyDir;
private LoadContext(string mainModulePathAssemblyPath)
: base(name: "MyModule", isCollectible: false)
{
_assemblyDir = Path.GetDirectoryName(mainModulePathAssemblyPath) ?? "";
_thisAssembly = typeof(LoadContext).Assembly;
_thisAssemblyName = _thisAssembly.GetName();
_moduleAssembly = LoadFromAssemblyPath(mainModulePathAssemblyPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
if (AssemblyName.ReferenceMatchesDefinition(_thisAssemblyName, assemblyName))
{
return _thisAssembly;
}
string asmPath = Path.Join(_assemblyDir, $"{assemblyName.Name}.dll");
if (File.Exists(asmPath))
{
return LoadFromAssemblyPath(asmPath);
}
else
{
return null;
}
}
public static Assembly Initialize()
{
LoadContext? instance = _instance;
if (instance is not null)
{
return instance._moduleAssembly;
}
string assemblyPath = typeof(LoadContext).Assembly.Location;
string modulePath = Path.Combine(
Path.GetDirectoryName(assemblyPath)!,
"MyModule.Module.dll"
);
_instance = new LoadContext(modulePath);
return _instance._moduleAssembly;
}
}
'@ -OutputAssembly "$moduleDir/MyModule.dll"
Add-Type -TypeDefinition @'
using System;
using System.Management.Automation;
namespace MyModule.Module;
[Cmdlet(VerbsDiagnostic.Test, "Cmdlet")]
public class TestCmdlet : PSCmdlet
{
protected override void ProcessRecord()
{
WriteObject("foo");
}
}
'@ -OutputAssembly "$moduleDir/MyModule.Module.dll"
Set-Content -Path "$moduleDir/MyModule.psm1" -Value @'
Add-Type -Path $PSScriptRoot/MyModule.dll
$mainModule = [MyModule.LoadContext]::Initialize()
Import-Module -Assembly $mainModule
'@
Set-Content -Path "$moduleDir/MyModule.psd1" -Value @'
@{
RootModule = 'MyModule.psm1'
ModuleVersion = '0.1.0'
GUID = 'd872c839-4306-49d8-9c89-2916500c0538'
Author = 'Author'
CompanyName = 'Company'
Copyright = '(c) 2023 Author. All rights reserved.'
Description = 'Repro for nested assembly problem'
PowerShellVersion = '7.2'
CmdletsToExport = @('Test-Cmdlet')
PrivateData = @{
PSData = @{}
}
}
'@
Import-Module -Name $moduleDir -Force -PassThru
Test-Cmdlet
Import-Module -Name $moduleDir -Force -PassThru
Test-Cmdlet
The module will add the MyModule.dll which contains the ALC loading code returning the ALC'd assembly. That assembly is then imported inside the module scope which is subsequently a nested module of MyModule. The first call works just fine but if Import-Module ... -Force is called a 2nd time on the module there is no longer any exported commands.
I tried to figure out what the cause behind this was and while I think I know roughly what the problem is I don't know the fix. On the first load of the module the ImportModule_ViaAssembly method will eventually call LoadBinaryModule and AddModuleToModuleTables. The LoadBinaryModule method will set the cmdlets inside the assembly to the context session scope and AddModuleToModuleTables will add the ALC assembly as a nested module to the main module.
On the 2nd import all this is skipped as the ALC assembly is stored in the Context.Modules.ModuleTable so it is no longer loaded, the cmdlets are not added to the session state, and it's not added as a nested module. This means the parent module will no longer find these cmdlets in the session state and will not see it as a cmdlet to export.
A workaround is to do the following in the psm1
$isReload = $true
if (-not ('MyModule.LoadContext' -as [type])) {
$isReload = $false
$moduleName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
Add-Type -Path $PSScriptRoot/MyModule.dll
}
$mainModule = [MyModule.LoadContext]::Initialize()
$alcModule = Import-Module -Assembly $mainModule -PassThru
if ($isReload) {
$addExportedCmdlet = [System.Management.Automation.PSModuleInfo].GetMethod(
'AddExportedCmdlet',
[System.Reflection.BindingFlags]'Instance, NonPublic'
)
foreach ($cmd in $alcModule.ExportedCommands.Values) {
$addExportedCmdlet.Invoke($ExecutionContext.SessionState.Module, @(, $cmd))
}
}
This unfortunately relies on an internal method to re-add the cmdlets inside the ALC assembly to the current module instance manually. Would be nice if pwsh handled this normally.
Expected behavior
PS /home/jborean> Import-Module -Name $moduleDir -Force -PassThru
ModuleType Version PreRelease Name ExportedCommands
---------- ------- ---------- ---- ----------------
Script 0.1.0 MyModule Test-Cmdlet
PS /home/jborean> Test-Cmdlet
foo
PS /home/jborean> Import-Module -Name $moduleDir -Force -PassThru
ModuleType Version PreRelease Name ExportedCommands
---------- ------- ---------- ---- ----------------
Script 0.1.0 MyModule Test-Cmdlet
PS /home/jborean> Test-Cmdlet
foo
Actual behavior
PS /home/jborean> Import-Module -Name $moduleDir -Force -PassThru
ModuleType Version PreRelease Name ExportedCommands
---------- ------- ---------- ---- ----------------
Script 0.1.0 MyModule Test-Cmdlet
PS /home/jborean> Test-Cmdlet
foo
PS /home/jborean> Import-Module -Name $moduleDir -Force -PassThru
ModuleType Version PreRelease Name ExportedCommands
---------- ------- ---------- ---- ----------------
Script 0.1.0 MyModule
PS /home/jborean> Test-Cmdlet
Test-Cmdlet: The term 'Test-Cmdlet' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
Error details
Exception :
Type : System.Management.Automation.CommandNotFoundException
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : The term 'Test-Cmdlet' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
HResult : -2146233087
TargetObject : Test-Cmdlet
CategoryInfo : ObjectNotFound: (Test-Cmdlet:String) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : CommandNotFoundException
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 1
HistoryId : 15
Line : Test-Cmdlet
PositionMessage : At line:1 char:1
+ Test-Cmdlet
+ ~~~~~~~~~~~
InvocationName : Test-Cmdlet
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, <No file>: line 1
CommandName : Test-Cmdlet
TargetSite :
Name : LookupCommandInfo
DeclaringType : System.Management.Automation.CommandDiscovery, System.Management.Automation, Version=7.3.9.500, Culture=neutral, PublicKeyToken=31bf3856ad364e35
MemberType : Method
Module : System.Management.Automation.dll
Message : The term 'Test-Cmdlet' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
Data : System.Collections.ListDictionaryInternal
Source : System.Management.Automation
HResult : -2146233087
StackTrace :
at System.Management.Automation.CommandDiscovery.LookupCommandInfo(String commandName, CommandTypes commandTypes, SearchResolutionOptions searchResolutionOptions, CommandOrigin
commandOrigin, ExecutionContext context)
at System.Management.Automation.ExecutionContext.CreateCommand(String command, Boolean dotSource)
at System.Management.Automation.PipelineOps.AddCommand(PipelineProcessor pipe, CommandParameterInternal[] commandElements, CommandBaseAst commandBaseAst, CommandRedirection[]
redirections, ExecutionContext context)
at System.Management.Automation.PipelineOps.InvokePipeline(Object input, Boolean ignoreInput, CommandParameterInternal[][] pipeElements, CommandBaseAst[] pipeElementAsts,
CommandRedirection[][] commandRedirections, FunctionContext funcContext)
at System.Management.Automation.Interpreter.ActionCallInstruction`6.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
TargetObject : Test-Cmdlet
CategoryInfo : ObjectNotFound: (Test-Cmdlet:String) [], CommandNotFoundException
FullyQualifiedErrorId : CommandNotFoundException
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 1
HistoryId : 15
Line : Test-Cmdlet
PositionMessage : At line:1 char:1
+ Test-Cmdlet
+ ~~~~~~~~~~~
InvocationName : Test-Cmdlet
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, <No file>: line 1
Environment data
Name Value
---- -----
PSVersion 7.3.9
PSEdition Core
GitCommitId 7.3.9
OS Linux 6.6.1-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 08 Nov 2023 16:05:38 +0000
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Also tested on 7.4.0.
Visuals
No response
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Trước tiên, hãy chạy reproducer PowerShell được cung cấp, sau đó đọc ImportModuleCommand.cs, đặc biệt là ImportModule_ViaAssembly, LoadBinaryModule và AddModuleToModuleTables. Theo dõi lần import đầu tiên và lần import thứ hai bị buộc thực hiện qua hành vi của bảng module; được xem là hoàn tất khi lần import thứ hai vẫn expose Test-Cmdlet và lệnh chạy thành công.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- csharp, powershell
- Lĩnh vực
- backend, cli
- Loại issue
- Lỗi
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức độ hoạt động
- Đình trệ
- Độ rõ ràng
- Khá rõ ràng
- Mức phù hợp với người mới
- 35/100