【问题标题】:PowerShell: Unable to find type when using PS 5 classesPowerShell:使用 PS 5 类时无法找到类型
【发布时间】:2017-03-16 14:49:55
【问题描述】:

我在 PS 中使用带有 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 cmdlet 放在最上面一行并带有指向WinSCPnet.dll 的直接路径,它也不会加载——它似乎会在运行任何东西之前检测到丢失的类型。

【问题讨论】:

    标签: powershell winscp-net add-type


    【解决方案1】:

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

    • 从 PSv5.1 开始,即使是脚本顶部的 using assembly 语句在这种情况下也无济于事,因为在您的情况下,类型是在 PS 类定义的上下文中引用的 - 这个may get fixed in PowerShell Core, however;所需的工作以及其他与课程相关的问题正在GitHub issue #6652 中进行跟踪。

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

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

    这是一个简化的演练

    创建测试模块tm如下

    • 在其中创建模块文件夹./tm 和清单(*.psd1):

        # 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\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中定义的标准位置之一,这样模块就可以只被name引用,而不需要指定一个(相对)路径。

    使用模块

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

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

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


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

    • 创建示例模块:
    # 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.2.0-preview.9 开始,尝试使用模块的 [Foo] 类只能在调用 using module ./tm 后莫名其妙地成功两次 -你不能在一个 single 脚本中做到这一点,目前这种方法毫无用处:
    # 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
    

    事实证明,问题是一个已知问题,可以追溯到 2017 年 - 请参阅 GitHub issue #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()
    
    • 或者,使用两个-脚本方法
      • 加载依赖程序集的主脚本,
      • 然后点源第二个脚本,其中包含依赖于依赖程序集类型的 class 定义。

    Takophiliac's helpful answer 中演示了这种方法。


    [1] 在 this 的情况下这不是问题,但一般来说,鉴于Invoke-Expression 可以调用存储在字符串中的 any 命令,应用它对字符串不完全由您控制可能会导致执行恶意命令 - 请参阅this answer 了解更多信息。 此警告类似地适用于其他语言,例如 Bash 的内置 eval 命令。

    【讨论】:

      【解决方案2】:

      虽然它本身不是解决方案,但我解决了它。但是,我会保留这个问题,因为它仍然存在

      我没有使用 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
      

      【讨论】:

        【解决方案3】:

        首先,我会推荐 mklement0 的答案。

        但是,您可以做一些跑来跑去,以更少的工作获得大致相同的效果,这对于较小的项目或早期阶段可能会有所帮助。

        可能只是 .在您的代码中获取另一个 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
        

        原始解析将通过,脚本将启动,您的引用将被添加,然后随着脚本的继续,.源文件将被读取和解析,添加您的自定义类没有问题,因为它们的参考库在解析代码时已在内存中。

        使用适当的清单制作和使用模块仍然要好得多,但这会很容易,并且很容易记住和使用。

        【讨论】:

          【解决方案4】:

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

          【讨论】:

          • 这是有希望的,并且处理顺序如您所描述的那样,但是在 定义 一个依赖于 ScriptsToProcess 脚​​本中的 Add-Type 加载程序集的类时会成功,这样的类不会出现在调用者的范围内的脚本中(必须)以using module开头 - 直到脚本运行的时间(如PowerShell Core 7.2.0-preview.9)。
          • 事实证明,这个问题是一个已知问题,早在 2017 年就被报告了 - 请参阅 GitHub issue #2962。我在答案中添加了一个独立的可重现案例。
          猜你喜欢
          • 1970-01-01
          • 2020-04-02
          • 1970-01-01
          • 1970-01-01
          • 2023-01-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多