From 139cd942b23b18c3220cb57ea2564e6f154c8fef Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sat, 7 Sep 2019 15:45:36 -0300 Subject: [PATCH] Transition ActionPreference.Suspend enumeration value into a non-supported, reserved state, and remove restriction on using ActionPreference.Ignore in preference variables (#10317) --- .../engine/ArgumentTypeConverterAttribute.cs | 5 +- .../engine/CommandBase.cs | 2 +- .../engine/ExecutionContext.cs | 30 ++- .../engine/MshCommandRuntime.cs | 16 +- .../resources/ErrorPackage.resx | 15 +- .../Scripting/ActionPreference.Tests.ps1 | 255 +++++++++++------- 6 files changed, 203 insertions(+), 120 deletions(-) diff --git a/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs b/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs index 19c383e6cd..746d2f82df 100644 --- a/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs +++ b/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs @@ -147,15 +147,14 @@ namespace System.Management.Automation // Note - this is duplicated in ExecutionContext.cs as parameter binding for script cmdlets can avoid this code path. if ((!bindingScriptCmdlet) && (!bindingParameters)) { - // ActionPreference of Suspend is not supported as a preference variable. We can only block "Suspend" - // during variable assignment (here) - "Ignore" is blocked during variable retrieval. + // ActionPreference.Suspend is reserved for future use and is not supported as a preference variable. if (_convertTypes[i] == typeof(ActionPreference)) { ActionPreference resultPreference = (ActionPreference)result; if (resultPreference == ActionPreference.Suspend) { - throw new PSInvalidCastException("InvalidActionPreference", null, ErrorPackage.UnsupportedPreferenceVariable, resultPreference); + throw new PSInvalidCastException("InvalidActionPreference", null, ErrorPackage.ActionPreferenceReservedForFutureUseError, resultPreference); } } } diff --git a/src/System.Management.Automation/engine/CommandBase.cs b/src/System.Management.Automation/engine/CommandBase.cs index 8848d86f3d..b2351059cc 100644 --- a/src/System.Management.Automation/engine/CommandBase.cs +++ b/src/System.Management.Automation/engine/CommandBase.cs @@ -291,7 +291,7 @@ namespace System.Management.Automation /// Ignore the event completely (not even logging it to the target stream) Ignore = 4, - /// Suspend the command for further diagnosis. Supported only for workflows. + /// Reserved for future use. Suspend = 5, /// Enter the debugger. diff --git a/src/System.Management.Automation/engine/ExecutionContext.cs b/src/System.Management.Automation/engine/ExecutionContext.cs index 45cb3bd7fa..ac6fea07a0 100644 --- a/src/System.Management.Automation/engine/ExecutionContext.cs +++ b/src/System.Management.Automation/engine/ExecutionContext.cs @@ -573,18 +573,9 @@ namespace System.Management.Automation object val = EngineSessionState.GetVariableValue(preferenceVariablePath, out _, out _); if (val is T) { - // We don't want to support "Ignore" as action preferences, as it leads to bad - // scripting habits. They are only supported as cmdlet overrides. - if (val is ActionPreference) + if (val is ActionPreference actionPreferenceValue) { - ActionPreference preference = (ActionPreference)val; - if ((preference == ActionPreference.Ignore) || (preference == ActionPreference.Suspend)) - { - // Reset the variable value - EngineSessionState.SetVariableValue(preferenceVariablePath.UserPath, defaultPref); - string message = StringUtil.Format(ErrorPackage.UnsupportedPreferenceError, preference); - throw new NotSupportedException(message); - } + CheckActionPreference(preferenceVariablePath, actionPreferenceValue, defaultPref); } T convertedResult = (T)val; @@ -611,6 +602,11 @@ namespace System.Management.Automation result = (T)PSObject.Base(val); defaultUsed = false; } + + if (result is ActionPreference actionPreferenceValue) + { + CheckActionPreference(preferenceVariablePath, actionPreferenceValue, defaultPref); + } } catch (InvalidCastException) { @@ -625,6 +621,18 @@ namespace System.Management.Automation return result; } + private void CheckActionPreference(VariablePath preferenceVariablePath, ActionPreference preference, object defaultValue) + { + if (preference == ActionPreference.Suspend) + { + // ActionPreference.Suspend is reserved for future use. When it is used, reset + // the variable to its default. + string message = StringUtil.Format(ErrorPackage.ReservedActionPreferenceReplacedError, preference, preferenceVariablePath.UserPath, defaultValue); + EngineSessionState.SetVariable(preferenceVariablePath, defaultValue, true, CommandOrigin.Internal); + throw new NotSupportedException(message); + } + } + /// /// Same as GetEnumPreference, but for boolean values. /// diff --git a/src/System.Management.Automation/engine/MshCommandRuntime.cs b/src/System.Management.Automation/engine/MshCommandRuntime.cs index 07208dc343..f9e4b6bae6 100644 --- a/src/System.Management.Automation/engine/MshCommandRuntime.cs +++ b/src/System.Management.Automation/engine/MshCommandRuntime.cs @@ -3009,6 +3009,11 @@ namespace System.Management.Automation set { + if (value == ActionPreference.Suspend) + { + throw PSTraceSource.NewNotSupportedException(ErrorPackage.ActionPreferenceReservedForFutureUseError, value); + } + _debugPreference = value; _isDebugPreferenceSet = true; } @@ -3098,7 +3103,7 @@ namespace System.Management.Automation { if (value == ActionPreference.Suspend) { - throw PSTraceSource.NewNotSupportedException(ErrorPackage.SuspendActionPreferenceErrorActionOnly); + throw PSTraceSource.NewNotSupportedException(ErrorPackage.ActionPreferenceReservedForFutureUseError, value); } _warningPreference = value; @@ -3268,7 +3273,7 @@ namespace System.Management.Automation { if (value == ActionPreference.Suspend) { - throw PSTraceSource.NewNotSupportedException(ErrorPackage.SuspendActionPreferenceSupportedOnlyOnWorkflow); + throw PSTraceSource.NewNotSupportedException(ErrorPackage.ActionPreferenceReservedForFutureUseError, value); } _errorAction = value; @@ -3301,6 +3306,11 @@ namespace System.Management.Automation set { + if (value == ActionPreference.Suspend) + { + throw PSTraceSource.NewNotSupportedException(ErrorPackage.ActionPreferenceReservedForFutureUseError, value); + } + _progressPreference = value; _isProgressPreferenceSet = true; } @@ -3335,7 +3345,7 @@ namespace System.Management.Automation { if (value == ActionPreference.Suspend) { - throw PSTraceSource.NewNotSupportedException(ErrorPackage.SuspendActionPreferenceErrorActionOnly); + throw PSTraceSource.NewNotSupportedException(ErrorPackage.ActionPreferenceReservedForFutureUseError, value); } _informationPreference = value; diff --git a/src/System.Management.Automation/resources/ErrorPackage.resx b/src/System.Management.Automation/resources/ErrorPackage.resx index 49d1e8c2fc..04c0467ba7 100644 --- a/src/System.Management.Automation/resources/ErrorPackage.resx +++ b/src/System.Management.Automation/resources/ErrorPackage.resx @@ -126,18 +126,13 @@ Object "{0}" is reported as an error. - - The action preference of "Suspend" is supported only for ErrorAction. - "Suspend" and ErrorAction should not be localized - - - The error action preference of "Suspend" is supported only on workflows. - "Suspend" should not be localized - it is a literal. - The value {0} is not supported for an ActionPreference variable. The provided value should be used only as a value for a preference parameter, and has been replaced by the default value. For more information, see the Help topic, "about_Preference_Variables." - - The value {0} is not supported for an ActionPreference variable. The provided value should be used only as a value for a preference parameter. For more information, see the Help topic, "about_Preference_Variables." + + The {0} ActionPreference value is reserved for future use and is not supported at this time. For more information about preference variables, see the Help topic, "about_Preference_Variables." + + + The {0} ActionPreference value is reserved for future use and is not supported at this time. It has been replaced in your {1} variable by the default value of {2}. For more information about preference variables, see the Help topic, "about_Preference_Variables." diff --git a/test/powershell/Language/Scripting/ActionPreference.Tests.ps1 b/test/powershell/Language/Scripting/ActionPreference.Tests.ps1 index 3cc2e0337c..3b8fb8e9ac 100644 --- a/test/powershell/Language/Scripting/ActionPreference.Tests.ps1 +++ b/test/powershell/Language/Scripting/ActionPreference.Tests.ps1 @@ -2,119 +2,190 @@ # Licensed under the MIT License. Describe "Tests for (error, warning, etc) action preference" -Tags "CI" { - BeforeAll { - $orgin = $GLOBAL:errorActionPreference + $commonActionPreferenceParameterTestCases = foreach ($commonParameterName in [System.Management.Automation.Cmdlet]::CommonParameters | Select-String Action) { + @{ + ActionPreferenceParameterName = $commonParameterName } + } - AfterAll { - if ($GLOBAL:errorActionPreference -ne $orgin) - { - $GLOBAL:errorActionPreference = $orgin + $actionPreferenceVariableTestCases = foreach ($variable in Get-Variable -Name *Preference -Scope Global | Where-Object Value -Is [System.Management.Automation.ActionPreference]) { + @{ + ActionPreferenceVariableName = $variable.Name + StreamName = $variable.Name -replace '(Action)?Preference$' + } + } + + $actionPreferenceVariableValueTestCases = @( + @{ + Value = [System.Management.Automation.ActionPreference]::Suspend + DisplayValue = '[System.Management.Automation.ActionPreference]::Suspend' + } + @{ + Value = 'Suspend' + DisplayValue = '''Suspend''' + } + ) + + BeforeAll { + $orgin = $GLOBAL:errorActionPreference + + function Join-TestCase { + [OutputType([Hashtable[]])] + [CmdletBinding()] + param( + [Hashtable[]]$Set1, + [Hashtable[]]$Set2 + ) + foreach ($ht1 in $Set1) { + foreach ($ht2 in $Set2) { + $ht1 + $ht2 + } } } - Context 'Setting ErrorActionPreference to stop prevents user from getting the error exception' { - $err = $null - try - { - get-childitem nosuchfile.nosuchextension -ErrorAction stop -ErrorVariable err + + function Test-ActionPreferenceVariableSuspendValue { + [CmdletBinding()] + param( + $Value + ) + if ($DebugPreference -eq $Value) { + Write-Debug -Message 'A debug message' + } elseif ($ErrorActionPreference -eq $Value) { + Write-Error -Message 'An error message' + } elseif ($InformationPreference -eq $Value) { + Write-Information -MessageData 'Some information' + } elseif ($ProgressPreference -eq $Value) { + Write-Progress -Activity 'Some progress' + } elseif ($VerbosePreference -eq $Value) { + Write-Verbose -Message 'A verbose message' + } elseif ($WarningPreference -eq $Value) { + Write-Warning -Message 'A warning message' } - catch {} - - It '$err.Count' { $err.Count | Should -Be 1 } - It '$err[0] should not be $null' { $err[0] | Should -Not -BeNullOrEmpty } - It '$err[0].GetType().Name' { $err[0] | Should -BeOfType "System.Management.Automation.ActionPreferenceStopException" } - It '$err[0].ErrorRecord' { $err[0].ErrorRecord | Should -Not -BeNullOrEmpty } - It '$err[0].ErrorRecord.Exception.GetType().Name' { $err[0].ErrorRecord.Exception | Should -BeOfType "System.Management.Automation.ItemNotFoundException" } } + } - It 'ActionPreference Ignore Works' { - $errorCount = $error.Count - Get-Process -Name asdfasdfsadfsadf -ErrorAction Ignore - - $error.Count | Should -BeExactly $errorCount - } - - It 'action preference of Ignore cannot be set as a preference variable' { - $e = { - $GLOBAL:errorActionPreference = "Ignore" - Get-Process -Name asdfasdfasdf - } | Should -Throw -ErrorId 'System.NotSupportedException' -PassThru - $e.CategoryInfo.Reason | Should -BeExactly 'NotSupportedException' - + AfterAll { + if ($GLOBAL:errorActionPreference -ne $orgin) { $GLOBAL:errorActionPreference = $orgin } + } - It 'action preference of Suspend cannot be set as a preference variable' { - $e = { - $GLOBAL:errorActionPreference = "Suspend" - Get-Process -Name asdfasdfasdf - } | Should -Throw -ErrorId 'RuntimeException' -PassThru - $e.CategoryInfo.Reason | Should -BeExactly 'ArgumentTransformationMetadataException' + Context 'Setting ErrorActionPreference to stop prevents user from getting the error exception' { + $err = $null + try { + Get-ChildItem nosuchfile.nosuchextension -ErrorAction stop -ErrorVariable err + } catch { } - $GLOBAL:errorActionPreference = $orgin - } + It '$err.Count' { $err.Count | Should -Be 1 } + It '$err[0] should not be $null' { $err[0] | Should -Not -BeNullOrEmpty } + It '$err[0].GetType().Name' { $err[0] | Should -BeOfType "System.Management.Automation.ActionPreferenceStopException" } + It '$err[0].ErrorRecord' { $err[0].ErrorRecord | Should -Not -BeNullOrEmpty } + It '$err[0].ErrorRecord.Exception.GetType().Name' { $err[0].ErrorRecord.Exception | Should -BeOfType "System.Management.Automation.ItemNotFoundException" } + } - It 'enum disambiguation works' { + It 'Action preference of Ignore can be set as a preference variable using a string value' { + try { + Remove-Variable -Name ErrorActionPreference -Scope Global -Force + $GLOBAL:ErrorActionPreference = 'Ignore' $errorCount = $error.Count - Get-Process -Name asdfasdfsadfsadf -ErrorAction Ig - + Get-Process -Name asdfasdfasdf $error.Count | Should -BeExactly $errorCount + } finally { + Remove-Variable -Name ErrorActionPreference -Scope Global + # Re-create the action preference variable as a strongly typed variable like it was before + [System.Management.Automation.ActionPreference]$GLOBAL:ErrorActionPreference = $orgin + } + } + + It 'Action preference of Ignore can be set as a preference variable using an enumerated value' { + try { + $GLOBAL:ErrorActionPreference = [System.Management.Automation.ActionPreference]::Ignore + $errorCount = $error.Count + Get-Process -Name asdfasdfasdf + $error.Count | Should -BeExactly $errorCount + } finally { + $GLOBAL:ErrorActionPreference = $orgin + } + } + + It 'The $global: variable does not support Suspend' -TestCases $actionPreferenceVariableTestCases { + param($ActionPreferenceVariableName) + + $e = { + Set-Variable -Name $ActionPreferenceVariableName -Scope Global -Value ([System.Management.Automation.ActionPreference]::Suspend) + } | Should -Throw -ErrorId RuntimeException -PassThru + + $e.CategoryInfo.Reason | Should -BeExactly 'ArgumentTransformationMetadataException' + } + + It 'A local $ variable does not support ' -TestCases (Join-TestCase -Set1 $actionPreferenceVariableTestCases -Set2 $actionPreferenceVariableValueTestCases) { + param( + $ActionPreferenceVariableName, + $StreamName, + $Value, + $DisplayValue + ) + + $e = { + Set-Variable -Name $ActionPreferenceVariableName -Value $Value + Test-ActionPreferenceVariableSuspendValue -Value $Value + } | Should -Throw -ErrorId "System.NotSupportedException$(if ($StreamName -ne 'Error') {",Microsoft.PowerShell.Commands.Write${StreamName}Command"})" -PassThru + + $e.CategoryInfo.Reason | Should -BeExactly 'NotSupportedException' + } + + It 'enum disambiguation works' { + $errorCount = $error.Count + Get-Process -Name asdfasdfsadfsadf -ErrorAction Ig + + $error.Count | Should -BeExactly $errorCount + } + + #issue 2076 + It 'The - common parameter does not support Suspend on cmdlets' -TestCases $commonActionPreferenceParameterTestCases { + param($ActionPreferenceParameterName) + + $commonParameters = @{ + "${ActionPreferenceParameterName}" = [System.Management.Automation.ActionPreference]::Suspend } - It 'ErrorAction = Suspend works on Workflow' -Skip:$IsCoreCLR { - . .\TestsOnWinFullOnly.ps1 - Run-TestOnWinFull "ActionPreference:ErrorAction=SuspendOnWorkflow" + { Write-Output -InputObject Test @commonParameters } | Should -Throw -ErrorId "ParameterBindingFailed,Microsoft.PowerShell.Commands.WriteOutputCommand" + } + + It 'The - common parameter does not support Suspend on functions' -TestCases $commonActionPreferenceParameterTestCases { + param($ActionPreferenceParameterName) + + function MyHelperFunction { + [CmdletBinding()] + param() + "Hello" } - It 'ErrorAction = Suspend does not work on functions' { - function MyHelperFunction { - [CmdletBinding()] - param() - "Hello" - } - - { MyHelperFunction -ErrorAction Suspend } | Should -Throw -ErrorId "ParameterBindingFailed,MyHelperFunction" + $commonParameters = @{ + "${ActionPreferenceParameterName}" = [System.Management.Automation.ActionPreference]::Suspend } - It 'ErrorAction = Suspend does not work on cmdlets' { - { Get-Process -ErrorAction Suspend } | Should -Throw -ErrorId "ParameterBindingFailed,Microsoft.PowerShell.Commands.GetProcessCommand" - } - - It 'WarningAction = Suspend does not work' { - { Get-Process -WarningAction Suspend } | Should -Throw -ErrorId "ParameterBindingFailed,Microsoft.PowerShell.Commands.GetProcessCommand" - } - - #issue 2076 - It 'ErrorAction and WarningAction are the only action preferences do not support suspend' -Pending{ - $params = [System.Management.Automation.Internal.CommonParameters].GetProperties().Name | Select-String Action - - $suspendErrors = $null - $num=0 - - $params | ForEach-Object { - $input=@{'InputObject' = 'Test';$_='Suspend'} - { Write-Output @input } | Should -Throw -ErrorId "ParameterBindingFailed,Microsoft.PowerShell.Commands.WriteOutputCommand" - } - } - - It ' does not take precedence over $ErrorActionPreference' -TestCases @( - @{switch="Verbose"}, - @{switch="Debug"} - ) { - param($switch) - $ErrorActionPreference = "SilentlyContinue" - $params = @{ - ItemType = "File"; - Path = "$testdrive\test.txt"; - Confirm = $false - } - New-Item @params > $null - $params += @{$switch=$true} - { New-Item @params } | Should -Not -Throw - $ErrorActionPreference = "Stop" - { New-Item @params } | Should -Throw -ErrorId "NewItemIOError,Microsoft.PowerShell.Commands.NewItemCommand" - Remove-Item "$testdrive\test.txt" -Force + { MyHelperFunction -ErrorAction Suspend } | Should -Throw -ErrorId "ParameterBindingFailed,MyHelperFunction" + } + + It ' does not take precedence over $ErrorActionPreference' -TestCases @( + @{switch = "Verbose" }, + @{switch = "Debug" } + ) { + param($switch) + $ErrorActionPreference = "SilentlyContinue" + $params = @{ + ItemType = "File"; + Path = "$testdrive\test.txt"; + Confirm = $false } + New-Item @params > $null + $params += @{$switch = $true } + { New-Item @params } | Should -Not -Throw + $ErrorActionPreference = "Stop" + { New-Item @params } | Should -Throw -ErrorId "NewItemIOError,Microsoft.PowerShell.Commands.NewItemCommand" + Remove-Item "$testdrive\test.txt" -Force + } } Describe 'ActionPreference.Break tests' -tag 'CI' {