BrandonE
BrandonE

Reputation: 21

Getting an exit code from powershell file invoked in C#

I am trying to have a powershell script called from a C# program give me the exit code of the powershell script. I have attempted it a few ways with no success. The PSObject from the invoke call always has a count of 0.

I have been using this C# code and the PSObject always has a count of 0

using (PowerShell ps = PowerShell.Create())
{
    ps.AddScript(File.ReadAllText(buildScript.ps1));       
    var psResults = ps.Invoke();

    foreach (PSObject psObj in psResults)
    {
        var result = psObj.ToString());
    }

    ps.Dispose;
}

I trimmed down the .PS1 file for testing and it looks like this.

Start-Sleep -Seconds 30

Write-Host "Exit code is : 25"

exit 25

Upvotes: 2

Views: 107

Answers (2)

mklement0
mklement0

Reputation: 437052

  • Use .AddCommand() rather than .AddScript(); .AddCommand() allows direct invocation of *.ps1 files by file path, and reflects their exit code in the automatic $LASTEXITCODE variable.[1]

    • However - on Windows only - invoking a script file makes the call subject to PowerShell's execution policy, so it's best to explicitly allow script execution as part of your application, i.e. to enact a process-specific override of the execution policy that may be in effect.[2]
  • After execution, you can invoke .Runspace.SessionStateProxy.GetVariable("LASTEXITCODE") on your System.Management.Automation.PowerShell instance to obtain the value of this variable.

Therefore:

// Create an initial default session state.
var iss = System.Management.Automation.Runspaces.InitialSessionState.CreateDefault2();

// Windows only: 
// Set the session state's script-file execution policy 
// (for the current session (process) only).
iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Bypass;

using (PowerShell ps = PowerShell.Create(iss))
{
    ps.AddCommand(@"/path/to/your/buildScript.ps1");

    // Invoke synchronously and process the success output.
    // To retrieve output from other streams, use ps.Streams later.
    foreach (PSObject psObj in ps.Invoke())
    {
      Console.WriteLine(psObj.ToString());
    }

    // Obtain the exit code.
    int exitCode = (int)ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE");
    Console.WriteLine($"Exit code: {exitCode}");
}

[1] The reasons for preferring .AddCommand() over .AddScript() are:
(a) You can use script file paths that contain spaces and other metacharacters as-is (whereas .AddScript() would require use of embedded quoting and &, the call operator)
(b) You can pass richly typed parameter values via .AddArgument() / .AddParameter() / .AddParameters() (whereas .AddScript() would require you to "bake" the parameter values as string literals into the single string argument passed to it).
In short: .AddScript() is for executing arbitrary PowerShell source code, whereas .AddCommand() is for executing a single command by name or path, such as a *.ps1 file. It is important to know this distinction, because AddScript() will only behave like .AddCommand() in the simplest of cases: with a space-less *.ps1 path that is also free of other metacharacters, to which no arguments need be passed.
See this answer for more information.

[2] Note, however, that if your machine's / user account's execution policy is controlled by GPOs, a process-level override will not work; see this answer for details.

Upvotes: 0

Santiago Squarzon
Santiago Squarzon

Reputation: 59772

Normally you shouldn't rely on exit codes from PowerShell script, but to answer your question, with your current implementation you can query $LASTEXITCODE automatic variable before disposing your PowerShell instance, however, for this to work you will need to pass-in the script path as .AddScript(...) argument, instead of reading the script content via File.ReadAllText(...). Preferably you should use an absolute path but relative might work

You should also handle the Write-Host output in your code, that output you can find it in ps.Streams.Information, it will not be output from .Invoke().

Alternatively, you could subscribe to DataAdding event:

ps.AddScript(@".\buildScript.ps1");
ps.Streams.Information.DataAdding += (s, e) =>
{
    InformationRecord info = (InformationRecord)e.ItemAdded;
    Console.WriteLine(info.MessageData);
};

In summary you can do:

int exitCode = 0;
using (PowerShell ps = PowerShell.Create())
{
    ps.AddScript(@".\buildScript.ps1");
    var psResults = ps.Invoke();

    foreach (PSObject psObj in psResults)
    {
        var result = psObj.ToString();
    }

    // if not using the event driven approach
    if (ps.Streams is { Information.Count: > 0 })
    {
        foreach (InformationRecord information in ps.Streams.Information)
        {
            // handle information output here...
        }
    }

    if (ps.HadErrors)
    {
        ps.Commands.Clear();
        exitCode = ps
            .AddScript("$LASTEXITCODE")
            .Invoke<int>()
            .FirstOrDefault();
    }
}

// Do something with exitCode

Upvotes: 2

Related Questions