我正在尝试创建一个类对象并使用
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
脚本块的上下文中重写函数。
这是一个较旧的问题,但与我相关。我找到了另一种方法来达到我的目的:
TestClass
在远程环境中已知,可以在处理之前将其包含在脚本的抽象语法树 (AST) 中。这对于 using statements(必须仅在文件顶部声明)或 functions(可以在远程脚本中本地使用和,无需双重声明)也非常有用。 Edit-RemoteScript
函数就是用于此目的。 (该解决方案的灵感来自另一个论坛中的这个答案。这个非常有用的工具可以帮助探索 AST。)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的回答建议:使功能更加通用;还添加了基于评论的帮助
和后台作业中使用的基于 XML 的序列化/反序列化仅以类型保真度处理少数已知类型。
像您这样的自定义类的实例是通过[pscustomobject]
实例形式的无方法“属性包”进行
模拟,这就是为什么您的类实例的模拟实例在远程计算机上没有方法。 有关 PowerShell 序列化/反序列化的更详细概述,请参阅
此答案的底部部分。
Mike Twc 所建议的,您可以通过将类 定义 传递给远程命令来解决该问题,允许您在那里重新定义类,然后在远程会话中重新创建自定义类的实例。
虽然您无法直接
动态获取自定义
class
定义的源代码,但您可以通过将其放置在帮助器脚本块中来解决此问题,这使您可以:
class
)定义
. { ... }
本地
通过脚本块的源代码远程重新创建它
{
和
}
)只需通过对其进行字符串化即可获得,即通过对其调用
.ToString()
。事实上,在 PowerShell 远程处理的上下文中,脚本块隐式变成了它的字符串表示形式——令人惊讶的是;请参阅 GitHub 问题 #11698。
Invoke-Expression
通常应该避免,但它的使用在这里是安全的,因为被评估为 PowerShell 代码的字符串完全在您的控制之下。
作用域而不是参数(通过
-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
的类似方法,请参阅此答案。