Skip to content

Conversation

@rkeithhill
Copy link
Collaborator

@rkeithhill rkeithhill commented Jul 11, 2021

PR Summary

Implements a terminating error when a native executable returns a non-zero exit code similar to set -e on Linux and macOS.

PR Context

Implement RFC 277

PR Checklist

This PR does impact the UseDeclaredVarsMoreThanAssignments PSSA rule. PSNativeCommandUseErrorActionPreference needs to be excluded from this rule.

@ghost ghost assigned daxian-dbw Jul 11, 2021
Warning = 13,
Information = 14,
Confirm = 15,
NativeCommandThrow = 16,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this preference needs to be defined here.

@rkeithhill
Copy link
Collaborator Author

There are internal APIs to get/set pref vars in ExecutionContext.cs, MshCommandRuntime.cs and PowerShell.cs (public api). Not sure if I need to define this preference variable in those files.

@rjmholt
Copy link
Collaborator

rjmholt commented Jul 14, 2021

Assigning myself since I've been tasked with taking this work on, but since you've already started @rkeithhill maybe I can support you here instead

@rkeithhill
Copy link
Collaborator Author

rkeithhill commented Jul 14, 2021

@rjmholt That would be great! I'd love to have your input on this. I need to update this PR as I started it off from my memory of the RFC and now I see that (as @iSazonov points out), I need to be checking the value of $ErrorActionPreference. I also see in the PR that there are specifics regarding the generated ErrorRecord that I need to snap to. I'll push an update to this PR tonight to cover those issues. I'll try to add the implementation for NativeCommandException but may have some questions about where to put that source.

<value>Dictates what type of prompt should be displayed for the current nesting level</value>
</data>
<data name="NativeCommandUseErrorActionPreferenceDescription" xml:space="preserve">
<value>If true, $ErrorActionPreference applies to native executable non-zero exit codes</value>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use some help with this wording.

Copy link
Collaborator

@rjmholt rjmholt Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you've got is pretty good -- hard to word. One alternative:

If true, $ErrorActionPreference will apply to native executables, so that non-zero exit codes will generate cmdlet-style errors governed by error action settings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rjmholt Can you give an example of where the -ErrorAction parameter would come into play? I've tried iex "whoami -xyzzy" -ea Stop and icm { whoami -xyzzy } -ea Stop. If I check the value of $ErrorActionPreference with the iex string or the icm scriptbock, the -ErrorAction parameter does not impact the value or pref variable.

PS> icm {
     "`$ErrorActionPreference is $ErrorActionPreference"
     $PSNativeCommandUseErrorActionPreference=$true
     whoami -xyzzy
} -ErrorAction Stop
$ErrorActionPreference is Continue
ERROR: Invalid argument/option - '-xyzzy'.
Type "WHOAMI /?" for usage.

NativeCommandException: Program "whoami.exe" ended with non-zero exit code 1.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you're right, there's no -ErrorAction parameter here. Just the preference variable

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edited my suggestion above to correct it

@rkeithhill rkeithhill force-pushed the rkeithhill/rfc-277-throw-on-native-nonzero-exit-code branch from cf30d87 to fc7037e Compare July 15, 2021 05:38
errorId: "NativeCommandThrow",
errorCategory: ErrorCategory.NotSpecified,
targetObject: this.NativeCommandName);
this.commandRuntime._WriteErrorSkipAllowCheck(errorRecord, isNativeError: true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have isNativeError flag why not move the new code to _WriteErrorSkipAllowCheck()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this was kind of a complicated question to answer, but I think it's worth us taking a look

@rjmholt rjmholt marked this pull request as draft July 15, 2021 22:31
@rkeithhill
Copy link
Collaborator Author

I implemented NativeCommandException. The result looks like this:

image

<data name="PauseHelpMessage" xml:space="preserve">
<value>Pause the current pipeline and return to the command prompt. Type "{0}" to resume the pipeline.</value>
</data>
<data name="ProgramFailedToComplete" xml:space="preserve">
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a ProgramFailedToExecute resource string in ParserStrings.resx but that doesn't seem to be quite right for this runtime error. Is CommandBaseStrings.resx the correct file for this resource sting?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems it is not used at all.

Copy link
Collaborator Author

@rkeithhill rkeithhill Jul 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProgramFailedToExecute is used in the same NativeCommandProcessor.cs file during the creation of another exception ApplicationFailedException that I thiink covers the general case of native command execution failure. This does make me wonder if someone looking at the code in the future is going to wonder why there's both an ApplicationFailedException and a NativeCommandException.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is still used ParserStrings.CantActivateDocumentInPowerShellCore with InterpreterError.NewInterpreterException.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does make me wonder if someone looking at the code in the future is going to wonder why there's both an ApplicationFailedException and a NativeCommandException.

Worth adding a comment in the elements added in this PR to note that despite those things pre-existing, we added new ones to be specifically for the native error action preference feature to ensure it didn't accidentally clobber any existing concepts.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this in my PR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to open new issue in Docs repo to explain ApplicationFailedException (PowerShell can not run normally an non-backgroud application) vs NativeCommandException (PowerShell could be able to run non-backgraund application but throw due to non zero return code).

# when $PSNativeCommandUseErrorActionPreference is $true

Describe 'Native command error handling tests' -Tags 'CI' {
Context 'PSNativeCommandUseErrorActionPreference is $true' {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd combine tests for true and false (and maybe for $ErrorActionPreference values) and use TestCases.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only things I would add here are:

  • Testing on the properties of the exception that we're supposed to implement, if they're available beyond the error message
  • A test with a try/catch
  • A test with a trap

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding these in my PR

Allow native errors from exit codes to throw properly
@rjmholt rjmholt changed the title WIP: Initial impl of RFC-277 - native exe throws on non-zero exit code Initial impl of RFC-277 - native exe throws on non-zero exit code Aug 5, 2021
@rkeithhill
Copy link
Collaborator Author

rkeithhill commented Aug 5, 2021

@rjmholt The current build checks seem to be stuck. Is there a way I restart them short of pushing a bogus commit? Nevermind - resolved the merge conflict and that has restarted the build.

@ghost ghost removed the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Aug 5, 2021
@rjmholt
Copy link
Collaborator

rjmholt commented Aug 5, 2021

Test failures are very weird...

/azp run

@rkeithhill
Copy link
Collaborator Author

rkeithhill commented Aug 5, 2021

@rjmholt There is a test failure in output rendering tests which I don't believe this PR should impact:

2021-08-05T17:44:15.0467419Z     Context NormalView tests
2021-08-05T17:44:15.0498743Z +
2021-08-05T17:44:15.0512900Z 
2021-08-05T17:44:15.0527114Z     Context DetailedView tests
2021-08-05T17:44:15.0555317Z +
2021-08-05T17:44:15.0570773Z 
2021-08-05T17:44:15.0603255Z 
2021-08-05T17:44:15.0611669Z   Describing OutputRendering tests
2021-08-05T17:44:15.0627692Z     [-] Error occurred in Describe block
2021-08-05T17:44:15.0649066Z       PSArgumentException: The key 'It:Skip' has already been added to the dictionary.
2021-08-05T17:44:15.0663568Z       MethodInvocationException: Exception calling "Add" with "2" argument(s): "The key 'It:Skip' has already been added to the dictionary."
2021-08-05T17:44:15.0704752Z       at <ScriptBlock>, D:\a\1\s\test\powershell\engine\Formatting\OutputRendering.Tests.ps1: line 6
2021-08-05T17:44:15.0717916Z       at Invoke-Blocks, D:\a\1\s\src\powershell-win-core\bin\Release\net6.0\win7-x64\publish\Modules\Pester\4.10.1\Functions\SetupTeardown.ps1: line 135
2021-08-05T17:44:15.0731656Z       at Invoke-TestGroupSetupBlocks, D:\a\1\s\src\powershell-win-core\bin\Release\net6.0\win7-x64\publish\Modules\Pester\4.10.1\Functions\SetupTeardown.ps1: line 121
2021-08-05T17:44:15.0746462Z       at DescribeImpl, D:\a\1\s\src\powershell-win-core\bin\Release\net6.0\win7-x64\publish\Modules\Pester\4.10.1\Functions\Describe.ps1: line 209

It's balking on this last line here:

Describe 'OutputRendering tests' {
    BeforeAll {
        $PSDefaultParameterValues.Add('It:Skip', (-not $EnabledExperimentalFeatures.Contains('PSAnsiRendering')))

Anything wrong with that? It's a bit different than how you skipped the tests for this PR when the exp feature is not enabled.

Hmm, it seems that the following approach to skipping a test is way more prevalent in the tests vs what the Output Rendering test does:

Describe "Get-HotFix Tests" -Tag CI {
    BeforeAll {
        $originalDefaultParameterValues = $PSDefaultParameterValues.Clone()

        $skip = $false
        if (!$IsWindows) {
            $skip = $true
        }
        else {
            # skip the tests if there are no hotfixes returned
            $qfe = Get-CimInstance Win32_QuickFixEngineering
            if ($qfe.Count -eq 0) {
                $skip = $true
            }
        }

        $PSDefaultParameterValues["it:skip"] = $skip
    }

    AfterAll {
        $global:PSDefaultParameterValues = $originalDefaultParameterValues
    }

/* WarningPreference */ typeof(ActionPreference),
/* InformationPreference */ typeof(ActionPreference),
/* ConfirmPreference */ typeof(ConfirmImpact),
/* PSNativeCommandUseErrorActionPreference */ typeof(SwitchParameter),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the same type as the builtin variable listing in InitialSessionState.cs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* PSNativeCommandUseErrorActionPreference */ typeof(SwitchParameter),
/* PSNativeCommandUseErrorActionPreference */ typeof(bool),

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this in my PR

@rkeithhill
Copy link
Collaborator Author

rkeithhill commented Aug 5, 2021

@rjmholt Woohoo! All checks pass! That said, we might want to think about whether the tests I've added are adequate.

@theJasonHelmick
Copy link
Collaborator

@rkeithhill - Thank you for all the hard work. I just spoke with @rjmholt and he thinking he might open a separate pr to add a couple of tests.

@rkeithhill
Copy link
Collaborator Author

Sounds good to me. I wouldn't be able to do anything until later this evening and even then I don't have a ton of time before I'm out of vacation next week.

@rjmholt
Copy link
Collaborator

rjmholt commented Aug 6, 2021

@rkeithhill see rkeithhill#21

@rkeithhill
Copy link
Collaborator Author

Merged.

@rkeithhill
Copy link
Collaborator Author

Markdown tests failed. Le sigh.

image

This is a downside of relying on websites during the build.

@rjmholt
Copy link
Collaborator

rjmholt commented Aug 6, 2021

/azp run

@rjmholt rjmholt marked this pull request as ready for review August 6, 2021 21:39
@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

@rjmholt rjmholt enabled auto-merge (squash) August 6, 2021 21:42
/// <see cref="System.Management.Automation.ActionPreferenceStopException"/>,
/// </remarks>
internal void _WriteErrorSkipAllowCheck(ErrorRecord errorRecord, ActionPreference? actionPreference = null, bool isNativeError = false)
internal void _WriteErrorSkipAllowCheck(ErrorRecord errorRecord, ActionPreference? actionPreference = null, bool isNativeStdErrCall = false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change is not related to the PR. Please revert.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it's directly related to it. We now call this API from multiple native calls, but that parameter was only added for a specific scenario (the stderr scenario).

The actual issue is that we'll likely be removing it soon as the experimental feature that disables it is stabilised.

Comment on lines +885 to +888
this.commandRuntime.PipelineProcessor.ExecutionFailed = true;

bool nativeCommandUseErrorActionPreference = ExperimentalFeature.IsEnabled(ExperimentalFeature.PSNativeCommandErrorActionPreferenceFeatureName)
&& this.Command.Context.GetBooleanPreference(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the "this" prefix? Can we cleanup all in the PR (new and changed code only)?

InitialSessionState.DefaultPSNativeCommandUseErrorActionPreference,
out _);

if (!nativeCommandUseErrorActionPreference)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer roll out the variable here and remove the help variable. This will simplify switching from experimental (less code changes).


if (errorActionPref != ActionPreference.Ignore)
{
const string errorId = "ProgramFailedToComplete";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code convention for consts.

Suggested change
const string errorId = "ProgramFailedToComplete";
const string ErrorId = "ProgramFailedToComplete";

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we discuss that for local consts camelCase is preferred?


internal static readonly VariablePath InformationPreferenceVarPath = new VariablePath(InformationPreference);

internal const string PSNativeCommandUseErrorActionPreference = nameof(PSNativeCommandUseErrorActionPreference);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can confuse a bit

Suggested change
internal const string PSNativeCommandUseErrorActionPreference = nameof(PSNativeCommandUseErrorActionPreference);
internal const string PSNativeCommandUseErrorActionPreferenceName = nameof(PSNativeCommandUseErrorActionPreference);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the other variables are named this way though, so convention has established this

<data name="PauseHelpMessage" xml:space="preserve">
<value>Pause the current pipeline and return to the command prompt. Type "{0}" to resume the pipeline.</value>
</data>
<data name="ProgramFailedToComplete" xml:space="preserve">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to open new issue in Docs repo to explain ApplicationFailedException (PowerShell can not run normally an non-backgroud application) vs NativeCommandException (PowerShell could be able to run non-backgraund application but throw due to non zero return code).

however that is used for ApplicationFailedExceptions thrown when the NativeCommandProcessor fails in an unexpected way.
In this case, we have a more specific error for the native command scenario, so the two are not conflated.
-->
<value>Program "{0}" ended with non-zero exit code {1}.</value>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<value>Program "{0}" ended with non-zero exit code {1}.</value>
<value>Program "{0}" ended with non-zero exit code: {1}.</value>

<value>Dictates what type of prompt should be displayed for the current nesting level</value>
</data>
<data name="PSNativeCommandUseErrorActionPreferenceDescription" xml:space="preserve">
<value>If true, $ErrorActionPreference applies to native executables, so that non-zero exit codes will generate cmdlet-style errors governed by error action settings</value>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably need:

Suggested change
<value>If true, $ErrorActionPreference applies to native executables, so that non-zero exit codes will generate cmdlet-style errors governed by error action settings</value>
<value>If true, $ErrorActionPreference applies to native executables, so that non-zero exit codes will generate cmdlet-style errors governed by error action settings.</value>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the other messages here end with .

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are both cases. We need use right. But I don't know what case is right :-)

It 'Non-zero exit code throws teminating error for $ErrorActionPreference = ''Stop''' {
$ErrorActionPreference = 'Stop'

{ testexe -returncode 1 } | Should -Throw -ErrorId 'ProgramFailedToComplete'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test shows the name is not good - the utility really executed and completed but return non-zero return code.
I think we need better name like NativeCommandReturnNonZeroReturnCode.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a fair point. @theJasonHelmick, @rkeithhill we should consider renaming this error ID

Comment on lines +80 to +85
if ($ErrorActionPref -eq 'Stop') {
{ testexe -returncode 0 } | Should -Not -Throw
}
else {
testexe -returncode 0 > $null
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why do we need separate branch for Stop and why do we need to write to null?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it looks like we should just merge these cases. Ideally we should have some test on the output and last exit code.

@rjmholt rjmholt disabled auto-merge August 9, 2021 19:08
@rjmholt
Copy link
Collaborator

rjmholt commented Aug 9, 2021

Opened rkeithhill#22 (@rkeithhill)

@rjmholt
Copy link
Collaborator

rjmholt commented Aug 9, 2021

Closing in favour of #15897 because I needed to rebase the branch and can't edit this one (@rkeithhill you can turn that on in future if you'd like)

@rjmholt rjmholt closed this Aug 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants