65.9K
CodeProject 正在变化。 阅读更多。
Home

从 C# 调用 PowerShell 的另一种解决方案

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (5投票s)

2021 年 11 月 26 日

CPOL

4分钟阅读

viewsIcon

14268

带有解释的基本 PowerShell 处理程序 C# 类。

引言

从 C# 支持 PowerShell 有点麻烦。在这里,我提供了一个简单的解决方案,而无需深入研究环境的细节。本文旨在成为一个系列文章的基础,该系列文章将通过从 C# 编写的程序调用 PowerShell 来支持各种 Microsoft 环境。

必备组件

所有示例均在 MS Visual Studio Community 2019 的 Windows 10 Pro 21H2 19044.1387 环境中创建、编译和测试。

要成功编译,需要安装 NuGet 包:Microsoft.PowerShell.3.ReferenceAssemblies

自定义扩展库中方法的先决条件

您可以使用相应命名空间中已定义的方法和类,但我包含了一些简单的解决方案示例,以供您理解和阐明。您当然可以以不同的方式(更简单或更清晰地)编写它们,但我们不必在此过多纠结。

以下方法在字符串值为空或仅包含空格时返回true

public static bool IsNullEmptyOrWhite(this string sValue) {...}

定义 PowerShell 参数的类,形式为Name/Value。它可以被任何方便的dictionary类替换

    public class ParameterPair
    {
        public string Name { get; set; } = string.Empty;
        public object Value { get; set; } = null;
    }

Using the Code

在 PowerShell 环境中执行命令

当然,有很多方法可以打开 PowerShell、在其内部发出命令并获取其结果。我将其限制为两种

  • 调用脚本,其中所有命令及其数据都必须表示为一行文本
  • 调用 Cmdlet,我们一次只能提供一个命令,并且其参数作为Name/Value对传递,其中值可以是任何类型

因此,定义了两个具有不同参数集的RunPS 方法,它们在经过适当的参数处理后,使用统一的ExecutePS方法。

/// <summary>
/// Basic method of calling PowerShell a script where all commands 
/// and their data must be presented as one line of text
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">A single line of text 
/// containing commands and their parameters (in text format)</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <returns>The method returns true when executed correctly 
/// and false when some errors have occurred</returns>
public static bool RunPS(PowerShell ps, string psCommand, out Collection<PSObject> outs)
{
    //Programmer's Commandment I: Remember to reset your variables
    outs = new Collection<PSObject>();
    HasError = false;
    
    //Cleanup of PowerShell also due to commandment I
    ps.Commands.Clear();
    ps.Streams.ClearStreams();
    
    //We put the script into the PowerShell environment 
    //along with all commands and their parameters
    ps.AddScript(psCommand);
    
    //We are trying to execute our command
    outs = ExecutePS(ps);
    
    //The method returns true when executed correctly and false 
    //when some errors have occurred
    return !HasError;
}

/// <summary>
/// Method 2 cmdlet call where we can only give one command at a time
/// and its parameters are passed as Name/Value pairs,
/// where values can be of any type
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">Single command with no parameters</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <param name="parameters">A collection of parameter pairs
/// in the form Name/Value</param>
/// <returns>The method returns true when executed correctly
/// and false when some errors have occurred</returns>
public static bool RunPS(PowerShell ps, string psCommand,
       out Collection<PSObject> outs, params ParameterPair[] parameters)
{
    //Programmer's Commandment I: Remember to reset your variables
    outs = new Collection<PSObject>();
    HasError = false;        
    
    if (!psCommand.Contains(' '))
    {
        //Cleanup of PowerShell also due to commandment I
        ps.Commands.Clear();
        ps.Streams.ClearStreams();
        
        //We put a single command into the PowerShell environment
        ps.AddCommand(psCommand);
        
        //Now we enter the command parameters in the form of Name/Value pairs
        foreach (ParameterPair PP in parameters)
        {
            if (PP.Name.IsNullEmptyOrWhite())
            {
                LastException = new Exception("E1008:Parameter cannot be unnamed");
                return false;
            }
            if (PP.Value == null) ps.AddParameter(PP.Name);
            else ps.AddParameter(PP.Name, PP.Value);
        }
        
        //We are trying to execute our command
        outs = ExecutePS(ps);
    }
    //And here we have a special exception 
    //if we tried to apply the method not to a single command
    else LastException = new Exception
    ("E1007:Only one command with no parameters is allowed");
    
    //The method returns true when executed correctly 
    //and false when some errors have occurred
    return !HasError;
}

/// <summary>
/// Internal method in which we try to execute a script or command with parameters
/// This method does not need to return a fixed value 
/// that indicates whether or not the execution succeeded,
/// since the parent methods use the principal properties of the class set in it.
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <returns>A collection of objects that contains the feedback</returns>
private static Collection<PSObject> ExecutePS(PowerShell ps)
{
    Collection<PSObject> retVal = new Collection<PSObject>();
    
    //We are trying to execute our script
    try
    {
        retVal = ps.Invoke();
        
        // ps.HadErrors !!! NO!
        // The PowerShell environment has a special property that
        // indicates in the assumption whether errors have occurred
        // unfortunately, most often I have found that despite errors,
        // its value is false or vice versa,
        // in the absence of errors, it pointed to the truth.
        
        //Therefore, we check the fact that errors have occurred,
        //using the error counter in PowerShell.Streams
        if (ps.Streams.Error.Count > 0) //czy są błędy wykonania
        {
            //We create another general exception, but we do not raise it.
            LastException = new Exception("E0002:Errors were detected during execution");
            
            //And we write runtime errors to the LastErrors collection
            LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
        }
    }
    
    //We catch script execution errors and exceptions
    catch (Exception ex)
    {
        //And if they do, we create a new general exception but don't raise it
        LastException = new Exception("E0001:" + ex.Message);
    }
    
    //Returns a collection of results
    return retVal;
}

如何使用这样准备的类

首先,我们创建一个空的集合用于存放结果

Collection<PSObject> Results = new Collection<PSObject>();

然后我们需要打开单个 PowerShell 环境

PowerShell ps = PowerShell.Create();

然后尝试执行脚本

if (PS.Basic.RunPS(ps, "Get-Service | 
    Where-Object {$_.canpauseandcontinue -eq \"True\"}", out Results))
{ Interpreting the results… }
else
{ Interpretation of errors… }

或者带有参数的命令

if (PS.Basic.RunPS(ps, "Get-Service", out Results,
   new ParameterPair { Name = "Name", Value = "Spooler"}
))
{ Interpreting the results… }
else
{ Interpretation of errors… }

下一步是解释结果

在每种情况下,我们都会检查 Collection<PSObject> Results 集合。

例如,对于脚本 "Get-Service ...",该集合包含基本类型 (Results [0] .BaseObject.GetType()) ServiceController) 的对象,其中包含属性 "name"。我们可以通过 Results [0] .Properties ["name"]. Value 来读取它。

结果的解释归结为检查Results并检索我们感兴趣的相应属性的值。

或者解释错误

当尝试执行 PowerShell 命令时出错时,基类有几个属性和变量用于处理。

由于命令准备不当而导致的执行错误

这些错误可能源于在执行命令之前准备 PowerShell 时出错。例如,我们可能会忘记通过一个空的ps来打开环境。或者,我们可能会传递一些语法错误而不是正确的脚本/命令。

在这种情况下,在尝试调用ps.Invoke();时会发生一个异常,您可以在其描述中找到错误的根本原因。在基类中,LastException 变量被定义为消息 "E0001:" + ex.Message(即,在代码 "E0001" 前面加上异常描述)。
在错误解释阶段,您可以通过检查 "ErrorCode" 属性的值(PS.Basic.ErrorCode == 1)来检查是否发生了此类错误,然后使用 LastException.Message 中的描述进行更详细的错误处理。

命令执行错误

我们还可能在执行完全有效的命令或脚本时遇到错误。例如,当我们指定一个对象的 Identity,而该值在 PowerShell 环境的可视范围中不存在任何对象时。这时我们会收到 "not found" 或 "wrong name" 的错误,但仅仅尝试执行命令不会引起异常。

我们可以在 PSDataCollection<ErrorRecord> LastErrors 集合中找到此类错误。

所引入的类中的错误处理模型将导致 LastException 中出现一个新异常,其描述形式为:"E0002: Errors were detected during execution"。在通过 "ErrorCode" (PS.Basic.ErrorCode == 2) 检查后,我们可以从集合中读取后续错误,并根据 LastErrors [0] .Exception.Message 中的异常描述确定其原因。与 LastException 一样,现在需要在此基础上进行更详细的错误处理。

C# 下 PowerShell 处理类完整代码

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;

namespace PS
{
    public static class Basic
    {
        /// <summary>
        /// The last exception that occurred in the PS.Basic class
        /// </summary>
        public static Exception LastException = null;

        /// <summary>
        /// Collection of PowerShell runtime errors
        /// </summary>
        public static PSDataCollection<ErrorRecord> LastErrors = 
                                                    new PSDataCollection<ErrorRecord>();

        /// <summary>
        /// Auxiliary Property that helps to check if there was an error and 
        /// resets the error state
        /// </summary>
        public static bool HasError
        {
            get
            {
                return LastException != null;
            }
            set
            {
                if(!value)
                {
                    LastException = null;
                    LastErrors = new PSDataCollection<ErrorRecord>();
                }
            }
        }

        /// <summary>
        /// A helper Property to help you get the error code
        /// </summary>
        public static int ErrorCode
        {
            get
            {
                if (HasError) return int.Parse(LastException.Message.Substring(1, 4));
                return 0;
            }
        }

        /// <summary>
        /// Basic method of calling PowerShell a script where all commands 
        /// and their data must be presented as one line of text
        /// </summary>
        /// <param name="ps">PowerShell environment</param>
        /// <param name="psCommand">A single line of text containing commands 
        /// and their parameters (in text format)</param>
        /// <param name="outs">A collection of objects that contains the feedback</param>
        /// <returns>The method returns true when executed correctly 
        /// and false when some errors have occurred</returns>
        public static bool RunPS
               (PowerShell ps, string psCommand, out Collection<PSObject> outs)
        {
            //Programmer's Commandment I: Remember to reset your variables
            outs = new Collection<PSObject>();
            HasError = false;

            //Cleanup of PowerShell also due to commandment I
            ps.Commands.Clear();
            ps.Streams.ClearStreams();

            //We put the script into the PowerShell environment 
            //along with all commands and their parameters
            ps.AddScript(psCommand);

            //We are trying to execute our command
            outs = ExecutePS(ps);

            //The method returns true when executed correctly and false 
            //when some errors have occurred
            return !HasError;
        }

        /// <summary>
        /// Method 2 cmdlet call where we can only give one command 
        /// at a time and its parameters are passed as Name/Value pairs,
        /// where values can be of any type
        /// </summary>
        /// <param name="ps">PowerShell environment</param>
        /// <param name="psCommand">Single command with no parameters</param>
        /// <param name="outs">A collection of objects that contains the feedback</param>
        /// <param name="parameters">A collection of parameter pairs 
        /// in the form Name/Value</param>
        /// <returns>The method returns true when executed correctly 
        /// and false when some errors have occurred</returns>
        public static bool RunPS(PowerShell ps, string psCommand, 
               out Collection<PSObject> outs, params ParameterPair[] parameters)
        {
            //Programmer's Commandment I: Remember to reset your variables
            outs = new Collection<PSObject>();
            HasError = false;
           
            if (!psCommand.Contains(' '))
            {
                //Cleanup of PowerShell also due to commandment I
                ps.Commands.Clear();
                ps.Streams.ClearStreams();

                //We put a single command into the PowerShell environment
                ps.AddCommand(psCommand);

                //Now we enter the command parameters in the form of Name/Value pairs
                foreach (ParameterPair PP in parameters)
                {
                    if (PP.Name.IsNullEmptyOrWhite())
                    {
                        LastException = new Exception("E1008:Parameter cannot be unnamed");
                        return false;
                    }

                    if (PP.Value == null) ps.AddParameter(PP.Name);
                    else ps.AddParameter(PP.Name, PP.Value);
                }

                //We are trying to execute our command
                outs = ExecutePS(ps);
            }
            //And here we have a special exception if we tried 
            //to apply the method not to a single command
            else LastException = new Exception("E1007:Only one command 
                                                with no parameters is allowed");

            //The method returns true when executed correctly and false 
            //when some errors have occurred
            return !HasError;
        }

        /// <summary>
        /// Internal method in which we try to execute a script or command with parameters
        /// This method does not need to return a fixed value that indicates 
        /// whether or not the execution succeeded,
        /// since the parent methods use the principal properties of the class set in it.
        /// </summary>
        /// <param name="ps">PowerShell environment</param>
        /// <returns>A collection of objects that contains the feedback</returns>
        private static Collection<PSObject> ExecutePS(PowerShell ps)
        {
            Collection<PSObject> retVal = new Collection<PSObject>();

            //We are trying to execute our script
            try
            {
                retVal = ps.Invoke();

                // ps.HadErrors !!! NO!
                // The PowerShell environment has a special property 
                // that indicates in the assumption whether errors have occurred
                // unfortunately, most often, I have found that despite errors 
                // its value is false or vice versa,
                // in the absence of errors, it pointed to the truth.

                // Therefore, we check the fact that errors have occurred, 
                // using the error counter in PowerShell.Streams
                if (ps.Streams.Error.Count > 0) //czy są błędy wykonania
                {
                    //We create another general exception, but we do not raise it.
                    LastException = new Exception
                                    ("E0002:Errors were detected during execution");

                    //And we write runtime errors to the LastErrors collection
                    LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
                }
            }
            //We catch script execution errors and exceptions
            catch (Exception ex)
            {
                //And if they do, we create a new general exception but don't raise it
                LastException = new Exception("E0001:" + ex.Message);
            }

            //Returns a collection of results
            return retVal;
        }
    }

    /// <summary>
    /// Class defining the PowerShell parameter in the form Name/Value.
    /// it can be replaced with any convenient dictionary class
    /// </summary>
    public class ParameterPair
    {
        public string Name { get; set; } = string.Empty;

        public object Value { get; set; } = null;
    }
}

历史

  • 2021 年 11 月 26 日:文章初稿
© . All rights reserved.