PowerShell:使用 PS 5 类时无法找到类型 [英] PowerShell: Unable to find type when using PS 5 classes

查看:11
本文介绍了PowerShell:使用 PS 5 类时无法找到类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在 PS 中使用 WinSCP PowerShell 程序集类.在其中一种方法中,我使用了 WinSCP 中的各种类型.

只要我已经添加了程序集,这就可以正常工作 - 但是,由于 PowerShell 在使用类时读取脚本的方式(我假设?),在加载程序集之前会引发错误.

其实,即使我在最上面放了一个Write-Host,它也不会加载.

在解析文件的其余部分之前,有什么方法可以强制运行某些内容吗?

Transfer() {$this.Logger = [Logger]::new()尝试 {添加类型 -Path $this.Paths.WinSCP$ConnectionType = $this.FtpSettings.Protocol.ToString()$SessionOptions = 新对象 WinSCP.SessionOptions -Property @{协议 = [WinSCP.Protocol]::$ConnectionType主机名 = $this.FtpSettings.Server用户名 = $this.FtpSettings.Username密码 = $this.FtpSettings.Password}

导致这样的错误:

Protocol = [WinSCP.Protocol]::$ConnectionType无法找到类型 [WinSCP.Protocol].

但是我在哪里加载程序集并不重要.即使我将 Add-Type cmdlet 放在带有 WinSCPnet.dll 直接路径的最上面一行,它也不会加载 - 它在运行任何东西之前检测丢失的类型,好像.

解决方案

如您所见,PowerShell 拒绝运行包含引用当时不可用(尚未加载)类型的类定义的脚本 - 脚本解析阶段失败.

正确的解决方案是创建一个脚本模块(*.psm1),其关联的清单(*.psd1)声明包含引用类型的程序集是先决条件,通过 RequiredAssemblies 键.

如果不能选择使用模块,请参阅底部的替代解决方案.

这是一个简单的演练:

创建测试模块tm如下:

使用您的类定义创建文件 ./tm/tm.psm1;例如:

 class Foo {# 作为一个简单的例子,返回WinSCP类型的全名.[字符串] 酒吧(){返回 [WinSCP.Protocol].FullName}}

注意:在现实世界中,模块通常放置在$env:PSMODULEPATH中定义的标准位置之一,这样模块就可以通过名称来引用仅,无需指定(相对)路径.

使用模块:

PS>使用模块 ./tm;[Foo]::new().Bar()WinSCP协议

using module 语句导入模块并且 - 与 Import-Module 不同 -还使模块中定义的可用于当前会话.

由于模块清单中的 RequiredAssemblies 键,导入模块隐式加载了 WinSCP 程序集,因此实例化引用程序集类型的类 Foo 成功.


如果您需要动态确定依赖程序集的路径以便加载它,或者甚至是临时编译一个(在这种情况下使用RequiredAssemblies 清单条目不是一个选项),应该能够使用Justin Grote 的有用答案 - 即,使用指向 *.ps1 脚本的 ScriptsToProcess 清单条目在加载脚本模块 (*.psm1) 之前,调用 Add-Type 动态加载依赖程序集 - 但这不会'从 PowerShell 7.2.0-preview.9 开始实际工作:而 *.psm1class定义依赖于依赖程序集类型的文件成功,调用者不会看到 class 直到带有 using module ./tm 语句的脚本可执行程序时间:

  • 创建示例模块:

# 创建模块文件夹(如果失败,删除预先存在的 ./tm 文件夹).$null = New-Item -Type Directory -ErrorAction Stop ./tm# 创建一个加载依赖的辅助脚本# 集会.# 在这个简单的例子中,程序集是动态创建的,# 类型为 [demo.FooHelper]@'添加类型@"命名空间演示{公共类 FooHelper {}}"@'@ >./tm/loadAssemblies.ps1# 创建根脚本模块.# 注意 [Foo] 类定义如何引用# [demo.FooHelper] 在 loadAssemblies.ps1 脚本中创建的类型.@'类 Foo {# 简单地返回依赖类型的全名.[字符串] 酒吧(){返回 [demo.FooHelper].FullName}}'@ >./tm/tm.psm1# 创建清单文件,指定 loadAssemblies.ps1# 作为在调用之前运行的脚本(在调用者的范围内)# 根模块被解析.新模块清单 ./tm/tm.psd1 -RootModule tm.psm1 -ScriptsToProcess loadAssemblies.ps1

  • 现在,仍然是 PowerShell 7.2.0-preview.9,尝试 use 模块的 [Foo] 类只能在调用 using module 后莫名其妙地成功./tm twice - 您无法在 单个 脚本中执行此操作,因此现在这种方法无用:

# 从 PowerShell 7.2.0-preview.9 开始:#!!第一次尝试失败:PS>使用模块 ./tm;[Foo]::new().Bar()InvalidOperation:无法找到类型 [Foo]# 第二次尝试:好的PS>使用模块 ./tm;[Foo]::new().Bar()demo.FooHelper

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


如果您的用例不允许使用模块:

  • 在紧要关头,您可以使用 Invoke-Expression,但请注意,为了健壮性,从而避免安全风险[1].

# 根据需要调整这个路径.添加类型 -LiteralPath C:path	oWinSCPnet.dll# 通过将类定义放在在*运行时*调用的字符串中# 通过 Invoke-Expression,*在 WinSCP 程序集加载后,# 类定义成功.调用表达式@'类 Foo {# 只需返回 WinSCP 类型的全名.[字符串] 酒吧(){返回 [WinSCP.Protocol].FullName}}'@[Foo]::new().Bar()

  • 或者,使用两个脚本方法:
    • 加载依赖程序集的主脚本,
    • 然后点源第二个脚本,其中包含依赖于依赖程序集的类型的 class 定义.

这种方法在 Takophiliac 的有用回答中有所体现.


[1] 在这种情况下不是问题,但一般来说,鉴于Invoke-Expression可以调用任何命令存储在字符串中,将其应用于不受您完全控制的字符串可能会导致执行恶意命令.此警告类似地适用于其他语言,例如 Bash 的内置 eval 命令.

I'm using classes in PS with WinSCP PowerShell Assembly. In one of the methods I'm using various types from WinSCP.

This works fine as long as I already have the assembly added - however, because of the way PowerShell reads the script when using classes (I assume?), an error is thrown before the assembly could be loaded.

In fact, even if I put a Write-Host at the top, it will not load.

Is there any way of forcing something to run before the rest of the file is parsed?

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
        }

Results in an error like this:

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

But it doesn't matter where I load the assembly. Even if I put the Add-Type cmdlet on the topmost line with a direct path to WinSCPnet.dll, it won't load - it detects the missing types before running anything, it seems.

解决方案

As you've discovered, PowerShell refuses to run scripts that contains class definitions that reference then-unavailable (not-yet-loaded) types - the script parsing stage fails.

  • As of PSv5.1, even a using assembly statement at the top of a script does not help in this case, because in your case the type is referenced in the context of a PS class definition - this may get fixed in PowerShell Core, however; the required work, along with other class-related issues, is being tracked in GitHub issue #6652.

The proper solution is to create a script module (*.psm1) whose associated manifest (*.psd1) declares the assembly containing the referenced types a prerequisite, via the RequiredAssemblies key.

See alternative solution at the bottom if using modules is not an option.

Here's a simplified walk-through:

Create test module tm as follows:

  • Create module folder ./tm and manifest (*.psd1) in it:

      # Create module folder (remove a preexisting ./tm folder if this fails).
      $null = New-Item -Type Directory -ErrorAction Stop ./tm
    
      # Create manifest file that declares the WinSCP assembly a prerequisite.
      # Modify the path to the assembly as needed; you may specify a relative path, but
      # note that the path must not contain variable references (e.g., $HOME).
      New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 `
        -RequiredAssemblies C:path	oWinSCPnet.dll
    

  • Create the script module file (*.psm1) in the module folder:

Create file ./tm/tm.psm1 with your class definition; e.g.:

    class Foo {
      # As a simple example, return the full name of the WinSCP type.
      [string] Bar() {
        return [WinSCP.Protocol].FullName
      }
    }

Note: In the real world, modules are usually placed in one of the standard locations defined in $env:PSMODULEPATH, so that the module can be referenced by name only, without needing to specify a (relative) path.

Use the module:

PS> using module ./tm; [Foo]::new().Bar()
WinSCP.Protocol

The using module statement imports the module and - unlike Import-Module - also makes the class defined in the module available to the current session.

Since importing the module implicitly loaded the WinSCP assembly thanks to the RequiredAssemblies key in the module manifest, instantiating class Foo, which references the assembly's types, succeeded.


If you need to determine the path to the dependent assembly dynamically in order to load it or even to ad-hoc-compile one (in which case use of a RequiredAssemblies manifest entry isn't an option), you should be able to use the approach recommended in Justin Grote's helpful answer - i.e., to use a ScriptsToProcess manifest entry that points to a *.ps1 script that calls Add-Type to dynamically load dependent assemblies before the script module (*.psm1) is loaded - but this doesn't actually work as of PowerShell 7.2.0-preview.9: while the definition of the class in the *.psm1 file relying on the dependent assembly's types succeeds, the caller doesn't see the class until a script with a using module ./tm statement is executed a second time:

  • Create a sample module:

# 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

  • Now, still as of PowerShell 7.2.0-preview.9, trying to use the module's [Foo] class inexplicably succeeds only after calling using module ./tm twice - which you cannot do in a single script, rendering this approach useless for now:

# As of PowerShell 7.2.0-preview.9:
# !! 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

The problem is a known one, as it turns out, and dates back to 2017 - see GitHub issue #2962


If your use case doesn't allow the use of modules:

  • In a pinch, you can use Invoke-Expression, but note that it's generally better to avoid Invoke-Expression in the interest of robustness and so as to avoid security risks[1] .

# Adjust this path as needed.
Add-Type -LiteralPath C:path	oWinSCPnet.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()

  • Alternatively, use a two-script approach:
    • A main script that loads the dependent assemblies,
    • which then dot-sources a second script that contains the class definitions relying on the types from the dependent assemblies.

This approach is demonstrated in Takophiliac's helpful answer.


[1] It's not a concern in this case, but generally, given that Invoke-Expression can invoke any command stored in a string, applying it to strings not fully under your control can result in the execution of malicious commands. This caveat applies to other language analogously, such as to Bash's built-in eval command.

这篇关于PowerShell:使用 PS 5 类时无法找到类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆