Make the native command error handling optionally honor ErrorActionPreference (#15897)

This commit is contained in:
Robert Holt 2021-08-24 15:27:22 -07:00 committed by GitHub
parent 56d22bc386
commit a4d14576b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 526 additions and 173 deletions

View file

@ -22,6 +22,7 @@ namespace System.Management.Automation
internal const string EngineSource = "PSEngine";
internal const string PSNativeCommandArgumentPassingFeatureName = "PSNativeCommandArgumentPassing";
internal const string PSNativeCommandErrorActionPreferenceFeatureName = "PSNativeCommandErrorActionPreference";
#endregion
@ -122,6 +123,9 @@ namespace System.Management.Automation
new ExperimentalFeature(
name: "PSAnsiRenderingFileInfo",
description: "Enable coloring for FileInfo objects"),
new ExperimentalFeature(
name: PSNativeCommandErrorActionPreferenceFeatureName,
description: "Native commands with non-zero exit codes issue errors according to $ErrorActionPreference when $PSNativeCommandUseErrorActionPreference is $true"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

View file

@ -4471,154 +4471,173 @@ end {
internal const ActionPreference DefaultInformationPreference = ActionPreference.SilentlyContinue;
internal const ErrorView DefaultErrorView = ErrorView.ConciseView;
internal const bool DefaultPSNativeCommandUseErrorActionPreference = false;
internal const bool DefaultWhatIfPreference = false;
internal const ConfirmImpact DefaultConfirmPreference = ConfirmImpact.High;
internal static readonly SessionStateVariableEntry[] BuiltInVariables = new SessionStateVariableEntry[]
static InitialSessionState()
{
// Engine variables that should be precreated before running profile
// Bug fix for Win7:2202228 Engine halts if initial command fulls up variable table
// Anytime a new variable that the engine depends on to run is added, this table
// must be updated...
new SessionStateVariableEntry(SpecialVariables.LastToken, null, string.Empty),
new SessionStateVariableEntry(SpecialVariables.FirstToken, null, string.Empty),
new SessionStateVariableEntry(SpecialVariables.StackTrace, null, string.Empty),
var builtinVariables = new List<SessionStateVariableEntry>()
{
// Engine variables that should be precreated before running profile
// Bug fix for Win7:2202228 Engine halts if initial command fulls up variable table
// Anytime a new variable that the engine depends on to run is added, this table
// must be updated...
new SessionStateVariableEntry(SpecialVariables.LastToken, null, string.Empty),
new SessionStateVariableEntry(SpecialVariables.FirstToken, null, string.Empty),
new SessionStateVariableEntry(SpecialVariables.StackTrace, null, string.Empty),
// Variable which controls the output rendering
new SessionStateVariableEntry(
SpecialVariables.PSStyle,
PSStyle.Instance,
RunspaceInit.PSStyleDescription,
ScopedItemOptions.None),
// Variable which controls the output rendering
new SessionStateVariableEntry(
SpecialVariables.PSStyle,
PSStyle.Instance,
RunspaceInit.PSStyleDescription,
ScopedItemOptions.None),
// Variable which controls the encoding for piping data to a NativeCommand
new SessionStateVariableEntry(
SpecialVariables.OutputEncoding,
Utils.utf8NoBom,
RunspaceInit.OutputEncodingDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(System.Text.Encoding))),
// Variable which controls the encoding for piping data to a NativeCommand
new SessionStateVariableEntry(
SpecialVariables.OutputEncoding,
Utils.utf8NoBom,
RunspaceInit.OutputEncodingDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(System.Text.Encoding))),
// Preferences
//
// NTRAID#Windows Out Of Band Releases-931461-2006/03/13
// ArgumentTypeConverterAttribute is applied to these variables,
// but this only reaches the global variable. If these are
// redefined in script scope etc, the type conversion
// is not applicable.
//
// Variables typed to ActionPreference
new SessionStateVariableEntry(
SpecialVariables.ConfirmPreference,
DefaultConfirmPreference,
RunspaceInit.ConfirmPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ConfirmImpact))),
new SessionStateVariableEntry(
SpecialVariables.DebugPreference,
DefaultDebugPreference,
RunspaceInit.DebugPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.ErrorActionPreference,
DefaultErrorActionPreference,
RunspaceInit.ErrorActionPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.ProgressPreference,
DefaultProgressPreference,
RunspaceInit.ProgressPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.VerbosePreference,
DefaultVerbosePreference,
RunspaceInit.VerbosePreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.WarningPreference,
DefaultWarningPreference,
RunspaceInit.WarningPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.InformationPreference,
DefaultInformationPreference,
RunspaceInit.InformationPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.ErrorView,
DefaultErrorView,
RunspaceInit.ErrorViewDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ErrorView))),
new SessionStateVariableEntry(
SpecialVariables.NestedPromptLevel,
0,
RunspaceInit.NestedPromptLevelDescription),
new SessionStateVariableEntry(
SpecialVariables.WhatIfPreference,
DefaultWhatIfPreference,
RunspaceInit.WhatIfPreferenceDescription),
new SessionStateVariableEntry(
FormatEnumerationLimit,
DefaultFormatEnumerationLimit,
RunspaceInit.FormatEnumerationLimitDescription),
// Preferences
//
// NTRAID#Windows Out Of Band Releases-931461-2006/03/13
// ArgumentTypeConverterAttribute is applied to these variables,
// but this only reaches the global variable. If these are
// redefined in script scope etc, the type conversion
// is not applicable.
//
// Variables typed to ActionPreference
new SessionStateVariableEntry(
SpecialVariables.ConfirmPreference,
DefaultConfirmPreference,
RunspaceInit.ConfirmPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ConfirmImpact))),
new SessionStateVariableEntry(
SpecialVariables.DebugPreference,
DefaultDebugPreference,
RunspaceInit.DebugPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.ErrorActionPreference,
DefaultErrorActionPreference,
RunspaceInit.ErrorActionPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.ProgressPreference,
DefaultProgressPreference,
RunspaceInit.ProgressPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.VerbosePreference,
DefaultVerbosePreference,
RunspaceInit.VerbosePreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.WarningPreference,
DefaultWarningPreference,
RunspaceInit.WarningPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.InformationPreference,
DefaultInformationPreference,
RunspaceInit.InformationPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ActionPreference))),
new SessionStateVariableEntry(
SpecialVariables.ErrorView,
DefaultErrorView,
RunspaceInit.ErrorViewDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(ErrorView))),
new SessionStateVariableEntry(
SpecialVariables.NestedPromptLevel,
0,
RunspaceInit.NestedPromptLevelDescription),
new SessionStateVariableEntry(
SpecialVariables.WhatIfPreference,
DefaultWhatIfPreference,
RunspaceInit.WhatIfPreferenceDescription),
new SessionStateVariableEntry(
FormatEnumerationLimit,
DefaultFormatEnumerationLimit,
RunspaceInit.FormatEnumerationLimitDescription),
// variable for PSEmailServer
new SessionStateVariableEntry(
SpecialVariables.PSEmailServer,
string.Empty,
RunspaceInit.PSEmailServerDescription),
// variable for PSEmailServer
new SessionStateVariableEntry(
SpecialVariables.PSEmailServer,
string.Empty,
RunspaceInit.PSEmailServerDescription),
// Start: Variables which control remoting behavior
new SessionStateVariableEntry(
Microsoft.PowerShell.Commands.PSRemotingBaseCmdlet.DEFAULT_SESSION_OPTION,
new System.Management.Automation.Remoting.PSSessionOption(),
RemotingErrorIdStrings.PSDefaultSessionOptionDescription,
ScopedItemOptions.None),
new SessionStateVariableEntry(
SpecialVariables.PSSessionConfigurationName,
"http://schemas.microsoft.com/powershell/Microsoft.PowerShell",
RemotingErrorIdStrings.PSSessionConfigurationName,
ScopedItemOptions.None),
new SessionStateVariableEntry(
SpecialVariables.PSSessionApplicationName,
"wsman",
RemotingErrorIdStrings.PSSessionAppName,
ScopedItemOptions.None),
// End: Variables which control remoting behavior
// Start: Variables which control remoting behavior
new SessionStateVariableEntry(
Microsoft.PowerShell.Commands.PSRemotingBaseCmdlet.DEFAULT_SESSION_OPTION,
new System.Management.Automation.Remoting.PSSessionOption(),
RemotingErrorIdStrings.PSDefaultSessionOptionDescription,
ScopedItemOptions.None),
new SessionStateVariableEntry(
SpecialVariables.PSSessionConfigurationName,
"http://schemas.microsoft.com/powershell/Microsoft.PowerShell",
RemotingErrorIdStrings.PSSessionConfigurationName,
ScopedItemOptions.None),
new SessionStateVariableEntry(
SpecialVariables.PSSessionApplicationName,
"wsman",
RemotingErrorIdStrings.PSSessionAppName,
ScopedItemOptions.None),
// End: Variables which control remoting behavior
#region Platform
new SessionStateVariableEntry(
SpecialVariables.IsLinux,
Platform.IsLinux,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
#region Platform
new SessionStateVariableEntry(
SpecialVariables.IsLinux,
Platform.IsLinux,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
new SessionStateVariableEntry(
SpecialVariables.IsMacOS,
Platform.IsMacOS,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
new SessionStateVariableEntry(
SpecialVariables.IsMacOS,
Platform.IsMacOS,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
new SessionStateVariableEntry(
SpecialVariables.IsWindows,
Platform.IsWindows,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
new SessionStateVariableEntry(
SpecialVariables.IsWindows,
Platform.IsWindows,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
new SessionStateVariableEntry(
SpecialVariables.IsCoreCLR,
Platform.IsCoreCLR,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
#endregion
};
new SessionStateVariableEntry(
SpecialVariables.IsCoreCLR,
Platform.IsCoreCLR,
string.Empty,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope),
#endregion
};
if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSNativeCommandErrorActionPreferenceFeatureName))
{
builtinVariables.Add(
new SessionStateVariableEntry(
SpecialVariables.PSNativeCommandUseErrorActionPreference,
DefaultPSNativeCommandUseErrorActionPreference,
RunspaceInit.PSNativeCommandUseErrorActionPreferenceDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(bool))));
}
BuiltInVariables = builtinVariables.ToArray();
}
internal static readonly SessionStateVariableEntry[] BuiltInVariables;
/// <summary>
/// Returns a new array of alias entries everytime it's called. This

View file

@ -2804,8 +2804,16 @@ namespace System.Management.Automation
_WriteErrorSkipAllowCheck(errorRecord, preference);
}
// NOTICE-2004/06/08-JonN 959638
// Use this variant to skip the ThrowIfWriteNotPermitted check
/// <summary>
/// Write an error, skipping the ThrowIfWriteNotPermitted check.
/// </summary>
/// <param name="errorRecord">The error record to write.</param>
/// <param name="actionPreference">The configured error action preference.</param>
/// <param name="isFromNativeStdError">
/// True when this method is called to write from a native command's stderr stream.
/// When errors are written through a native stderr stream, they do not interact with the error preference system,
/// but must still present as errors in PowerShell.
/// </param>
/// <exception cref="System.Management.Automation.PipelineStoppedException">
/// The pipeline has already been terminated, or was terminated
/// during the execution of this method.
@ -2819,7 +2827,7 @@ namespace System.Management.Automation
/// but the command failure will ultimately be
/// <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 isFromNativeStdError = false)
{
ThrowIfStopping();
@ -2839,7 +2847,7 @@ namespace System.Management.Automation
this.PipelineProcessor.LogExecutionError(_thisCommand.MyInvocation, errorRecord);
}
if (!isNativeError)
if (!isFromNativeStdError)
{
this.PipelineProcessor.ExecutionFailed = true;
@ -2905,7 +2913,7 @@ namespace System.Management.Automation
// when tracing), so don't add the member again.
// We don't add a note property on messages that comes from stderr stream.
if (!isNativeError)
if (!isFromNativeStdError)
{
errorWrap.WriteStream = WriteStreamType.Error;
}

View file

@ -3,21 +3,22 @@
#pragma warning disable 1634, 1691
using System.Diagnostics;
using System.IO;
using System.ComponentModel;
using System.Text;
using System.Collections;
using System.Threading;
using System.Management.Automation.Internal;
using System.Xml;
using System.Runtime.InteropServices;
using Dbg = System.Management.Automation.Diagnostics;
using System.Runtime.Serialization;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Xml;
using Dbg = System.Management.Automation.Diagnostics;
namespace System.Management.Automation
{
@ -130,6 +131,100 @@ namespace System.Management.Automation
}
}
#nullable enable
/// <summary>
/// This exception is used by the NativeCommandProcessor to indicate an error
/// when a native command retuns a non-zero exit code.
/// </summary>
[Serializable]
public sealed class NativeCommandExitException : RuntimeException
{
// NOTE:
// When implementing the native error action preference integration,
// reusing ApplicationFailedException was rejected.
// Instead of reusing a type already used in another scenario
// it was decided instead to use a fresh type to avoid conflating the two scenarios:
// * ApplicationFailedException: PowerShell was not able to complete execution of the application.
// * NativeCommandExitException: the application completed execution but returned a non-zero exit code.
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="NativeCommandExitException"/> class with information on the native
/// command, a specified error message and a specified error ID.
/// </summary>
/// <param name="path">The full path of the native command.</param>
/// <param name="exitCode">The exit code returned by the native command.</param>
/// <param name="processId">The process ID of the process before it ended.</param>
/// <param name="message">The error message.</param>
/// <param name="errorId">The PowerShell runtime error ID.</param>
internal NativeCommandExitException(string path, int exitCode, int processId, string message, string errorId)
: base(message)
{
SetErrorId(errorId);
SetErrorCategory(ErrorCategory.NotSpecified);
Path = path;
ExitCode = exitCode;
ProcessId = processId;
}
/// <summary>
/// Initializes a new instance of the <see cref="NativeCommandExitException"/> class with serialized data.
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
private NativeCommandExitException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
if (info is null)
{
throw new PSArgumentNullException(nameof(info));
}
Path = info.GetString(nameof(Path));
ExitCode = info.GetInt32(nameof(ExitCode));
ProcessId = info.GetInt32(nameof(ProcessId));
}
#endregion Constructors
/// <summary>
/// Serializes the exception data.
/// </summary>
/// <param name="info">Serialization information.</param>
/// <param name="context">Streaming context.</param>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info is null)
{
throw new PSArgumentNullException(nameof(info));
}
base.GetObjectData(info, context);
info.AddValue(nameof(Path), Path);
info.AddValue(nameof(ExitCode), ExitCode);
info.AddValue(nameof(ProcessId), ProcessId);
}
/// <summary>
/// Gets the path of the native command.
/// </summary>
public string? Path { get; }
/// <summary>
/// Gets the exit code returned by the native command.
/// </summary>
public int ExitCode { get; }
/// <summary>
/// Gets the native command's process ID.
/// </summary>
public int ProcessId { get; }
}
#nullable restore
/// <summary>
/// Provides way to create and execute native commands.
/// </summary>
@ -762,8 +857,35 @@ namespace System.Management.Automation
}
this.Command.Context.SetVariable(SpecialVariables.LastExitCodeVarPath, _nativeProcess.ExitCode);
if (_nativeProcess.ExitCode != 0)
this.commandRuntime.PipelineProcessor.ExecutionFailed = true;
if (_nativeProcess.ExitCode == 0)
{
return;
}
this.commandRuntime.PipelineProcessor.ExecutionFailed = true;
if (!ExperimentalFeature.IsEnabled(ExperimentalFeature.PSNativeCommandErrorActionPreferenceFeatureName)
|| !(bool)Command.Context.GetVariableValue(SpecialVariables.PSNativeCommandUseErrorActionPreferenceVarPath, defaultValue: false))
{
return;
}
const string errorId = nameof(CommandBaseStrings.ProgramExitedWithNonZeroCode);
string errorMsg = StringUtil.Format(
CommandBaseStrings.ProgramExitedWithNonZeroCode,
NativeCommandName,
_nativeProcess.ExitCode);
var exception = new NativeCommandExitException(
Path,
_nativeProcess.ExitCode,
_nativeProcess.Id,
errorMsg,
errorId);
var errorRecord = new ErrorRecord(exception, errorId, ErrorCategory.NotSpecified, targetObject: NativeCommandName);
this.commandRuntime._WriteErrorSkipAllowCheck(errorRecord);
}
}
catch (Win32Exception e)
@ -1087,7 +1209,7 @@ namespace System.Management.Automation
ErrorRecord record = outputValue.Data as ErrorRecord;
Dbg.Assert(record != null, "ProcessReader should ensure that data is ErrorRecord");
record.SetInvocationInfo(this.Command.MyInvocation);
this.commandRuntime._WriteErrorSkipAllowCheck(record, isNativeError: true);
this.commandRuntime._WriteErrorSkipAllowCheck(record, isFromNativeStdError: true);
}
else if (outputValue.Stream == MinishellStream.Output)
{

View file

@ -255,6 +255,11 @@ namespace System.Management.Automation
internal static readonly VariablePath InformationPreferenceVarPath = new VariablePath(InformationPreference);
internal const string PSNativeCommandUseErrorActionPreference = nameof(PSNativeCommandUseErrorActionPreference);
internal static readonly VariablePath PSNativeCommandUseErrorActionPreferenceVarPath =
new(PSNativeCommandUseErrorActionPreference);
#endregion Preference Variables
// Native command argument passing style
@ -321,25 +326,30 @@ namespace System.Management.Automation
/* PSCommandPath */ typeof(string),
};
internal static readonly string[] PreferenceVariables = {
SpecialVariables.DebugPreference,
SpecialVariables.VerbosePreference,
SpecialVariables.ErrorActionPreference,
SpecialVariables.WhatIfPreference,
SpecialVariables.WarningPreference,
SpecialVariables.InformationPreference,
SpecialVariables.ConfirmPreference,
};
// This array and the one below it exist to optimize the way common parameters work in advanced functions.
// Common parameters work by setting preference variables in the scope of the function and restoring the old value afterward.
// Variables that don't correspond to common cmdlet parameters don't need to be added here.
internal static readonly string[] PreferenceVariables =
{
SpecialVariables.DebugPreference,
SpecialVariables.VerbosePreference,
SpecialVariables.ErrorActionPreference,
SpecialVariables.WhatIfPreference,
SpecialVariables.WarningPreference,
SpecialVariables.InformationPreference,
SpecialVariables.ConfirmPreference,
};
internal static readonly Type[] PreferenceVariableTypes = {
/* DebugPreference */ typeof(ActionPreference),
/* VerbosePreference */ typeof(ActionPreference),
/* ErrorPreference */ typeof(ActionPreference),
/* WhatIfPreference */ typeof(SwitchParameter),
/* WarningPreference */ typeof(ActionPreference),
/* InformationPreference */ typeof(ActionPreference),
/* ConfirmPreference */ typeof(ConfirmImpact),
};
internal static readonly Type[] PreferenceVariableTypes =
{
/* DebugPreference */ typeof(ActionPreference),
/* VerbosePreference */ typeof(ActionPreference),
/* ErrorPreference */ typeof(ActionPreference),
/* WhatIfPreference */ typeof(SwitchParameter),
/* WarningPreference */ typeof(ActionPreference),
/* InformationPreference */ typeof(ActionPreference),
/* ConfirmPreference */ typeof(ConfirmImpact),
};
// The following variables are created in every session w/ AllScope. We avoid creating local slots when we
// see an assignment to any of these variables so that they get handled properly (either throwing an exception

View file

@ -156,6 +156,15 @@
<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="ProgramExitedWithNonZeroCode" xml:space="preserve">
<!-- NOTE:
This string was added for the native command error action preference integration feature.
ParserStrings already declares a ProgramFailedToExecute string,
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>
</data>
<data name="ShouldProcessMessage" xml:space="preserve">
<value>Performing the operation "{0}" on target "{1}".</value>
</data>

View file

@ -186,6 +186,9 @@
<data name="NestedPromptLevelDescription" xml:space="preserve">
<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>
</data>
<data name="WhatIfPreferenceDescription" xml:space="preserve">
<value>If true, WhatIf is considered to be enabled for all commands.</value>
</data>

View file

@ -0,0 +1,178 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Functional tests to verify that native executables throw errors (non-terminating and terminating) appropriately
# when $PSNativeCommandUseErrorActionPreference is $true
Describe 'Native command error handling tests' -Tags 'CI' {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if (-not [ExperimentalFeature]::IsEnabled('PSNativeCommandErrorActionPreference'))
{
$PSDefaultParameterValues['It:Skip'] = $true
return
}
$exeName = $IsWindows ? 'testexe.exe' : 'testexe'
$errorActionPrefTestCases = @(
@{ ErrorActionPref = 'Stop' }
@{ ErrorActionPref = 'Continue' }
@{ ErrorActionPref = 'SilentlyContinue' }
@{ ErrorActionPref = 'Ignore' }
)
}
AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
BeforeEach {
$Error.Clear()
}
Context 'PSNativeCommandUseErrorActionPreference is $true' {
BeforeEach {
$PSNativeCommandUseErrorActionPreference = $true
}
It 'Non-zero exit code throws teminating error for $ErrorActionPreference = ''Stop''' {
$ErrorActionPreference = 'Stop'
{ testexe -returncode 1 } | Should -Throw -ErrorId 'ProgramExitedWithNonZeroCode'
$error.Count | Should -Be 1
$error[0].FullyQualifiedErrorId | Should -BeExactly 'ProgramExitedWithNonZeroCode'
$error[0].TargetObject | Should -BeExactly $exeName
}
It 'Non-zero exit code outputs a non-teminating error for $ErrorActionPreference = ''Continue''' {
$ErrorActionPreference = 'Continue'
$stderr = testexe -returncode 1 2>&1
$error[0].FullyQualifiedErrorId | Should -BeExactly 'ProgramExitedWithNonZeroCode'
$error[0].TargetObject | Should -BeExactly $exeName
$stderr[1].Exception.Message | Should -BeExactly "Program `"$exeName`" ended with non-zero exit code: 1."
}
It 'Non-zero exit code generates a non-teminating error for $ErrorActionPreference = ''SilentlyContinue''' {
$ErrorActionPreference = 'SilentlyContinue'
testexe -returncode 1 > $null
$error.Count | Should -Be 1
$error[0].FullyQualifiedErrorId | Should -BeExactly 'ProgramExitedWithNonZeroCode'
$error[0].TargetObject | Should -BeExactly $exeName
}
It 'Non-zero exit code does not generates an error record for $ErrorActionPreference = ''Ignore''' {
$ErrorActionPreference = 'Ignore'
testexe -returncode 1 > $null
$LASTEXITCODE | Should -Be 1
$error.Count | Should -Be 0
}
It 'Zero exit code generates no error for $ErrorActionPreference = ''<ErrorActionPref>''' -TestCases $errorActionPrefTestCases {
param($ErrorActionPref)
$ErrorActionPreference = $ErrorActionPref
$output = testexe -returncode 0
$output | Should -BeExactly '0'
$LASTEXITCODE | Should -Be 0
$Error.Count | Should -Be 0
}
It 'Works as expected with a try/catch block when $ErrorActionPreference = ''<ErrorActionPref>''' -TestCase $errorActionPrefTestCases {
param($ErrorActionPref)
$ErrorActionPreference = $ErrorActionPref
$threw = $false
$continued = $false
$hitFinally = $false
try
{
testexe -returncode 17 2>&1 > $null
$continued = $true
}
catch
{
$threw = $true
$exception = $_.Exception
}
finally
{
$hitFinally = $true
}
$hitFinally | Should -BeTrue
$continued | Should -Be ($ErrorActionPreference -ne 'Stop')
$threw | Should -Be ($ErrorActionPreference -eq 'Stop')
if ($threw)
{
$exception.Path | Should -BeExactly (Get-Command -Name testexe -CommandType Application).Path
$exception.ExitCode | Should -Be $LASTEXITCODE
$exception.ProcessId | Should -BeGreaterThan 0
}
}
It 'Works with trap when $ErrorActionPreference = ''<ErrorActionPref>''' -TestCases $errorActionPrefTestCases {
param($ErrorActionPref)
$ErrorActionPreference = $ErrorActionPref
trap
{
$hitTrap = $true
$exception = $_
continue
}
$hitTrap = $false
# Expect this to be trapped
testexe -returncode 17 2>&1 > $null
if ($ErrorActionPreference -eq 'Stop')
{
$hitTrap | Should -BeTrue
$exception.ExitCode | Should -Be $LASTEXITCODE
$exception.Path | Should -BeExactly (Get-Command -Name testexe -CommandType Application).Path
$exception.ProcessId | Should -BeGreaterThan 0
}
else
{
$hitTrap | Should -BeFalse
$exception | Should -BeNullOrEmpty
}
}
}
Context 'PSNativeCommandUseErrorActionPreference is $false' {
BeforeEach {
$PSNativeCommandUseErrorActionPreference = $false
}
It 'Non-zero exit code generates no error for $ErrorActionPreference = ''<ErrorActionPref>''' -TestCases $errorActionPrefTestCases {
param($ErrorActionPref)
$ErrorActionPreference = $ErrorActionPref
if ($ErrorActionPref -eq 'Stop') {
{ testexe -returncode 1 } | Should -Not -Throw
}
else {
testexe -returncode 1 > $null
}
$LASTEXITCODE | Should -Be 1
$Error.Count | Should -Be 0
}
}
}