Powershell: невозможно найти тип при использовании классов PS 5

Я использую классы в PS с WinSCP Powershell Assembly. В одном из методов я использую различные типы из 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

Невозможно найти тип [WinSCP.Protocol].

5 ответов

Решение

Как вы обнаружили, PowerShell отказывается запускать сценарии, содержащие определения классов, которые ссылаются на недоступные (еще не загруженные) типы - этап разбора сценария завершается неудачно.

Правильным решением является создание скриптового модуля ( *.psm1 ) чей связанный манифест ( *.psd1 ) объявляет сборку, содержащую ссылочные типы, обязательным условием через RequiredAssemblies ключ.

Посмотрите альтернативное решение внизу, если использование модулей не вариант.

Вот упрощенное прохождение:

Создать тестовый модуль tm следующим образом:

  • Создать папку модуля ./tm и манифест (*.psd1) в этом:

    # Create module folder
    mkdir ./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 {
      # Simply return the full name of the WinSCP type.
      [string] Bar() {
        return [WinSCP.Protocol].FullName
      }
    }
    

    Примечание. В реальном мире модули обычно размещаются в одном из стандартных мест, определенных в $env:PSMODULEPATH, так что на модуль можно ссылаться только по имени, без необходимости указывать (относительный) путь.

Используйте модуль:

PS> using module ./tm; (New-Object Foo).Bar()
WinSCP.Protocol

using module оператор импортирует модуль и - в отличие Import-Module - также делает класс, определенный в модуле, доступным для текущего сеанса.

После импорта модуля неявно загружена сборка WinSCP благодаря RequiredAssemblies введите манифест модуля, создание экземпляра класса Foo, который ссылается на типы сборки, успешно.


Если ваш вариант использования не позволяет использовать модули, вы можете использовать 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
  }
}
'@

(New-Object Foo).Bar()

[1] Это не проблема в данном случае, но в целом, учитывая, что Invoke-Expression может вызвать любую команду, хранящуюся в строке, применение ее к строкам, не полностью контролируемым вами, может привести к выполнению вредоносных команд. Это предостережение относится к другим языкам аналогично, например, к встроенному в Bash eval команда.

Дополнительное решение - поместить логику Add-Type в отдельный файл .ps1 (назовите его или что-то в этом роде), а затем добавьте его в раздел манифеста вашего модуля. выполняется перед манифестом модуля, и сборки будут загружены в то время, когда определения классов ищут их.

Во-первых, я бы порекомендовал ответ 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

Исходный синтаксический анализ пройдет проверку, скрипт запустится, ваша ссылка будет добавлена, а затем, по мере продолжения скрипта, файл. Исходный файл будет прочитан и проанализирован, добавив ваши пользовательские классы без проблем, поскольку их справочная библиотека находится в памяти к моменту анализа кода.

По-прежнему намного лучше создать и использовать модуль с надлежащим манифестом, но это легко пройдет, и его очень легко запомнить и использовать.

Хотя это не решение само по себе, я обошел его. Тем не менее, я оставлю вопрос открытым, поскольку он все еще стоит

Вместо использования 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

Add-Type -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()

Другие вопросы по тегам