对于已编译的 Cmdlet,如何从调用的脚本块立即输出每个对象?

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

考虑以下因素

Invoke-Command {
    'a'; Write-Verbose 'a' -Verbose
    'b'; Write-Verbose 'b' -Verbose
} |
. { process { Write-Verbose "downstream_$_" -Verbose } }

输出

VERBOSE: downstream_a
VERBOSE: a
VERBOSE: downstream_b
VERBOSE: b

请注意,当第一个脚本块发出对象时,会立即调用第二个脚本块的

process{}

另一方面,考虑类似的构造,除了用自定义编译的

Invoke-Command
替换
Cmdlet

Add-Type       `
    -PassThru  `
    -TypeDefinition @'
using System.Management.Automation;

namespace n {
    [Cmdlet(VerbsLifecycle.Invoke,"SomeScriptBlockThing")]
    public class InvokeSomeScriptBlockThing : Cmdlet
    {
        [Parameter(Mandatory=true,Position=1)]
        public ScriptBlock ScriptBlock { get; set; }
        protected override void EndProcessing() {
            var output = ScriptBlock.Invoke("some_fancy_argument");
            foreach (var o in output) {
                WriteObject(o);
            }
        }
    }
}
'@             |
    % Assembly |
    Import-Module

Invoke-SomeScriptBlockThing {
    param($SomeFancyArgument)
    'a'; Write-Verbose 'a' -Verbose
    'b'; Write-Verbose 'b' -Verbose
} |
    . { process { Write-Verbose "downstream_$_" -Verbose } }

输出

VERBOSE: a
VERBOSE: b
VERBOSE: downstream_a
VERBOSE: downstream_b

请注意,在这种情况下,第一个脚本块输出的所有对象都会累积在

output
中,直到第一个脚本块完成,然后所有对象都会一次性输出。

我在这里解决的真正挑战是使用

此处
演示的技术获得CancellationToken。 对于该技术,
Cmdlet
的调用必须比令牌更长寿。 确保这一点的自然方法是让
Cmdlet
使其调用的脚本块可以使用令牌(其中
$SomeFancyArgument
出现在此处)。 但根据所示的实现,在脚本块完成之前,
Cmdlet
不会发出任何内容。

如何使我的

Cmdlet
的执行顺序与第一个示例的执行顺序相匹配? 也就是说,如何让我的
Cmdlet
输出由它调用的脚本块生成的每个对象?


请注意,这个旧的问题和答案可以说是重复的,但它早于 PowerShell 的开源,因此没有优势了解例如

Invoke-Command
Cmdlet
如何实现这一目标。 该问题的唯一答案通过将脚本块转换为字符串并启动一个完全独立的 PowerShell 实例来实现目标,该实例除其他外,在完全独立的
SessionState
中调用脚本,而无需访问任何父作用域。 这是同一个问题,但来自另一个时代。

powershell pipeline powershell-cmdlet scriptblock
1个回答
0
投票

您本质上要求的是所谓的streaming,如果您同步调用脚本块,那么实现起来并不难,您需要从每个

订阅
DataAdding事件
 PowerShell.Streams
并将您的输出 (success) 流作为
TOutput

using System.Threading;
using System.Management.Automation;

[Cmdlet(VerbsLifecycle.Invoke, "SomeScriptBlockThing")]
public class InvokeSomeScriptBlockThing : PSCmdlet
{
    [Parameter(Mandatory = true, Position = 0)]
    public ScriptBlock ScriptBlock { get; set; }

    protected override void EndProcessing()
    {
        using PSDataCollection<PSObject> output = [];
        using PowerShell ps = PowerShell
            .Create(RunspaceMode.CurrentRunspace)
            .AddScript(ScriptBlock.ToString(), useLocalScope: true)
            // should be the Token from CancellationTokenSource here:
            .AddArgument(CancellationToken.None); 

        ps.Streams.Verbose.DataAdding += (s, e) => WriteVerbose(((VerboseRecord)e.ItemAdded).Message);
        output.DataAdding += (s, e) => WriteObject(e.ItemAdded);
        ps.Invoke(null, output); // hook output and null input
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.