2

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

2 Answers 2

3

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
Sign up to request clarification or add additional context in comments.

7 Comments

The exit keyword in the script is not going to affect $LASTEXITCODE in the hosting runspace - no process was started, no process exited = no exit code
@MathiasR.Jessen working fine for me: i.imgur.com/4lOw06Q.png
@MathiasR.Jessen nevermind, i see why it is working, i've clarified that in the answer. as long as he uses the path as argument it should be fine
@BrandonE $LASTEXITCODE is only populated when you call script file (see the documentation). if you invoke powershell code instead of calling a ps1 it will never be populated
@SantiagoSquarzon sorry I wasn't clear: yes that's what I meant, invoking runspace-sourced scripts (as opposed to script files) will never result in exit code
|
1
  • 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.

5 Comments

AddScript and AddCommand both allow direct invocation of ps1 script and both will allow retrieval thru .SessionStateProxy.GetVariable(..).
There was an execution policy error and I had to add code to change it before calling my script in the code. The execution policy is set to unrestricted on all scopes on my machine, so I changed it in code.
@SantiagoSquarzon: I had assumed that using .AddScript() behaves like the -Command CLI parameter and translates any nonzero exit code into 1, but I now see that's not the case. However, there are still good reasons to prefer .AddCommand(): See the footnote I've just added to the answer.
@BrandonE: Please see my update re setting the execution policy. Note that if the effective execution policy is Unrestricted, you shouldn't need to set it in your PowerShell SDK code, but I agree that it's good practice.
@SantiagoSquarzon, 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. (I've updated the footnote accordingly.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.