Make the native command error handling optionally honor ErrorActionPreference
(#15897)
This commit is contained in:
parent
56d22bc386
commit
a4d14576b3
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue