如何让 PowerShell 添加类型使用添加类型

发布于 2024-08-29 00:33:03 字数 1227 浏览 5 评论 0原文

我正在开发一个 PoSh 项目,该项目生成 CSharp 代码,然后将其添加到内存中。

新类型使用磁盘 DLL 中的现有类型,该 DLL 通过 Add-Type 加载。

一切都很好,直到我真正尝试调用新类型的方法。这是我正在做的一个示例:

$PWD = "."
rm -Force $PWD\TestClassOne*
$code = "
namespace TEST{
public class TestClassOne
{
    public int DoNothing()
    {
        return 1;
    }
}
}"
$code | Out-File tcone.cs
Add-Type -OutputAssembly $PWD\TestClassOne.dll -OutputType Library -Path $PWD\tcone.cs
Add-Type -Path $PWD\TestClassOne.dll
$a = New-Object TEST.TestClassOne
"Using TestClassOne"
$a.DoNothing()


"Compiling TestClassTwo"
Add-Type -Language CSharpVersion3 -TypeDefinition "
namespace TEST{
public class TestClassTwo
{
    public int CallTestClassOne()
    {
        var a = new TEST.TestClassOne();
        return a.DoNothing();
    }
}
}" -ReferencedAssemblies $PWD\TestClassOne.dll
"OK"
$b = New-Object TEST.TestClassTwo
"Using TestClassTwo"
$b.CallTestClassOne()

运行上面的脚本在最后一行给出以下错误:

使用“0”参数调用“CallTestClassOne”时出现异常: “无法加载文件或程序集“TestClassOne,...” 或其依赖项之一。系统找不到指定的文件。” 在 AddTypeTest.ps1:39 字符:20 + $b.CallTestClassOne <<<<< () + 类别信息: 未指定: (:) [], MethodInitationException + FullQualifiedErrorId :DotNetMethodException

我做错了什么?

I'm working on a PoSh project that generates CSharp code, and then Add-Types it into memory.

The new types use existing types in an on disk DLL, which is loaded via Add-Type.

All is well and good untill I actualy try to invoke methods on the new types. Here's an example of what I'm doing:

$PWD = "."
rm -Force $PWD\TestClassOne*
$code = "
namespace TEST{
public class TestClassOne
{
    public int DoNothing()
    {
        return 1;
    }
}
}"
$code | Out-File tcone.cs
Add-Type -OutputAssembly $PWD\TestClassOne.dll -OutputType Library -Path $PWD\tcone.cs
Add-Type -Path $PWD\TestClassOne.dll
$a = New-Object TEST.TestClassOne
"Using TestClassOne"
$a.DoNothing()


"Compiling TestClassTwo"
Add-Type -Language CSharpVersion3 -TypeDefinition "
namespace TEST{
public class TestClassTwo
{
    public int CallTestClassOne()
    {
        var a = new TEST.TestClassOne();
        return a.DoNothing();
    }
}
}" -ReferencedAssemblies $PWD\TestClassOne.dll
"OK"
$b = New-Object TEST.TestClassTwo
"Using TestClassTwo"
$b.CallTestClassOne()

Running the above script gives the following error on the last line:

Exception calling "CallTestClassOne" with "0" argument(s):
"Could not load file or assembly 'TestClassOne,...'
or one of its dependencies. The system cannot find the file specified."
At AddTypeTest.ps1:39 char:20
+ $b.CallTestClassOne <<<< ()
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

What am I doing wrong?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

堇色安年 2024-09-05 00:33:03

发生这种情况是因为 CLR 加载程序会在应用程序(PowerShell 的)基目录中查找任何程序集。当然,它在那里找不到你的程序集。解决此问题的最佳方法是像 stej 提到的那样挂钩 AssemblyResolve 事件,但使用它来告诉 CLR 程序集在哪里。您无法使用 PowerShell 2.0 的 Register-ObjectEvent 来执行此操作,因为它不适用于需要返回值(即程序集)的事件。在这种情况下,让我们通过 Add-Type 使用更多 C# 来为我们完成这项工作。这段代码的工作原理是:

ri .\TestClassOne.dll -for -ea 0

$resolver = @'
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Utils
{
    public static class AssemblyResolver
    {
        private static Dictionary<string, string> _assemblies;

        static AssemblyResolver()
        {
            var comparer = StringComparer.CurrentCultureIgnoreCase;
            _assemblies = new Dictionary<string,string>(comparer);
            AppDomain.CurrentDomain.AssemblyResolve += ResolveHandler;
        }

        public static void AddAssemblyLocation(string path)
        {
            // This should be made threadsafe for production use
            string name = Path.GetFileNameWithoutExtension(path);
            _assemblies.Add(name, path);
        }

        private static Assembly ResolveHandler(object sender, 
                                               ResolveEventArgs args) 
        {
            var assemblyName = new AssemblyName(args.Name);
            if (_assemblies.ContainsKey(assemblyName.Name))
            {
                return Assembly.LoadFrom(_assemblies[assemblyName.Name]);
            }
            return null;
        }
    }
}
'@

Add-Type -TypeDefinition $resolver -Language CSharpVersion3

$code = @'
namespace TEST {
    public class TestClassOne {
        public int DoNothing() {
            return 1;
        }
    }
}
'@
$code | Out-File tcone.cs
Add-Type -OutputAssembly TestClassOne.dll -OutputType Library -Path tcone.cs

# This is the key, register this assembly's location with our resolver utility
[Utils.AssemblyResolver]::AddAssemblyLocation("$pwd\TestClassOne.dll")

Add-Type -Language CSharpVersion3 `
         -ReferencedAssemblies "$pwd\TestClassOne.dll" `
         -TypeDefinition @'
namespace TEST {
    public class TestClassTwo {
        public int CallTestClassOne() {
            var a = new TEST.TestClassOne();
            return a.DoNothing();
        }
    }
}
'@ 

$b = new-object Test.TestClassTwo
$b.CallTestClassOne()

This happens because any assemblies are looked for by the CLR loader in the application's (PowerShell's) base directory. Of course, it doesn't find your assembly there. The best way to solve this is to hook the AssemblyResolve event as stej mentions but use it to tell the CLR where the assembly is. You can't do this with PowerShell 2.0's Register-ObjectEvent because it doesn't work with events that require a return value (ie the assembly). In this case, let's use more C# via Add-Type to do this work for us. This snippet of code works:

ri .\TestClassOne.dll -for -ea 0

$resolver = @'
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Utils
{
    public static class AssemblyResolver
    {
        private static Dictionary<string, string> _assemblies;

        static AssemblyResolver()
        {
            var comparer = StringComparer.CurrentCultureIgnoreCase;
            _assemblies = new Dictionary<string,string>(comparer);
            AppDomain.CurrentDomain.AssemblyResolve += ResolveHandler;
        }

        public static void AddAssemblyLocation(string path)
        {
            // This should be made threadsafe for production use
            string name = Path.GetFileNameWithoutExtension(path);
            _assemblies.Add(name, path);
        }

        private static Assembly ResolveHandler(object sender, 
                                               ResolveEventArgs args) 
        {
            var assemblyName = new AssemblyName(args.Name);
            if (_assemblies.ContainsKey(assemblyName.Name))
            {
                return Assembly.LoadFrom(_assemblies[assemblyName.Name]);
            }
            return null;
        }
    }
}
'@

Add-Type -TypeDefinition $resolver -Language CSharpVersion3

$code = @'
namespace TEST {
    public class TestClassOne {
        public int DoNothing() {
            return 1;
        }
    }
}
'@
$code | Out-File tcone.cs
Add-Type -OutputAssembly TestClassOne.dll -OutputType Library -Path tcone.cs

# This is the key, register this assembly's location with our resolver utility
[Utils.AssemblyResolver]::AddAssemblyLocation("$pwd\TestClassOne.dll")

Add-Type -Language CSharpVersion3 `
         -ReferencedAssemblies "$pwd\TestClassOne.dll" `
         -TypeDefinition @'
namespace TEST {
    public class TestClassTwo {
        public int CallTestClassOne() {
            var a = new TEST.TestClassOne();
            return a.DoNothing();
        }
    }
}
'@ 

$b = new-object Test.TestClassTwo
$b.CallTestClassOne()
自我难过 2024-09-05 00:33:03

当您将 TestClassTwo 输出到 dll(与 TestClassOne 位于同一目录中)并 Add-Type 时,它就可以工作。或者至少在我的机器上;)所以这就是丑陋的解决方法。

当调用 $b.CallTestClassOne() 时,PowerShell 尝试(出于某种我不知道的原因)在这些位置查找程序集 TestClassOne.dll:

LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.EXE
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.EXE

这是 fuslogvw 工具的输出。它可能对你有用。使用 ProcessMonitor 可以实时看到相同的路径列表。

您也可以尝试这个(在调用 CallTestClassOne() 之前),

[appdomain]::CurrentDomain.add_assemblyResolve({
    $global:x = $args
})
$b.CallTestClassOne()
$x | fl

这将向您显示哪些程序集失败了以及更多信息。

我同意它应该按您的预期工作。这就是为什么这看起来有些错误。

When you output the TestClassTwo to a dll (in the same directory as TestClassOne) and Add-Type it, it works. Or at least at my machine ;) So that's the ugly workaround.

When calling $b.CallTestClassOne() PowerShell tries (from some reason I don't know) to find assembly TestClassOne.dll at these locations:

LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.DLL
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne.EXE
LOG: Pokus o stažení nové adresy URL file:///C:/Windows/SysWOW64/WindowsPowerShell/v1.0/TestClassOne/TestClassOne.EXE

This is output from fuslogvw tool. It might be useful for you. The same list of paths can bee seen live using ProcessMonitor.

You might also try this (before calling CallTestClassOne()

[appdomain]::CurrentDomain.add_assemblyResolve({
    $global:x = $args
})
$b.CallTestClassOne()
$x | fl

This will show you what assembly failed and some more info.

I agree that it should work as you expect. So that's why this looks somewhat buggy.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文