1

我有下面的方法,它被不同的 PS 脚本调用,我希望 PowerShell 对象只创建一次,为此我将 Powershell 对象设为静态(参见下面的代码)。但后来它给了我错误

当前 PowerShell 实例的状态对此操作无效。

我该如何处理?优化以下代码的最佳方法是什么?注意:如果我删除静态,下面的代码可以正常工作。

class DataRulesPSScripts
    {
        static PowerShell ps = PowerShell.Create();
        public IEnumerable<object> RunScriptBlock( ScriptBlock scriptBlock, Dictionary<string, object> scriptParameters )
        {
            var vars = scriptParameters.Select( p => new PSVariable( p.Key, p.Value ) ).ToList();
            return scriptBlock.InvokeWithContext( null, vars );
        }

        public async Task<ScriptBlock> CreateScriptBlock( string pSScript )
        {            
            ps.AddScript( pSScript );
            var scriptBlock = (await ps.InvokeAsync())[0].BaseObject as ScriptBlock;
            return scriptBlock;
        }
    }
}

这是从这里调用的:

internal async Task<string> GeneratePartitionKey( Dictionary<string, EntityProperty> arg)
        {       
            var result =await GenerateKeys(arg);
            return result[0].ToString();
        }        

        internal async Task<string> GenerateRowKey( Dictionary<string, EntityProperty> arg )
        {
            var result = await GenerateKeys( arg );
            return result[1].ToString();
        }

        private async Task<List<object>> GenerateKeys( Dictionary<string, EntityProperty> arg )
        {
            var pars = new Dictionary<string, object>();
            pars.Add( "_", arg );
            DataRulesPSScripts ds = new DataRulesPSScripts();
            var scriptBlock = await ds.CreateScriptBlock( PSScript );
            var results = ds.RunScriptBlock( scriptBlock, pars ).ToList();
            return results;
        }
4

1 回答 1

2

没有理由ScriptBlock直接在您的 C# 代码中创建实例并与之交互 - 它们由 PowerShell SDK 在内部使用:[1]当您将一段 PowerShell 代码作为字符串传递给PowerShell.AddScript()方法时,它们是在内部创建和存储的,并且通过PowerShell实例的,.Invoke()方法调用。

虽然通过实例PowerShell创建通过.AddScript()调用ps.AddScript( pSScript ); var scriptBlock = (await ps.InvokeAsync())[0].BaseObject as ScriptBlock;( _ _直接在 C# 代码中创建脚本块,由于未连接到PowerShell 运行空间,您将无法全部调用它),此类调用仅提供成功输出-所有其他PowerShell 流的输出将丢失- 即,原始实例的.Invoke()PowerShell.Streams属性不会反映这样的输出,这会导致发生的非终止错误无法访问,同样,该.HadErrors属性也不会反映是否发生了非终止错误。因此,应该避免这种方法。[2]

这是一个隐式创建脚本块的示例,在幕后,通过PowerShell.AddScript()将参数传递给它并调用它:

// Define the script-block text.
// It expects a single argument that is an object with .Name and .Code
// properties, whose values are echoed.
// NOTE: Do NOT include { ... }
var scriptBlockText = "$obj = $args[0]; $obj.Name; $obj.Code";

// Define an object to pass to the script block.
var obj = new { Name = "Abc", Code = 42 };

using (var ps = PowerShell.Create()) {
  
  // Add the script block and an argument to pass to it.
  ps
    .AddScript(scriptBlockText)
    .AddArgument(obj);

  // Invoke and echo the results.  
  foreach (var o in ps.Invoke()) {
    Console.WriteLine(o);
  }

}

但是,上述内容不可重用,因为一旦您使用 or 添加了参数或参数.AddParameter(s).AddArgument()就无法删除它们并指定不同的参数来执行另一个调用 - 据我所知。

解决方法是使用 PowerShell管道输入(通过input您可以传递给的可选参数提供PowerShell.Invoke(),因为这样可以使用不同的输入进行重复调用。 但是,您的脚本块必须相应地构造:

// Define the script-block text.
// This time, expect the input to come via the *pipeline*, which
// can be accessed via the $input enumerator.
// NOTE: Do NOT include { ... }
var scriptBlockText = "$obj = $($input); $obj.Name; $obj.Code";

// Define two objects to pass to the script block, one each in 
// two separate invocations:
object[] objects = {
  new { Name = "Abc", Code = 42 },
  new { Name = "Def", Code = 43 }
};

using (var ps = PowerShell.Create()) {
  
  // Add the script block.
  ps.AddScript(scriptBlockText);

  // Perform two separate invocations.
  foreach (var obj in objects) {

    // For housekeeping, clean the previous non-success streams.
    ps.Streams.ClearStreams();

    // Invoke the script block with the object at hand and echo the results.
    // Note: The input argument must be an enumerable, so we wrap the object
    //       in an aux. array.
    foreach (var o in ps.Invoke(new[] { obj })) {
      Console.WriteLine(o);
    }

  }

}

或者,如果可行,请考虑不使用脚本块,因为它们需要解析(尽管在这种情况下是一次性开销)并且 - 在 Windows 上 - 受有效执行策略的约束,这可能会阻止它们的执行(尽管您可以绕过对每个进程的这种限制,请参阅此答案)。

如果没有脚本块,您必须单独调用一个或多个命令,使用PowerShell.AddCommand()调用,用PowerShell.AddStatement().

  • 如果单个命令或命令管道通过管道接受所有输入,则可以使用与上述相同的方法。

  • 否则 - 如果.AddParameter(s)/.AddArgument()需要 - 您必须在每次(重复)调用之前调用ps.Commands.Clear()重新添加命令;但是,与调用相比.AddScript(),这应该会引入很少的开销。


使可重用技术适应您的代码

Class DataRulesPSScripts,它使用静态PowerShell实例并在其静态构造函数中添加一次脚本块。

  • 注意:考虑改用实例属性并使类实现IDisposable以允许类用户控制 PowerShell 实例的生命周期。
class DataRulesPSScripts
{
  static PowerShell ps = PowerShell.Create();
  // The script-block text:
  // Note that $ParamA and $ParamB must correspond to the keys of the
  // dictionary passed to the script block on invocation via .InvokeAsync()
  static string PSScript = @"$argDict = $($input); & { param($ParamA, $ParamB) [pscustomobject] @{ Partition = $ParamA; Key = 1 }, [pscustomobject] @{ Row = $ParamB; Key = 2 } } @argDict";

  static DataRulesPSScripts() {
    // Add the script-block text only once, which therefore incurs the
    // overhead of parsing the text into a script block only once,
    // and allows repeated later invocations via .Invoke() with pipeline input.
    ps.AddScript(PSScript);
  }
  public async Task<IEnumerable<object>> RunScriptBlock(Dictionary<string, EntityProperty> scriptParameters)
  {
    // Pass the parameter dictionary as pipeline input.
    // Note: Since dictionaries are enumerable themselves, an aux. array
    //       is needed to pass the dictionary as a single object.
    return await ps.InvokeAsync<object>(new [] { scriptParameters });
  }

}

使用类的代码,通过管道传递参数:

internal async Task<string> GeneratePartitionKey(Dictionary<string, EntityProperty> arg)
{
  var result = await GenerateKeys(arg);
  return result[0].ToString();
}

internal async Task<string> GenerateRowKey(Dictionary<string, EntityProperty> arg)
{
  var result = await GenerateKeys(arg);
  return result[1].ToString();
}


private async Task<List<object>> GenerateKeys(Dictionary<string, EntityProperty> args)
{
  DataRulesPSScripts ds = new DataRulesPSScripts();
  var results = await ds.RunScriptBlock(args);
  return results.ToList();
}

示例调用(obj是包含上述方法的对象;假定EntityProperty具有属性的简化类.Value):

Console.WriteLine(
  obj.GenerateRowKey(
    new Dictionary<string, EntityProperty> { ["ParamA"] = new EntityProperty { Value = "bar" },  ["ParamB"] = new EntityProperty { Value = "baz" } }
  ).Result
);

上面应该产生类似的东西:

@{Row=demo.EntityProperty; Key=2}

这是脚本块输出的第二个自定义对象的字符串表示形式。


[1]相比之下,在PowerShell 脚本代码ScriptBlock中,直接使用实例,通常脚本块 文字( { ... })的形式&,通过调用运算符 调用

[2] 这是来自 PowerShell 的快速演示:
$ps=[PowerShell]::Create(); $sb = $ps.AddScript("{ 'hi'; Get-Item nosuchfile }").Invoke()[0]; "call result: $($sb.Invoke())"; "had errors: $($ps.HadErrors)"; "error stream: $($ps.Streams.Error)"
即使调用产生了非终止错误,也会.HadErrors报告$false,并且.Streams.Error是空的。

于 2021-02-23T18:15:54.017 回答