PowerShell:使用PS 5类时无法找到类型

17
我正在使用WinSCP PowerShell程序集中的类。在其中一个方法中,我正在使用来自WinSCP的各种类型。只要我已经添加了程序集,这就可以正常工作-然而,由于PowerShell在使用类时读取脚本的方式(我猜是这样?),会在加载程序集之前抛出错误。实际上,即使我在顶部放置Write-Host,它也不会加载。有没有办法在解析文件的其余部分之前强制运行某些内容?
Transfer() {
    $this.Logger = [Logger]::new()
    try {

        Add-Type -Path $this.Paths.WinSCP            
        $ConnectionType = $this.FtpSettings.Protocol.ToString()
        $SessionOptions = New-Object WinSCP.SessionOptions -Property @{
            Protocol = [WinSCP.Protocol]::$ConnectionType
            HostName = $this.FtpSettings.Server
            UserName = $this.FtpSettings.Username
            Password = $this.FtpSettings.Password
        }

导致以下类似错误:

Protocol = [WinSCP.Protocol]::$ConnectionType
Unable to find type [WinSCP.Protocol].

但是我加载程序集的位置并不重要。即使我将Add-Type命令放在最顶部,并使用指向WinSCPnet.dll的直接路径,它也不会加载 - 在运行任何内容之前,它似乎会检测到缺少的类型。

5个回答

25
正如你所发现的,PowerShell拒绝运行包含class定义并引用尚未加载的类型的脚本 - 脚本解析阶段会失败。
截至 PowerShell (Core) 7.3.4(根据本文撰写时的最新版本),即使在脚本顶部使用using assembly语句也无法解决这个问题,因为在你的情况下,该类型是在PS类定义的上下文中引用的 - 这个问题 可能最终会得到修复;修复已经获得批准,但还没有人着手实施;相关工作以及其他与类相关的问题正在GitHub issue #6652中跟踪。
适当的解决方案是创建一个脚本模块(*.psm1),其关联清单(*.psd1)声明包含所引用类型的程序集为先决条件,通过RequiredAssemblies键。
如果不能使用模块,则请参见底部的替代解决方案。
以下是一个简化的步骤指南:
按照以下方式创建测试模块tm:
创建模块文件夹./tm并在其中创建清单文件(*.psd1):
# 创建模块文件夹(如果失败,则删除现有的./tm文件夹)。
$null = New-Item -Type Directory -ErrorAction Stop ./tm

# 创建声明WinSCP程序集为先决条件的清单文件。
# 根据需要修改程序集的路径;您可以指定相对路径,但请注意路径不能包含变量引用(例如,$HOME)。
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 `
  -RequiredAssemblies C:\path\to\WinSCPnet.dll

在模块文件夹中创建脚本模块文件(*.psm1):
创建文件./tm/tm.psm1并定义你的类;例如:
    class Foo {
      # As a simple example, return the full name of the WinSCP type.
      [string] Bar() {
        return [WinSCP.Protocol].FullName
      }
    }

注意:在现实世界中,模块通常放置在$env:PSMODULEPATH定义的标准位置之一,这样就可以仅通过名称引用模块,而无需指定(相对)路径。 使用模块
PS> using module ./tm; [Foo]::new().Bar()
WinSCP.Protocol

使用using module语句导入模块,并且与Import-Module不同的是,它还使得模块中定义的在当前会话中可用。
由于导入模块隐式加载了WinSCP程序集,这要归功于模块清单中的RequiredAssemblies键,因此实例化引用该程序集类型的Foo类成功。
如果您需要动态确定依赖程序集的路径以加载它,甚至可以临时编译一个(在这种情况下,使用RequiredAssemblies清单条目不是一个选择),您应该能够使用Justin Grote的有用答案中推荐的方法 - 即使用指向一个*.ps1脚本的ScriptsToProcess清单条目,该脚本调用Add-Type来在加载脚本模块(*.psm1)之前动态加载依赖程序集 - 但是截至本文撰写时,这实际上在PowerShell 7.3.4(当前版本)中无法正常工作:尽管依赖程序集类型的class在依赖程序集的*.psm1文件中的定义成功,但在执行带有using module ./tm语句的脚本第二次之前,调用者无法看到该class
创建一个示例模块:
# Create module folder (remove a preexisting ./tm folder if this fails).
$null = New-Item -Type Directory -ErrorAction Stop ./tm

# Create a helper script that loads the dependent
# assembly.
# In this simple example, the assembly is created dynamically,
# with a type [demo.FooHelper]
@'
Add-Type @"
namespace demo {
  public class FooHelper {
  }
}
"@
'@ > ./tm/loadAssemblies.ps1

# Create the root script module.
# Note how the [Foo] class definition references the
# [demo.FooHelper] type created in the loadAssemblies.ps1 script.
@'
class Foo {
  # Simply return the full name of the dependent type.
  [string] Bar() {
    return [demo.FooHelper].FullName
  }
}
'@ > ./tm/tm.psm1

# Create the manifest file, designating loadAssemblies.ps1
# as the script to run (in the caller's scope) before the
# root module is parsed.
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 -ScriptsToProcess loadAssemblies.ps1

现在,截至 PowerShell 7.3.4 版本,尝试使用模块的 [Foo] 类只有在调用 "using module ./tm" 两次后才能成功 - 而在单个脚本中无法实现这一点,因此目前这种方法是无效的。
# Up to at least PowerShell 7.3.4
# !! First attempt FAILS:
PS> using module ./tm; [Foo]::new().Bar()
InvalidOperation: Unable to find type [Foo]

# Second attempt: OK
PS> using module ./tm; [Foo]::new().Bar()
demo.FooHelper

问题是众所周知的,事实证明,它可以追溯到2017年-请参阅GitHub问题#2962


如果您的使用情况不允许使用模块:
- 在紧急情况下,您可以使用Invoke-Expression,但请注意,为了保证稳定性并避免安全风险,通常最好避免使用Invoke-Expression[1]。
# Adjust this path as needed.
Add-Type -LiteralPath C:\path\to\WinSCPnet.dll

# By placing the class definition in a string that is invoked at *runtime*
# via Invoke-Expression, *after* the WinSCP assembly has been loaded, the
# class definition succeeds.
Invoke-Expression @'
class Foo {
  # Simply return the full name of the WinSCP type.
  [string] Bar() {
    return [WinSCP.Protocol].FullName
  }
}
'@

[Foo]::new().Bar()

或者,可以采用“两脚本方法”: - 主脚本加载依赖的程序集, - 然后点源(dot-source)第二个包含依赖程序集类型的类定义的脚本。
这种方法在Takophiliac的有用答案中有示例演示。

[1] 这个案例并不担心,但是一般来说,由于Invoke-Expression可以调用存储在字符串中的任意命令,在对不完全受您控制的字符串应用它时可能会导致执行恶意命令 - 更多信息请参见this answer。 这个警告同样适用于其他语言,比如Bash的内置eval命令。


我看到这个最近有更新。对于7.3.4版本,这还是最新的吗?是否可以创建一个没有.dll程序集的type?必须使用C#吗,还是所有内容都可以用PowerShell编写?如果可以的话,我在哪里可以找到一个例子? - lit
@lit,是的,我认为答案仍然是最新的(我刚刚更新了版本引用)。使用PowerShell class定义也可以创建.NET类型,但仍有一些情况需要借助C#来解决。 - mklement0

5

另一个解决方案是将Add-Type逻辑放入一个单独的.ps1文件中(命名为AssemblyBootStrap.ps1或其他名称),然后将其添加到模块清单的ScriptsToProcess部分。 ScriptsToProcess在根脚本模块(*.psm1)之前运行,而程序集将在类定义需要它们的时候加载。


这很有前途,处理的顺序就像你描述的那样,但是当在ScriptsToProcess脚本中定义依赖于Add-Type加载的程序集的类时,虽然成功了,但是在一个以using module开头的脚本中(必要时),这样的类在调用者的范围内不会出现 - 直到第二次运行该脚本(截至PowerShell Core 7.2.0-preview.9)。 - mklement0
问题是众所周知的,事实证明,它在2017年就已经报告了 - 请参见GitHub问题#2962。我已经在我的答案中添加了一个自包含的可重现案例。 - mklement0

4

首先,我建议使用mklement0的答案。

然而,在较小的项目或早期阶段,您可以进行一些额外的操作,以更少的工作量获得相同的效果,这可能会有所帮助。

您可以在代码中仅 . source 另一个包含类引用尚未加载库的ps1文件,之后加载引用的程序集。

##########
MyClasses.ps1

Class myClass
{
     [3rdParty.Fancy.Object] $MyFancyObject
}

然后,您可以使用“.”从主脚本调用您的自定义类库。
#######
MyMainScriptFile.ps1

#Load fancy object's library
Import-Module Fancy.Module #If it's in a module
Add-Type -Path "c:\Path\To\FancyLibrary.dll" #if it's in a dll you have to reference

. C:\Path\to\MyClasses.ps1

原始解析将通过审核,脚本将启动,你的参考资料将被添加,随着脚本的继续,. sourced 文件将被读取和解析,由于引用库已在内存中,所以你的自定义类别将被添加而没有问题。
最好还是制作并使用具有适当清单的模块,但这样做非常容易记住和使用。

1
虽然这不是完美的解决方案,但我已经解决了。然而,我仍然保留这个问题,因为它仍然存在。
我没有使用WinSCP类型,而是使用字符串。因为我已经有了与WinSCP.Protocol相同的枚举值。
Enum Protocols {
    Sftp
    Ftp
    Ftps
}

并且已在FtpSettings中设置了协议。

$FtpSettings.Protocol = [Protocols]::Sftp

我可以这样设置协议:

$SessionOptions = New-Object WinSCP.SessionOptions -Property @{
            Protocol = $this.FtpSettings.Protocol.ToString()
            HostName = $this.FtpSettings.Server
            UserName = $this.FtpSettings.Username
            Password = $this.FtpSettings.Password
        }

我在 [WinSCP.TransferMode] 上使用了类似的方法。

$TransferOptions.TransferMode = "Binary" #[WinSCP.TransferMode]::Binary

0

添加类型 -LiteralPath C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.IO.Compression.FileSystem.dll $psclasses = GC "C:\Windows\Temp\foobarclass.ps1" -Raw Invoke-Expression $psclasses [Foo]::new().Bar()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接