如何将参数列表中的类对象传递到另一台计算机并在其上调用函数?

问题描述 投票:0回答:2

我正在尝试创建一个类对象并使用

Invoke-Command
来调用远程计算机上的类上的函数。当我使用没有计算机名称的
Invoke-Command
时,效果很好,但是当我尝试在远程计算机上执行此操作时,我收到错误消息,指出该类型不包含我的方法。这是我用来测试这个的脚本。

$ComputerName = "<computer name>"

[TestClass]$obj = [TestClass]::new("1", "2")

Get-Member -InputObject $obj

$credentials = Get-Credential

Invoke-Command -ComputerName $ComputerName -Credential $credentials -Authentication Credssp -ArgumentList ([TestClass]$obj) -ScriptBlock {
    $obj = $args[0]

    Get-Member -InputObject $obj

    $obj.DoWork()
    $obj.String3
}

class TestClass {
    [string]$String1
    [string]$String2
    [string]$String3

    [void]DoWork(){
        $this.String3 = $this.String1 + $this.String2
    }

    TestClass([string]$string1, [string]$string2) {
        $this.String1 = $string1
        $this.String2 = $string2
    }
}

这是我得到的输出。

PS > .\Test-Command.ps1

cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:
User: <my user>
Password for user <my user>: *

   TypeName: TestClass

Name        MemberType Definition
----        ---------- ----------
DoWork      Method     void DoWork()
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()
String1     Property   string String1 {get;set;}
String2     Property   string String2 {get;set;}
String3     Property   string String3 {get;set;}


   TypeName: Deserialized.TestClass

Name     MemberType Definition
----     ---------- ----------
GetType  Method     type GetType()
ToString Method     string ToString(), string ToString(string format, System.IFormatProvider formatProvider), string IFormattable.ToString(string format, System.IFormatProvider formatProvider)
String1  Property   System.String {get;set;}
String2  Property   System.String {get;set;}
String3  Property    {get;set;}
Method invocation failed because [Deserialized.TestClass] does not contain a method named 'DoWork'.
    + CategoryInfo          : InvalidOperation: (DoWork:String) [], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound
    + PSComputerName        : <computer name>

我可以看到类型从

TestClass
变为
Deserialized.TestClass
,我想知道是否有办法解决这个问题?我的目标是能够将我需要的函数发送到我运行脚本的每台机器,这样我就不必在
Invoke-Command
脚本块的上下文中重写函数。

powershell serialization invoke-command
2个回答
2
投票

这是一个较旧的问题,但与我相关。我找到了另一种方法来达到我的目的:

  1. 为了使
    TestClass
    在远程环境中已知,可以在处理之前将其包含在脚本的抽象语法树 (AST) 中。这对于 using statements(必须仅在文件顶部声明)或 functions(可以在远程脚本中本地使用,无需双重声明)也非常有用。
    Edit-RemoteScript
    函数就是用于此目的。 (该解决方案的灵感来自另一个论坛中的这个答案这个非常有用的工具可以帮助探索 AST。)
  2. 为了远程获取自定义类的对象作为“活”对象或从远程环境返回后,可以将其从Deserialized.TestClass
    转换为
    TestClass
    。接受 
    PSObject
     的新构造函数可以实现此目的。或者,也接受 
    op-Implicit
    op_Explicit
    PSObject
     运算符也可以执行相同的操作。在此运算符内,必须调用类构造函数。

示例代码说明了功能:

using namespace Microsoft.PowerShell.Commands using namespace System.Collections using namespace System.Diagnostics.CodeAnalysis using namespace System.Management.Automation using namespace System.Management.Automation.Language Set-StrictMode -Version ([Version]::new(3, 0)) class TestClass { [string]$String1 [string]$String2 [string]$String3 [void]DoWork() { $this.String3 = $this.String1 + $this.String2 } TestClass([string]$string1, [string]$string2) { $this.String1 = $string1 $this.String2 = $string2 } TestClass([PSObject]$ClassObject) { $this.String1 = $ClassObject.String1 $this.String2 = $ClassObject.String2 $this.String3 = $ClassObject.String3 } } <# .DESCRIPTION This function adds using statements, functions, filters and types to ScriptBlocks to be used for remote access. .PARAMETER ScriptBlock The ScriptBlock to be processed. Mandatory. .PARAMETER Namespace The list of namespaces to add. 'default' adds any namespaces listed in the root script's using statements. Alternatively or additionally, any other namespaces can be added. These have to be fully qualified. The statement 'using namespace' must not be prefixed. The using Statements are inserted in the processed ScriptBlock at the position where it contains the #ImportedUsings comment. Defaut is an empty list. .PARAMETER Module The list of PowerShell modules to add. 'default' adds any module listed in the root script's using statements. Alternatively or additionally, any other module can be added. The value of the argument can be a module name, a full module specification, or a path to a module file. When it is a path, the path can be fully qualified or relative. A relative path is resolved relative to the script that contains the using statement. The modules referenced by path must be located identically in the file systems of the calling site and the remote site. The statement 'using namespace' must not be prefixed. When it is a name or module specification, PowerShell searches the PSModulePath for the specified module. A module specification is a hashtable that has the following keys: - ModuleName - Required Specifies the module name. - GUID - Optional Specifies the GUID of the module. - It's also Required to specify at least one of the three below keys. - ModuleVersion - Specifies a minimum acceptable version of the module. - MaximumVersion - Specifies the maximum acceptable version of the module. - RequiredVersion - Specifies an exact, required version of the module. This can't be used with the other Version keys. The using Statements are inserted in the processed ScriptBlock at the position where it contains the #ImportedUsings comment. Defaut is an empty list. .PARAMETER Assembly The list of .NET assemblies to add. 'default' adds any assembly listed in the root script's using statements. Alternatively or additionally, any other assembly can be added. The value can be fully qualified or relative path. A relative path is resolved relative to the script that contains the using statement. The assemblies referenced must be located identically in the file systems of the calling site and the remote site. The using Statements are inserted in the processed ScriptBlock at the position where it contains the #ImportedUsings comment. Defaut is an empty list. .PARAMETER Type The list of names from types defined by the root script to add the specified types to the processed script. The type definitions are inserted in the processed ScriptBlock at the position where it contains the #ImportedTypes comment. Defaut is an empty list. .PARAMETER Function The list of names from functions or filters defined by the root script to add the specified functions and filters to the processed script. The function definitions are inserted in the processed ScriptBlock at the position where it contains the #ImportedFunctions comment. Defaut is an empty list. .PARAMETER SearchNestedScriptBlocks If this parameter is set, ScriptBlocks contained in the root script are also searched for functions, filters and types, otherwise only the root script itself. .EXAMPLE In this example the namespaces used by the root script and two additional using namespace statements are added to $MyScriptBlock. One type and two functions, defined by the root script, are also added: $MyScriptBlock | Edit-RemoteScript ` -Namespace 'default', 'System.Collections', 'System.Collections.Generic' ` -Type 'MyType' ` -Function 'ConvertTo-MyType', 'ConvertFrom-MyType' .NOTES Because the using statements must come before any other statements in a module and no uncommented statement can precede it, including parameters, one cannot define any using statement in a nested ScriptBlock. Therefore, the only alternative to post-inserting the using statements into a previously defined ScriptBlock, as is done in this function, is to define $script as a string and create the ScriptBlock using [ScriptBlock]::Create($script). But then you lose syntax highlighting and other functionality of the IDE used. An alternative way of including functions, filters and types that are used in both, the root script and the remote script, in the latter is shown in the links below. An alternative to post-insertion would be to redefine these functions, filters, and types in the remote script. However, the downside is that changes to the code have to be kept in syn in different placesc, which reduces its maintainability. .LINK this function: https://stackoverflow.com/a/76695304/2883733 .LINK alternative for types: https://stackoverflow.com/a/59923349/2883733 .LINK alternative for functions: https://stackoverflow.com/a/71272589/2883733 #> function Edit-RemoteScript { [CmdletBinding()] [OutputType([ScriptBlock])] [SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'functionText', Justification = "Doesn't apply")] param( [Parameter(Mandatory, ValueFromPipeline)] [ScriptBlock]$ScriptBlock, [Parameter()] [AllowEmptyCollection()] [String[]]$Namespace = @(), [Parameter()] [AllowEmptyCollection()] [ModuleSpecification[]]$Module = @(), [Parameter()] [AllowEmptyCollection()] [String[]]$Assembly = @(), [Parameter()] [AllowEmptyCollection()] [String[]]$Type = @(), [Parameter()] [AllowEmptyCollection()] [String[]]$Function = @(), [Parameter()] [Switch]$SearchNestedScriptBlocks ) begin { [Ast]$cmdletAst = $MyInvocation.MyCommand.ScriptBlock.Ast do { [Ast]$tempAst = $cmdletAst.Parent } while ($null -ne $tempAst -and ($cmdletAst = $tempAst)) [String[]]$remoteUsings = @() [String[]]$remoteTypes = @() [String[]]$remoteFunctions = @() } process { if (($Namespace -or $Module -or $Assembly) -and -not $remoteUsings) { if ('default' -iin $Namespace -or 'default' -iin $Assembly -or ( $Module | Where-Object -Property 'Name' -EQ -Value 'default' | Select-Object -First 1 ) ) { [UsingStatementAst[]]$allUsings = @($cmdletAst.FindAll({ $args[0] -is [UsingStatementAst] }, $false)) } $remoteUsings = @( @( @{ Kind = [UsingStatementKind]::Namespace Names = $Namespace }, @{ Kind = [UsingStatementKind]::Module Names = $Module }, @{ Kind = [UsingStatementKind]::Assembly Names = $Assembly } ) | ForEach-Object -Process { [UsingStatementKind]$Kind = $_.Kind $_.Names | ForEach-Object -Process { if (($Kind -eq [UsingStatementKind]::Module -and $_.Name -ieq 'default') -or ($Kind -ne [UsingStatementKind]::Module -and $_ -ieq 'default')) { @($allUsings | Where-Object -Property 'UsingStatementKind' -EQ -Value $Kind | ForEach-Object -Process { $_.ToString() }) } else { if ($Kind -eq [UsingStatementKind]::Assembly) { "using $( $Kind.ToString().ToLowerInvariant() ) '$_'" } else { "using $( $Kind.ToString().ToLowerInvariant() ) $_" } } } } ) } if ($Type -and -not $remoteTypes) { $remoteTypes = @( $cmdletAst.FindAll({ $args[0] -is [TypeDefinitionAst] }, $SearchNestedScriptBlocks) | Where-Object -Property 'Name' -In $Type | ForEach-Object -Process { $_.ToString() } ) } if ($Function -and -not $remoteFunctions) { $remoteFunctions = @( if ($SearchNestedScriptBlocks) { # this is slower $cmdletAst.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true) | Where-Object -FilterScript { $_.Name -iin $Function -and ([String]$functionText = $_.ToString()) -imatch '^(function|filter)\s+' } | ForEach-Object -Process { $functionText } } else { # this is faster Get-ChildItem -Path 'Function:' | Where-Object -Property 'Name' -In $Function | ForEach-Object -Process { if ($_.CommandType -eq [CommandTypes]::Filter) { "filter $( $_.Name ) {$( $_.ScriptBlock.ToString() )}" } else { "function $( $_.Name ) {$( $_.ScriptBlock.ToString() )}" } } } ) } [ScriptBlock]::Create($ScriptBlock.ToString(). ` Replace('#ImportedUsings', $remoteUsings -join "`n"). ` Replace('#ImportedTypes', $remoteTypes -join "`n"). ` Replace('#ImportedFunctions', $remoteFunctions -join "`n")) } end { } } function TestFunction { 42 } $ComputerName = 'Server1' [TestClass]$obj = [TestClass]::new('1', '2') [ScriptBlock]$testScript = { #ImportedUsings # the imported using statements will be inserted here Set-StrictMode -Version ([Version]::new(3, 0)) #ImportedTypes # the imported types will be inserted here #ImportedFunctions # the imported functions will be inserted here $obj = $args[0] [ArrayList]$results = @() # using statements are working remotely [TestClass]$castedObj = [TestClass]$obj # the type is known remotely [void]$results.Add('') [void]$results.Add('* * * remote * * *') [void]$results.Add((TestFunction)) # the function is known remotely $castedObj.DoWork() # the type has his functionality remotely [void]$results.Add($castedObj.String3) [void]$results.Add((Get-Member -InputObject $obj)) [void]$results.Add((Get-Member -InputObject $castedObj)) [void]$results.Add('') [void]$results.Add($castedObj) [void]$results.Add([TestClass]::new('3', '4')) $results } $testScript = $testScript | Edit-RemoteScript -Namespace 'default' -Type 'TestClass' -Function 'TestFunction' $credentials = Get-Credential '* * * local * * *' TestFunction Get-Member -InputObject $obj $results = Invoke-Command -ComputerName $ComputerName -Credential $credentials -ArgumentList ([TestClass]$obj) -ScriptBlock $testScript foreach ($ctr in 0..6) { $results[$ctr] } [TestClass]$resultObj = $results[7] # also returned objects can be casted back to the original type "this is the original instance, DoWork() is already done, String3 = '$( $resultObj.String3 )'" $resultObj = $results[8] "this is a new instance, DoWork() isn't done yet, String3 = '$( $resultObj.String3 )'" $resultObj.DoWork() "... but now, String3 = '$( $resultObj.String3 )'"
输出:

* * * local * * * 42 TypeName: TestClass Name MemberType Definition ---- ---------- ---------- DoWork Method void DoWork() Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() String1 Property string String1 {get;set;} String2 Property string String2 {get;set;} String3 Property string String3 {get;set;} * * * remote * * * 42 12 TypeName: Deserialized.TestClass Name MemberType Definition ---- ---------- ---------- GetType Method type GetType() ToString Method string ToString(), string ToString(string format, System.IFormatProvider formatProvider), string IFormattable.ToString(string format, System.IFormatProvider formatPro… String1 Property System.String {get;set;} String2 Property System.String {get;set;} String3 Property {get;set;} TypeName: TestClass Name MemberType Definition ---- ---------- ---------- DoWork Method void DoWork() Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() String1 Property string String1 {get;set;} String2 Property string String2 {get;set;} String3 Property string String3 {get;set;} this is the original instance, DoWork() is already done, String3 = '12' this is a new instance, DoWork() isn't done yet, String3 = '' ... but now, String3 = '34'
在这种情况下,这肯定是一个很大的开销,而且重新定义实际上会更容易

TestClass

。然而,在具有复杂类的大型项目中,该过程可能是值得的。另一个优点:在进行更改时不再需要同步已声明多次的函数和类。

如果您正在使用

PSSession

,其中多个远程调用相继传递,甚至可能值得首先远程执行一个专门用于声明的脚本。然后可以使用特定类型参数类型 
TestClass
 来代替 
Object
PSObject
,因为在调用脚本时类型 
TestClass
 是已知的。在这种情况下可以省略参数的转换:

[ScriptBlock]$TestScript = { param([Parameter()] [TestClass]$Obj) .... $Obj.DoWork() # the type has his functionality remotely [void]$results.Add($Obj.String3) ... }

编辑1:对功能代码进行小修正并插入有用的链接

编辑2:@mklement0的回答建议:使功能更加通用;还添加了基于评论的帮助


2
投票
简而言之:PowerShell 在远程处理期间

和后台作业中使用的基于 XML 的序列化/反序列化仅以类型保真度处理少数已知类型

像您这样的自定义类的实例是通过[pscustomobject]实例形式的无方法“属性包”进行

模拟
,这就是为什么您的类实例的模拟实例在远程计算机上没有方法。

有关 PowerShell 序列化/反序列化的更详细概述,请参阅

此答案的底部部分。


正如

Mike Twc 所建议的,您可以通过将类 定义 传递给远程命令来解决该问题,允许您在那里重新定义类,然后在远程会话中重新创建自定义类的实例。

虽然您无法

直接

动态获取自定义
class定义的源代码,但您可以通过将其放置在帮助器脚本块中来解决此问题,这使您可以:

一个简化的示例,它使用

$using:

 作用域
而不是参数(通过 -ArgumentList
)来包含调用者作用域中的值。

# Define your custom class in a helper script block # and dot-source the script block to define the class locally. # The script block's string representation is then the class definition. . ( $classDef = { class TestClass { [string] $String1 [string] $String2 [string] $String3 DoWork() { $this.String3 = $this.String1 + $this.String2 } TestClass([string] $string1, [string] $string2) { $this.String1 = $string1 $this.String2 = $string2 } # IMPORTANT: # Also define a parameter-less constructor, for convenient # construction by a hashtable of properties. TestClass() {} } } ) # Construct an instance locally. $obj = [TestClass]::new("1", "2") # Invoke a command remotely, passing both the class definition and the input object. Invoke-Command -ComputerName . -ScriptBlock { # Define the class in the remote session too, via its source code. # NOTE: This particular use of Invoke-Expression is safe, because you control the input, # but it should generally be avoided. # See https://blogs.msdn.microsoft.com/powershell/2011/06/03/invoke-expression-considered-harmful/ Invoke-Expression $using:classDef # Now you can cast the emulated original object to the recreated class. $recreatedObject = [TestClass] $using:obj # Now you can call the method... $recreatedObject.DoWork() # ... and output the modified property $recreatedObject.String3 }
另请参阅:

    有关远程使用本地定义的
  • function
     的类似方法,请参阅
    此答案
© www.soinside.com 2019 - 2024. All rights reserved.