Home > database >  Unable to retrieve results of WSFC FailoverCluster Powershell Module executed via C#
Unable to retrieve results of WSFC FailoverCluster Powershell Module executed via C#

Time:10-08

Since there is no integrated methods or APIs to access WindowsServerFailoverCluster features, I'm attempting to launch some PowerShell cmdlets to ascertain WSFC's status and return it to C# (healthcheck purposes)

This works as a smoke test and contains the console output of PowerShell with my object in it, but unfortunately as a flat string.

Option 1:

System.Diagnostics.ProcessStartInfo processInfo = new System.Diagnostics.ProcessStartInfo();
processInfo.FileName = @"powershell.exe";
processInfo.Arguments = @"& {Get-ClusterResource -Name '"  _wsfcResourceName  "'}";
processInfo.Verb = "runas";
processInfo.RedirectStandardError = true;
processInfo.RedirectStandardOutput = true;
processInfo.UseShellExecute = false;
processInfo.CreateNoWindow = true;

System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo = processInfo;
process.Start();

var stdout = process.StandardOutput.ReadToEnd();
var stderror = process.StandardError.ReadToEnd();

As soon as I try something like this:

System.Management.Automation.Runspaces.InitialSessionState initialSessionState = System.Management.Automation.Runspaces.InitialSessionState.CreateDefault();
initialSessionState.ImportPSModule(new[] { "FailoverClusters"});
initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
using System.Management.Automation.Runspaces.Runspace runspace = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace(initialSessionState);
runspace.Open();
using (PowerShell PowerShellInst = PowerShell.Create(runspace))
{
    PowerShellInst.AddCommand($@"Get-ClusterResource");
    PowerShellInst.AddParameter("Name", _wsfcResourceName);
    
    //Same result using these
    //PowerShellInst.AddScript($@"Get-ClusterResource -Name '{_wsfcResourceName}'");
    //PowerShellInst.AddScript($@"Import-Module FailoverClusters; Get-ClusterResource -Name '{_wsfcResourceName}'");

    Collection<PSObject> PSOutput = PowerShellInst.Invoke();
    var rsrcResults = PSOutput.ToList();
... etc

... The app throws an exception up from PowerShell

The 'Get-ClusterResource' command was found in the module 'FailoverClusters', but the module could not be loaded. Fore more information, run 'Import-Module FailoverClusters'.

Which confuses me when Option 1 works (this is running from an IIS Web App).

Things I have tried:

  1. Passing only "Import-Module FailoverClusters -Force -Verbose" to see if it errors. It doesn't.
  2. Specifying initialSessionState.ImportPSModule(new[] { "FailoverClusters"}); in Runspaces.InitialSessionState
  3. Specifying 32-bit in my class library handling this method
  4. ... unteen different ways of writing it.

Could anyone offer some advice?

Update to @Cpt.Whale's answer:

Code changed to:

PowerShellInst.Commands.AddCommand("Import-Module").AddArgument(@"FailoverClusters");
                        PowerShellInst.Commands.AddParameter("SkipEditionCheck");
                        PowerShellInst.Commands.AddParameter("Force");
                        PowerShellInst.Commands.AddParameter("Verbose");

When I now call: Collection<PSObject> PSOutput = PowerShellInst.Invoke();

I get an exception I'm seeing for the first time:

System.Management.Automation.CmdletInvocationException: Could not load type 'System.Diagnostics.Eventing.EventDescriptor' from assembly 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.
---> System.TypeLoadException: Could not load type 'System.Diagnostics.Eventing.EventDescriptor' from assembly 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.
 at Microsoft.FailoverClusters.UI.Common.TraceWriterEtw.TraceCmdletEnter(String cmdletName, String invocation, String parameters, UInt32 sequence, String source, String tag)
 at Microsoft.FailoverClusters.UI.Common.TraceCmdlet.Starting()
 at Microsoft.FailoverClusters.UI.Common.TraceSampleBase.Start() 
at Microsoft.FailoverClusters.UI.Common.ClusterLog.StartTraceCmdlet(TraceSource source, String cmdletName, String invocation, String parameters) at Microsoft.FailoverClusters.PowerShell.FCCmdlet.ProcessRecord()
 at Microsoft.FailoverClusters.PowerShell.ClusterPipeCmdlet.ProcessRecord() at System.Management.Automation.CommandProcessor.ProcessRecord() --- End of inner exception stack trace --- at System.Management.Automation.Runspaces.PipelineBase.Invoke(IEnumerable input) at System.Management.Automation.PowerShell.Worker.ConstructPipelineAndDoWork(Runspace rs, Boolean performSyncInvoke)
 at System.Management.Automation.PowerShell.Worker.CreateRunspaceIfNeededAndDoWork(Runspace rsToUse, Boolean isSync) at System.Management.Automation.PowerShell.CoreInvokeHelper[TInput,TOutput](PSDataCollection`1 input, PSDataCollection`1 output, PSInvocationSettings settings)
 at System.Management.Automation.PowerShell.CoreInvoke[TInput,TOutput](PSDataCollection`1 input, PSDataCollection`1 output, PSInvocationSettings settings) at System.Management.Automation.PowerShell.Invoke(IEnumerable input, PSInvocationSettings settings)

Not sure what it means yet...

Edit #2

"Import-Module errors out when it attempts to load the module 
with an error message stating that Import-Module was not able to load the 
.NET type System.Diagnostics.Eventing.EventDescriptor. Since the EventDescriptor 
type is not available in .NET Core, you cannot use the ServerManager module natively. 
The only alternatives are for the Server Manager team to update their code, 
or for you to run the  cmdlet in Windows PowerShell (either explicitly or via the compatibility mechanism)" 

I believe this explains why "Option 1" worked. Which puts me back where I started. Is there anyway to make some kind of object from process.StandardOutput.ReadToEnd()?

CodePudding user response:

The only way I could figure out how to accomplish what I want is by relying on the fact that my PSOBject only had a small amount objects and I made sure I could safely assume they would have no spaces in them.

I then call upon StartProcess() to launch powershell with my cmdlet as an argument BUT PIPING IT TO "Format-List -HideTableHeaders", and then parsing it using space as a separator.

public async Task<string> GetClusterResource()
{
  try
   {
    System.Diagnostics.ProcessStartInfo processInfo = new System.Diagnostics.ProcessStartInfo();
    processInfo.FileName = @"powershell.exe";

    processInfo.Arguments = @"& {Get-ClusterResource -Name '"   _wsfcResourceName   "' | Select Name, State, OwnerGroup | ft -HideTableHeaders}";
                    
    processInfo.RedirectStandardError = true;
    processInfo.RedirectStandardOutput = true;
    processInfo.UseShellExecute = false;
    processInfo.CreateNoWindow = true;


    System.Diagnostics.Process process = new System.Diagnostics.Process();
    process.StartInfo = processInfo;
    process.Start();

    var results = await process.StandardOutput.ReadToEndAsync();
    var errors = await process.StandardError.ReadToEndAsync();

    return results;
   }
   catch (Exception ex)
   {
     throw;
   }
}
var CoreClusterResults = await GetClusterResource();

CoreClusterResults = CleanStringOfNewlineStrings(CoreClusterResults); //this removes any occurences of /r/n, /r or /n

var ParamsSplit = CoreClusterResults.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);

if (ParamsSplit.Any())
{
   var clusterParamName = ParamsSplit[0];
   var clusterParamValue = ParamsSplit[1];

     ...
}

Thanks to @Cpt for the steer

  • Related