Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3bf31dc
WIP: Initial impl of RFC-277 - native exe throws on non-zero exit code
rkeithhill Jul 11, 2021
aa6d8dc
Align impl more with RFC, still need to impl custom exception
rkeithhill Jul 15, 2021
fc7037e
Missed a pref var rename
rkeithhill Jul 15, 2021
879c138
Adress some of @iSazonov PR feedback
rkeithhill Jul 15, 2021
b270a58
Decrease code indentation per PR feedback
rkeithhill Jul 16, 2021
2d87745
Use nameof for preference variable declaration per PR feedback
rkeithhill Jul 18, 2021
9ba3982
First pass impl of NativeCommandException
rkeithhill Jul 18, 2021
e60a4c0
Fix some of the code factor issues around exception doc comments
rkeithhill Jul 20, 2021
67cdd1c
First pass at adding tests
rkeithhill Jul 22, 2021
79fdcfd
For tests, switch to testexe, simplify tests using -TestCases
rkeithhill Jul 24, 2021
73d0535
Allow native errors from exit codes to throw properly
rjmholt Jul 26, 2021
1ca8d91
Add experimental feature support
rjmholt Jul 27, 2021
949608e
Change name of exception to NativeCommandExitException
rjmholt Jul 27, 2021
31f958d
Merge pull request #17 from rjmholt/nativeerror-preference
rkeithhill Aug 5, 2021
a9be5fc
Merge branch 'master' into rkeithhill/rfc-277-throw-on-native-nonzero…
rkeithhill Aug 5, 2021
9a40ae4
Change way NativeCommandErrorHandling tests set/reset It:Skip, fix typo
rkeithhill Aug 5, 2021
3990835
Align native error variable type
rjmholt Aug 6, 2021
68e0506
Add comments explaining new errors
rjmholt Aug 6, 2021
f00e494
Update wording on native error action preference description
rjmholt Aug 6, 2021
20ffbe5
Add extra tests
rjmholt Aug 6, 2021
a9c54e0
Merge pull request #21 from rjmholt/nativeerror-extra-tests
rkeithhill Aug 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class ExperimentalFeature
internal const string EngineSource = "PSEngine";
internal const string PSAnsiProgressFeatureName = "PSAnsiProgress";
internal const string PSNativeCommandArgumentPassingFeatureName = "PSNativeCommandArgumentPassing";
internal const string PSNativeCommandErrorActionPreferenceFeatureName = "PSNativeCommandErrorActionPreference";

#endregion

Expand Down Expand Up @@ -140,6 +141,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: "PSLoadAssemblyFromNativeCode",
description: "Expose an API to allow assembly loading from native code"),
new ExperimentalFeature(
name: PSNativeCommandErrorActionPreferenceFeatureName,
description: "Native commands with non-zero exit codes issue errors according to $ErrorActionPreference when $PSNativeCommandUseErrorActionPreference is $true"),
new ExperimentalFeature(
name: "PSAnsiRenderingFileInfo",
description: "Enable coloring for FileInfo objects"),
Expand Down
309 changes: 164 additions & 145 deletions src/System.Management.Automation/engine/InitialSessionState.cs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/System.Management.Automation/engine/MshCommandRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2819,7 +2819,7 @@ private void DoWriteError(object obj)
/// 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 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.

{
ThrowIfStopping();

Expand All @@ -2839,7 +2839,7 @@ internal void _WriteErrorSkipAllowCheck(ErrorRecord errorRecord, ActionPreferenc
this.PipelineProcessor.LogExecutionError(_thisCommand.MyInvocation, errorRecord);
}

if (!(ExperimentalFeature.IsEnabled("PSNotApplyErrorActionToStderr") && isNativeError))
if (!(ExperimentalFeature.IsEnabled("PSNotApplyErrorActionToStderr") && isNativeStdErrCall))
{
this.PipelineProcessor.ExecutionFailed = true;

Expand Down Expand Up @@ -2905,7 +2905,7 @@ internal void _WriteErrorSkipAllowCheck(ErrorRecord errorRecord, ActionPreferenc
// 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 (!isNativeStdErrCall)
{
errorWrap.WriteStream = WriteStreamType.Error;
}
Expand Down
182 changes: 170 additions & 12 deletions src/System.Management.Automation/engine/NativeCommandProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@

#pragma warning disable 1634, 1691

using System.Collections;
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.ComponentModel;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
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;

namespace System.Management.Automation
{
Expand Down Expand Up @@ -130,6 +131,120 @@ internal ProcessOutputObject(object data, MinishellStream stream)
}
}

/// <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 class NativeCommandExitException : RuntimeException
{
// NOTE:
// When implementing the native error action preference integration,
// reusing ApplicationFailedException was contemplated.
// However, rather than possibly reusing a type already used in another scenario
// it was decided instead to use a fresh type to minimize the chance of conflating the two scenarios.

/// <summary>
/// Gets the path of the native command.
/// </summary>
public string Path { get; private set; }

/// <summary>
/// Gets the exit code returned by the native command.
/// </summary>
public int ExitCode { get; private set; }

/// <summary>
/// Gets the native command's process Id.
/// </summary>
public int ProcessId { get; private set; }

#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 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.
/// </summary>
public NativeCommandExitException() : base() { }

/// <summary>
/// Initializes a new instance of the <see cref="NativeCommandExitException"/> class with a specified error message.
/// </summary>
/// <param name="message">
/// A localized error message.
/// </param>
public NativeCommandExitException(string message) : base(message) { }

/// <summary>
/// Initializes a new instance of the <see cref="NativeCommandExitException"/> class with a specified error message
/// and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">
/// Localized error message.
/// </param>
/// <param name="innerException">
/// Inner exception.
/// </param>
public NativeCommandExitException(string message, Exception innerException) : base(message, innerException) { }

/// <summary>
/// Initializes a new instance of the <see cref="NativeCommandExitException"/> class with serialized data.
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
protected NativeCommandExitException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
if (info == 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 == 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>
/// Provides way to create and execute native commands.
/// </summary>
Expand Down Expand Up @@ -762,8 +877,51 @@ internal override void Complete()
}

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;

bool nativeCommandUseErrorActionPreference = ExperimentalFeature.IsEnabled(ExperimentalFeature.PSNativeCommandErrorActionPreferenceFeatureName)
&& this.Command.Context.GetBooleanPreference(
Comment on lines +885 to +888
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)?

SpecialVariables.PSNativeCommandUseErrorActionPreferenceVarPath,
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).

{
return;
}

ActionPreference errorActionPref =
this.Command.Context.GetEnumPreference(
SpecialVariables.ErrorActionPreferenceVarPath,
ActionPreference.SilentlyContinue,
out _);

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?


string errorMsg =
StringUtil.Format(
CommandBaseStrings.ProgramFailedToComplete,
this.NativeCommandName,
_nativeProcess.ExitCode);

var exception =
new NativeCommandExitException(
this.Path,
_nativeProcess.ExitCode,
_nativeProcess.Id,
errorMsg,
errorId);

var errorRecord = new ErrorRecord(exception, errorId, ErrorCategory.NotSpecified, null);
this.commandRuntime._WriteErrorSkipAllowCheck(errorRecord);
}
}
}
catch (Win32Exception e)
Expand Down Expand Up @@ -1087,7 +1245,7 @@ private void ProcessOutputRecord(ProcessOutputObject outputValue)
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, isNativeStdErrCall: true);
}
else if (outputValue.Stream == MinishellStream.Output)
{
Expand Down
48 changes: 29 additions & 19 deletions src/System.Management.Automation/engine/SpecialVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ internal static class SpecialVariables

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


internal static readonly VariablePath PSNativeCommandUseErrorActionPreferenceVarPath =
new VariablePath(PSNativeCommandUseErrorActionPreference);

#endregion Preference Variables

// Native command argument passing style
Expand Down Expand Up @@ -321,25 +326,29 @@ internal static class SpecialVariables
/* PSCommandPath */ typeof(string),
};

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 string[] PreferenceVariables =
{
SpecialVariables.DebugPreference,
SpecialVariables.VerbosePreference,
SpecialVariables.ErrorActionPreference,
SpecialVariables.WhatIfPreference,
SpecialVariables.WarningPreference,
SpecialVariables.InformationPreference,
SpecialVariables.ConfirmPreference,
SpecialVariables.PSNativeCommandUseErrorActionPreference,
};

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),
/* 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.

Should we follow history pattern and do not use "PS" prefix?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This was decided in the RFC though

};

// 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
Expand Down Expand Up @@ -406,5 +415,6 @@ internal enum PreferenceVariable
Warning = 13,
Information = 14,
Confirm = 15,
PSNativeCommandUseErrorAction = 16,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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="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).

<!-- 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>
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>

</data>
<data name="ShouldProcessMessage" xml:space="preserve">
<value>Performing the operation "{0}" on target "{1}".</value>
</data>
Expand Down
3 changes: 3 additions & 0 deletions src/System.Management.Automation/resources/RunspaceInit.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
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 :-)

</data>
<data name="WhatIfPreferenceDescription" xml:space="preserve">
<value>If true, WhatIf is considered to be enabled for all commands.</value>
</data>
Expand Down
Loading