diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImplicitRemotingCommands.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImplicitRemotingCommands.cs
index 325632c44cee..19bc5d44aec7 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImplicitRemotingCommands.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImplicitRemotingCommands.cs
@@ -2823,13 +2823,14 @@ private void GenerateHelperFunctions(TextWriter writer)
$clientSideParameters = Get-PSImplicitRemotingClientSideParameters $PSBoundParameters ${8}
- $scriptCmd = {{ & $script:InvokeCommand `
- @clientSideParameters `
- -HideComputerName `
- -Session (Get-PSImplicitRemotingSession -CommandName '{0}') `
- -Arg ('{0}', $PSBoundParameters, $positionalArguments) `
- -Script {{ param($name, $boundParams, $unboundParams) & $name @boundParams @unboundParams }} `
- }}
+ $scriptCmd = {{
+ & $script:InvokeCommand `
+ @clientSideParameters `
+ -HideComputerName `
+ -Session (Get-PSImplicitRemotingSession -CommandName '{0}') `
+ -Arg ('{0}', $PSBoundParameters, $positionalArguments) `
+ -Script {{ param($name, $boundParams, $unboundParams) & $name @boundParams @unboundParams }} `
+ }}
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($myInvocation.ExpectingInput, $ExecutionContext)
diff --git a/src/System.Management.Automation/engine/CommandBase.cs b/src/System.Management.Automation/engine/CommandBase.cs
index 048eafd56c8c..bb7aec959f61 100644
--- a/src/System.Management.Automation/engine/CommandBase.cs
+++ b/src/System.Management.Automation/engine/CommandBase.cs
@@ -233,6 +233,13 @@ internal virtual void DoStopProcessing()
{
}
+ ///
+ /// When overridden in the derived class, performs clean-up after the command execution.
+ ///
+ internal virtual void DoCleanResource()
+ {
+ }
+
#endregion Override
///
diff --git a/src/System.Management.Automation/engine/CommandInfo.cs b/src/System.Management.Automation/engine/CommandInfo.cs
index 7e28fece99d2..1acca145ff9b 100644
--- a/src/System.Management.Automation/engine/CommandInfo.cs
+++ b/src/System.Management.Automation/engine/CommandInfo.cs
@@ -529,7 +529,7 @@ private void GetMergedCommandParameterMetadata(out MergedCommandParameterMetadat
processor = scriptCommand != null
? new CommandProcessor(scriptCommand, _context, useLocalScope: true, fromScriptFile: false,
sessionState: scriptCommand.ScriptBlock.SessionStateInternal ?? Context.EngineSessionState)
- : new CommandProcessor((CmdletInfo)this, _context) { UseLocalScope = true };
+ : new CommandProcessor((CmdletInfo)this, _context);
ParameterBinderController.AddArgumentsToCommandProcessor(processor, Arguments);
CommandProcessorBase oldCurrentCommandProcessor = Context.CurrentCommandProcessor;
diff --git a/src/System.Management.Automation/engine/CommandMetadata.cs b/src/System.Management.Automation/engine/CommandMetadata.cs
index eb59f9140a29..bbe2b6666b2e 100644
--- a/src/System.Management.Automation/engine/CommandMetadata.cs
+++ b/src/System.Management.Automation/engine/CommandMetadata.cs
@@ -875,8 +875,11 @@ internal string GetProxyCommand(string helpComment, bool generateDynamicParamete
end
{{{5}}}
+
+clean
+{{{6}}}
<#
-{6}
+{7}
#>
",
GetDecl(),
@@ -885,6 +888,7 @@ internal string GetProxyCommand(string helpComment, bool generateDynamicParamete
GetBeginBlock(),
GetProcessBlock(),
GetEndBlock(),
+ GetCleanBlock(),
CodeGeneration.EscapeBlockCommentContent(helpComment));
return result;
@@ -1063,6 +1067,11 @@ internal string GetBeginBlock()
internal string GetProcessBlock()
{
+ // The reason we wrap scripts in 'try { } catch { throw }' (here and elsewhere) is to turn
+ // an exception that could be thrown from .NET method invocation into a terminating error
+ // that can be propagated up.
+ // By default, an exception thrown from .NET method is not terminating, but when enclosed
+ // in try/catch, it will be turned into a terminating error.
return @"
try {
$steppablePipeline.Process($_)
@@ -1113,6 +1122,16 @@ internal string GetEndBlock()
";
}
+ internal string GetCleanBlock()
+ {
+ // Here we don't need to enclose the script in a 'try/catch' like elsewhere, because
+ // 1. the 'Clean' block doesn't propagate up any exception (terminating error);
+ // 2. only one expression in the script, so nothing else needs to be stopped when invoking the method fails.
+ return @"
+ $steppablePipeline.Clean()
+";
+ }
+
#endregion
#region Helper methods for restricting commands needed by implicit and interactive remoting
diff --git a/src/System.Management.Automation/engine/CommandProcessor.cs b/src/System.Management.Automation/engine/CommandProcessor.cs
index fb1fe81fc540..62eebe5d9377 100644
--- a/src/System.Management.Automation/engine/CommandProcessor.cs
+++ b/src/System.Management.Automation/engine/CommandProcessor.cs
@@ -309,13 +309,11 @@ internal override void DoBegin()
internal override void ProcessRecord()
{
// Invoke the Command method with the request object
-
if (!this.RanBeginAlready)
{
RanBeginAlready = true;
try
{
- // NOTICE-2004/06/08-JonN 959638
using (commandRuntime.AllowThisCommandToWrite(true))
{
if (Context._debuggingMode > 0 && Command is not PSScriptCmdlet)
@@ -326,12 +324,9 @@ internal override void ProcessRecord()
Command.DoBeginProcessing();
}
}
- // 2004/03/18-JonN This is understood to be
- // an FXCOP violation, cleared by KCwalina.
- catch (Exception e) // Catch-all OK, 3rd party callout.
+ catch (Exception e)
{
- // This cmdlet threw an exception, so
- // wrap it and bubble it up.
+ // This cmdlet threw an exception, so wrap it and bubble it up.
throw ManageInvocationException(e);
}
}
@@ -366,6 +361,7 @@ internal override void ProcessRecord()
// NOTICE-2004/06/08-JonN 959638
using (commandRuntime.AllowThisCommandToWrite(true))
+ using (ParameterBinderBase.bindingTracer.TraceScope("CALLING ProcessRecord"))
{
if (CmdletParameterBinderController.ObsoleteParameterWarningList != null &&
CmdletParameterBinderController.ObsoleteParameterWarningList.Count > 0)
@@ -400,14 +396,13 @@ internal override void ProcessRecord()
}
catch (LoopFlowException)
{
- // Win8:84066 - Don't wrap LoopFlowException, we incorrectly raise a PipelineStoppedException
+ // Don't wrap LoopFlowException, we incorrectly raise a PipelineStoppedException
// which gets caught by a script try/catch if we wrap here.
throw;
}
- // 2004/03/18-JonN This is understood to be
- // an FXCOP violation, cleared by KCwalina.
- catch (Exception e) // Catch-all OK, 3rd party callout.
+ catch (Exception e)
{
+ // Catch-all OK, 3rd party callout.
exceptionToThrow = e;
}
finally
diff --git a/src/System.Management.Automation/engine/CommandProcessorBase.cs b/src/System.Management.Automation/engine/CommandProcessorBase.cs
index c6825cc69252..8033e2b3677b 100644
--- a/src/System.Management.Automation/engine/CommandProcessorBase.cs
+++ b/src/System.Management.Automation/engine/CommandProcessorBase.cs
@@ -5,8 +5,7 @@
using System.Collections.ObjectModel;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
-
-using Dbg = System.Management.Automation.Diagnostics;
+using System.Runtime.InteropServices;
namespace System.Management.Automation
{
@@ -46,6 +45,7 @@ internal CommandProcessorBase(CommandInfo commandInfo)
string errorTemplate = expAttribute.ExperimentAction == ExperimentAction.Hide
? DiscoveryExceptions.ScriptDisabledWhenFeatureOn
: DiscoveryExceptions.ScriptDisabledWhenFeatureOff;
+
string errorMsg = StringUtil.Format(errorTemplate, expAttribute.ExperimentName);
ErrorRecord errorRecord = new ErrorRecord(
new InvalidOperationException(errorMsg),
@@ -54,6 +54,8 @@ internal CommandProcessorBase(CommandInfo commandInfo)
commandInfo);
throw new CmdletInvocationException(errorRecord);
}
+
+ HasCleanBlock = scriptCommand.ScriptBlock.HasCleanBlock;
}
CommandInfo = commandInfo;
@@ -87,6 +89,11 @@ internal bool AddedToPipelineAlready
///
internal CommandInfo CommandInfo { get; set; }
+ ///
+ /// Gets whether the command has a 'Clean' block defined.
+ ///
+ internal bool HasCleanBlock { get; }
+
///
/// This indicates whether this command processor is created from
/// a script file.
@@ -371,13 +378,10 @@ internal void RestorePreviousScope()
Context.EngineSessionState = _previousCommandSessionState;
- if (_previousScope != null)
- {
- // Restore the scope but use the same session state instance we
- // got it from because the command may have changed the execution context
- // session state...
- CommandSessionState.CurrentScope = _previousScope;
- }
+ // Restore the scope but use the same session state instance we
+ // got it from because the command may have changed the execution context
+ // session state...
+ CommandSessionState.CurrentScope = _previousScope;
}
private SessionStateScope _previousScope;
@@ -452,16 +456,14 @@ internal void DoPrepare(IDictionary psDefaultParameterValues)
HandleObsoleteCommand(ObsoleteAttribute);
}
}
- catch (Exception)
+ catch (InvalidComObjectException e)
{
- if (_useLocalScope)
- {
- // If we had an exception during Prepare, we're done trying to execute the command
- // so the scope we created needs to release any resources it hold.s
- CommandSessionState.RemoveScope(CommandScope);
- }
+ // This type of exception could be thrown from parameter binding.
+ string msg = StringUtil.Format(ParserStrings.InvalidComObjectException, e.Message);
+ var newEx = new RuntimeException(msg, e);
- throw;
+ newEx.SetErrorId("InvalidComObjectException");
+ throw newEx;
}
finally
{
@@ -508,26 +510,23 @@ internal virtual void DoBegin()
// The RedirectShellErrorOutputPipe flag is used by the V2 hosting API to force the
// redirection.
//
- if (this.RedirectShellErrorOutputPipe || _context.ShellFunctionErrorOutputPipe != null)
+ if (RedirectShellErrorOutputPipe || _context.ShellFunctionErrorOutputPipe is not null)
{
- _context.ShellFunctionErrorOutputPipe = this.commandRuntime.ErrorOutputPipe;
+ _context.ShellFunctionErrorOutputPipe = commandRuntime.ErrorOutputPipe;
}
_context.CurrentCommandProcessor = this;
+ SetCurrentScopeToExecutionScope();
+
using (commandRuntime.AllowThisCommandToWrite(true))
+ using (ParameterBinderBase.bindingTracer.TraceScope("CALLING BeginProcessing"))
{
- using (ParameterBinderBase.bindingTracer.TraceScope(
- "CALLING BeginProcessing"))
+ if (Context._debuggingMode > 0 && Command is not PSScriptCmdlet)
{
- SetCurrentScopeToExecutionScope();
-
- if (Context._debuggingMode > 0 && Command is not PSScriptCmdlet)
- {
- Context.Debugger.CheckCommand(this.Command.MyInvocation);
- }
-
- Command.DoBeginProcessing();
+ Context.Debugger.CheckCommand(Command.MyInvocation);
}
+
+ Command.DoBeginProcessing();
}
}
catch (Exception e)
@@ -589,20 +588,14 @@ internal virtual void Complete()
try
{
using (commandRuntime.AllowThisCommandToWrite(true))
+ using (ParameterBinderBase.bindingTracer.TraceScope("CALLING EndProcessing"))
{
- using (ParameterBinderBase.bindingTracer.TraceScope(
- "CALLING EndProcessing"))
- {
- this.Command.DoEndProcessing();
- }
+ this.Command.DoEndProcessing();
}
}
- // 2004/03/18-JonN This is understood to be
- // an FXCOP violation, cleared by KCwalina.
catch (Exception e)
{
- // This cmdlet threw an exception, so
- // wrap it and bubble it up.
+ // This cmdlet threw an exception, wrap it as needed and bubble it up.
throw ManageInvocationException(e);
}
}
@@ -631,46 +624,121 @@ internal void DoComplete()
// The RedirectShellErrorOutputPipe flag is used by the V2 hosting API to force the
// redirection.
//
- if (this.RedirectShellErrorOutputPipe || _context.ShellFunctionErrorOutputPipe != null)
+ if (RedirectShellErrorOutputPipe || _context.ShellFunctionErrorOutputPipe is not null)
{
- _context.ShellFunctionErrorOutputPipe = this.commandRuntime.ErrorOutputPipe;
+ _context.ShellFunctionErrorOutputPipe = commandRuntime.ErrorOutputPipe;
}
_context.CurrentCommandProcessor = this;
-
SetCurrentScopeToExecutionScope();
Complete();
}
finally
{
- OnRestorePreviousScope();
-
_context.ShellFunctionErrorOutputPipe = oldErrorOutputPipe;
_context.CurrentCommandProcessor = oldCurrentCommandProcessor;
- // Destroy the local scope at this point if there is one...
- if (_useLocalScope && CommandScope != null)
- {
- CommandSessionState.RemoveScope(CommandScope);
- }
+ RestorePreviousScope();
+ }
+ }
- // and the previous scope...
- if (_previousScope != null)
+ protected virtual void CleanResource()
+ {
+ try
+ {
+ using (commandRuntime.AllowThisCommandToWrite(permittedToWriteToPipeline: true))
+ using (ParameterBinderBase.bindingTracer.TraceScope("CALLING CleanResource"))
{
- // Restore the scope but use the same session state instance we
- // got it from because the command may have changed the execution context
- // session state...
- CommandSessionState.CurrentScope = _previousScope;
+ Command.DoCleanResource();
}
+ }
+ catch (HaltCommandException)
+ {
+ throw;
+ }
+ catch (FlowControlException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ // This cmdlet threw an exception, so wrap it and bubble it up.
+ throw ManageInvocationException(e);
+ }
+ }
+
+ internal void DoCleanup()
+ {
+ // The property 'PropagateExceptionsToEnclosingStatementBlock' controls whether a general exception
+ // (an exception thrown from a .NET method invocation, or an expression like '1/0') will be turned
+ // into a terminating error, which will be propagated up and thus stop the rest of the running script.
+ // It is usually used by TryStatement and TrapStatement, which makes the general exception catch-able.
+ //
+ // For the 'Clean' block, we don't want to bubble up the general exception when the command is enclosed
+ // in a TryStatement or has TrapStatement accompanying, because no exception can escape from 'Clean' and
+ // thus it's pointless to bubble up the general exception in this case.
+ //
+ // Therefore we set this property to 'false' here to mask off the previous setting that could be from a
+ // TryStatement or TrapStatement. Example:
+ // PS:1> function b { end {} clean { 1/0; Write-Host 'clean' } }
+ // PS:2> b
+ // RuntimeException: Attempted to divide by zero.
+ // clean
+ // ## Note that, outer 'try/trap' doesn't affect the general exception happens in 'Clean' block.
+ // ## so its behavior is consistent regardless of whether the command is enclosed by 'try/catch' or not.
+ // PS:3> try { b } catch { 'outer catch' }
+ // RuntimeException: Attempted to divide by zero.
+ // clean
+ //
+ // Be noted that, this doesn't affect the TryStatement/TrapStatement within the 'Clean' block. Example:
+ // ## 'try/trap' within 'Clean' block makes the general exception catch-able.
+ // PS:3> function a { end {} clean { try { 1/0; Write-Host 'clean' } catch { Write-Host "caught: $_" } } }
+ // PS:4> a
+ // caught: Attempted to divide by zero.
+ bool oldExceptionPropagationState = _context.PropagateExceptionsToEnclosingStatementBlock;
+ _context.PropagateExceptionsToEnclosingStatementBlock = false;
- // Restore the previous session state
- if (_previousCommandSessionState != null)
+ Pipe oldErrorOutputPipe = _context.ShellFunctionErrorOutputPipe;
+ CommandProcessorBase oldCurrentCommandProcessor = _context.CurrentCommandProcessor;
+
+ try
+ {
+ if (RedirectShellErrorOutputPipe || _context.ShellFunctionErrorOutputPipe is not null)
{
- Context.EngineSessionState = _previousCommandSessionState;
+ _context.ShellFunctionErrorOutputPipe = commandRuntime.ErrorOutputPipe;
}
+
+ _context.CurrentCommandProcessor = this;
+ SetCurrentScopeToExecutionScope();
+ CleanResource();
+ }
+ finally
+ {
+ _context.PropagateExceptionsToEnclosingStatementBlock = oldExceptionPropagationState;
+ _context.ShellFunctionErrorOutputPipe = oldErrorOutputPipe;
+ _context.CurrentCommandProcessor = oldCurrentCommandProcessor;
+
+ RestorePreviousScope();
}
}
+ internal void ReportCleanupError(Exception exception)
+ {
+ var error = exception is IContainsErrorRecord icer
+ ? icer.ErrorRecord
+ : new ErrorRecord(exception, "Clean.ReportException", ErrorCategory.NotSpecified, targetObject: null);
+
+ PSObject errorWrap = PSObject.AsPSObject(error);
+ errorWrap.WriteStream = WriteStreamType.Error;
+
+ var errorPipe = commandRuntime.ErrorMergeTo == MshCommandRuntime.MergeDataStream.Output
+ ? commandRuntime.OutputPipe
+ : commandRuntime.ErrorOutputPipe;
+
+ errorPipe.Add(errorWrap);
+ _context.QuestionMarkVariableValue = false;
+ }
+
///
/// For diagnostic purposes.
///
@@ -777,23 +845,16 @@ internal PipelineStoppedException ManageInvocationException(Exception e)
{
do // false loop
{
- ProviderInvocationException pie = e as ProviderInvocationException;
- if (pie != null)
+ if (e is ProviderInvocationException pie)
{
- // If a ProviderInvocationException occurred,
- // discard the ProviderInvocationException and
- // re-wrap in CmdletProviderInvocationException
- e = new CmdletProviderInvocationException(
- pie,
- Command.MyInvocation);
+ // If a ProviderInvocationException occurred, discard the ProviderInvocationException
+ // and re-wrap it in CmdletProviderInvocationException.
+ e = new CmdletProviderInvocationException(pie, Command.MyInvocation);
break;
}
- // 1021203-2005/05/09-JonN
- // HaltCommandException will cause the command
- // to stop, but not be reported as an error.
- // 906445-2005/05/16-JonN
- // FlowControlException should not be wrapped
+ // HaltCommandException will cause the command to stop, but not be reported as an error.
+ // FlowControlException should not be wrapped.
if (e is PipelineStoppedException
|| e is CmdletInvocationException
|| e is ActionPreferenceStopException
@@ -813,9 +874,7 @@ internal PipelineStoppedException ManageInvocationException(Exception e)
}
// wrap all other exceptions
- e = new CmdletInvocationException(
- e,
- Command.MyInvocation);
+ e = new CmdletInvocationException(e, Command.MyInvocation);
} while (false);
// commandRuntime.ManageException will always throw PipelineStoppedException
@@ -943,15 +1002,27 @@ public void Dispose()
private void Dispose(bool disposing)
{
if (_disposed)
+ {
return;
+ }
if (disposing)
{
- // 2004/03/05-JonN Look into using metadata to check
- // whether IDisposable is implemented, in order to avoid
- // this expensive reflection cast.
- IDisposable id = Command as IDisposable;
- if (id != null)
+ if (UseLocalScope)
+ {
+ // Clean up the PS drives that are associated with this local scope.
+ // This operation may be needed at multiple stages depending on whether the 'clean' block is declared:
+ // 1. when there is a 'clean' block, it needs to be done only after 'clean' block runs, because the scope
+ // needs to be preserved until the 'clean' block finish execution.
+ // 2. when there is no 'clean' block, it needs to be done when
+ // (1) there is any exception thrown from 'DoPrepare()', 'DoBegin()', 'DoExecute()', or 'DoComplete';
+ // (2) OR, the command runs to the end successfully;
+ // Doing this cleanup at those multiple stages is cumbersome. Since we will always dispose the command in
+ // the end, doing this cleanup here will cover all the above cases.
+ CommandSessionState.RemoveScope(CommandScope);
+ }
+
+ if (Command is IDisposable id)
{
id.Dispose();
}
diff --git a/src/System.Management.Automation/engine/ExecutionContext.cs b/src/System.Management.Automation/engine/ExecutionContext.cs
index 8ffd25949c5b..18ba461a68ec 100644
--- a/src/System.Management.Automation/engine/ExecutionContext.cs
+++ b/src/System.Management.Automation/engine/ExecutionContext.cs
@@ -783,11 +783,6 @@ internal Pipe RedirectErrorPipe(Pipe newPipe)
return oldPipe;
}
- internal void RestoreErrorPipe(Pipe pipe)
- {
- ShellFunctionErrorOutputPipe = pipe;
- }
-
///
/// Reset all of the redirection book keeping variables. This routine should be called when starting to
/// execute a script.
@@ -840,15 +835,13 @@ internal void ResetRedirection()
internal void AppendDollarError(object obj)
{
ErrorRecord objAsErrorRecord = obj as ErrorRecord;
- if (objAsErrorRecord == null && obj is not Exception)
+ if (objAsErrorRecord is null && obj is not Exception)
{
Diagnostics.Assert(false, "Object to append was neither an ErrorRecord nor an Exception in ExecutionContext.AppendDollarError");
return;
}
- object old = this.DollarErrorVariable;
- ArrayList arraylist = old as ArrayList;
- if (arraylist == null)
+ if (DollarErrorVariable is not ArrayList arraylist)
{
Diagnostics.Assert(false, "$error should be a global constant ArrayList");
return;
diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
index dd0f72096350..cb0be3193a74 100644
--- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
+++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
@@ -23,6 +23,7 @@ public class ExperimentalFeature
internal const string EngineSource = "PSEngine";
internal const string PSNativeCommandArgumentPassingFeatureName = "PSNativeCommandArgumentPassing";
internal const string PSNativeCommandErrorActionPreferenceFeatureName = "PSNativeCommandErrorActionPreference";
+ internal const string PSCleanBlockFeatureName = "PSCleanBlock";
#endregion
@@ -126,6 +127,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: PSNativeCommandErrorActionPreferenceFeatureName,
description: "Native commands with non-zero exit codes issue errors according to $ErrorActionPreference when $PSNativeCommandUseErrorActionPreference is $true"),
+ new ExperimentalFeature(
+ name: PSCleanBlockFeatureName,
+ description: "Add support of a 'Clean' block to functions and script cmdlets for easy resource cleanup"),
};
EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures);
diff --git a/src/System.Management.Automation/engine/MshCommandRuntime.cs b/src/System.Management.Automation/engine/MshCommandRuntime.cs
index 209652584627..16fa8652bcce 100644
--- a/src/System.Management.Automation/engine/MshCommandRuntime.cs
+++ b/src/System.Management.Automation/engine/MshCommandRuntime.cs
@@ -942,10 +942,8 @@ internal void SetupOutVariable()
// Handle the creation of OutVariable in the case of Out-Default specially,
// as it needs to handle much of its OutVariable support itself.
- if (
- (!string.IsNullOrEmpty(this.OutVariable)) &&
- (!(this.OutVariable.StartsWith('+'))) &&
- string.Equals("Out-Default", _thisCommand.CommandInfo.Name, StringComparison.OrdinalIgnoreCase))
+ if (!OutVariable.StartsWith('+') &&
+ string.Equals("Out-Default", _commandInfo.Name, StringComparison.OrdinalIgnoreCase))
{
if (_state == null)
_state = new SessionState(Context.EngineSessionState);
@@ -2426,7 +2424,6 @@ public Exception ManageException(Exception e)
}
// Log a command health event
-
MshLog.LogCommandHealthEvent(
Context,
e,
@@ -3765,8 +3762,10 @@ internal void SetVariableListsInPipe()
{
Diagnostics.Assert(_thisCommand is PSScriptCmdlet, "this is only done for script cmdlets");
- if (_outVarList != null)
+ if (_outVarList != null && !OutputPipe.IgnoreOutVariableList)
{
+ // A null pipe is used when executing the 'Clean' block of a PSScriptCmdlet.
+ // In such a case, we don't capture output to the out variable list.
this.OutputPipe.AddVariableList(VariableStreamKind.Output, _outVarList);
}
@@ -3793,7 +3792,7 @@ internal void SetVariableListsInPipe()
internal void RemoveVariableListsInPipe()
{
- if (_outVarList != null)
+ if (_outVarList != null && !OutputPipe.IgnoreOutVariableList)
{
this.OutputPipe.RemoveVariableList(VariableStreamKind.Output, _outVarList);
}
diff --git a/src/System.Management.Automation/engine/Pipe.cs b/src/System.Management.Automation/engine/Pipe.cs
index 8ee000a4faf9..9e24c6471db6 100644
--- a/src/System.Management.Automation/engine/Pipe.cs
+++ b/src/System.Management.Automation/engine/Pipe.cs
@@ -109,6 +109,13 @@ public override string ToString()
///
internal int OutBufferCount { get; set; } = 0;
+ ///
+ /// Gets whether the out variable list should be ignored.
+ /// This is used for scenarios like the `clean` block, where writing to output stream is intentionally
+ /// disabled and thus out variables should also be ignored.
+ ///
+ internal bool IgnoreOutVariableList { get; set; }
+
///
/// If true, then all input added to this pipe will simply be discarded...
///
diff --git a/src/System.Management.Automation/engine/ProxyCommand.cs b/src/System.Management.Automation/engine/ProxyCommand.cs
index 5703adec664f..6a2e6dce66d4 100644
--- a/src/System.Management.Automation/engine/ProxyCommand.cs
+++ b/src/System.Management.Automation/engine/ProxyCommand.cs
@@ -247,6 +247,30 @@ public static string GetEnd(CommandMetadata commandMetadata)
return commandMetadata.GetEndBlock();
}
+ ///
+ /// This method constructs a string representing the clean block of the command
+ /// specified by . The returned string only contains the
+ /// script, it is not enclosed in "clean { }".
+ ///
+ ///
+ /// An instance of CommandMetadata representing a command.
+ ///
+ ///
+ /// A string representing the end block of the command.
+ ///
+ ///
+ /// If is null.
+ ///
+ public static string GetClean(CommandMetadata commandMetadata)
+ {
+ if (commandMetadata == null)
+ {
+ throw PSTraceSource.NewArgumentNullException(nameof(commandMetadata));
+ }
+
+ return commandMetadata.GetCleanBlock();
+ }
+
private static T GetProperty(PSObject obj, string property) where T : class
{
T result = null;
diff --git a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs
index 99b685e07d25..54d4f45849b0 100644
--- a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs
+++ b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs
@@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
+using System.Management.Automation.Runspaces;
using System.Reflection;
using Dbg = System.Management.Automation.Diagnostics;
@@ -47,7 +48,7 @@ protected ScriptCommandProcessorBase(IScriptCommandInfo commandInfo, ExecutionCo
protected bool _dontUseScopeCommandOrigin;
///
- /// If true, then an exit exception will be rethrown to instead of caught and processed...
+ /// If true, then an exit exception will be rethrown instead of caught and processed...
///
protected bool _rethrowExitException;
@@ -237,6 +238,7 @@ internal sealed class DlrScriptCommandProcessor : ScriptCommandProcessorBase
private MutableTuple _localsTuple;
private bool _runOptimizedCode;
private bool _argsBound;
+ private bool _anyClauseExecuted;
private FunctionContext _functionContext;
internal DlrScriptCommandProcessor(ScriptBlock scriptBlock, ExecutionContext context, bool useNewScope, CommandOrigin origin, SessionStateInternal sessionState, object dollarUnderbar)
@@ -327,8 +329,7 @@ internal override void DoBegin()
ScriptBlock.LogScriptBlockStart(_scriptBlock, Context.CurrentRunspace.InstanceId);
- // Even if there is no begin, we need to set up the execution scope for this
- // script...
+ // Even if there is no begin, we need to set up the execution scope for this script...
SetCurrentScopeToExecutionScope();
CommandProcessorBase oldCurrentCommandProcessor = Context.CurrentCommandProcessor;
try
@@ -410,6 +411,7 @@ internal override void Complete()
if (_scriptBlock.HasEndBlock)
{
var endBlock = _runOptimizedCode ? _scriptBlock.EndBlock : _scriptBlock.UnoptimizedEndBlock;
+
if (this.CommandRuntime.InputPipe.ExternalReader == null)
{
if (IsPipelineInputExpected())
@@ -433,7 +435,33 @@ internal override void Complete()
}
finally
{
- ScriptBlock.LogScriptBlockEnd(_scriptBlock, Context.CurrentRunspace.InstanceId);
+ if (!_scriptBlock.HasCleanBlock)
+ {
+ ScriptBlock.LogScriptBlockEnd(_scriptBlock, Context.CurrentRunspace.InstanceId);
+ }
+ }
+ }
+
+ protected override void CleanResource()
+ {
+ if (_scriptBlock.HasCleanBlock && _anyClauseExecuted)
+ {
+ // The 'Clean' block doesn't write to pipeline.
+ Pipe oldOutputPipe = _functionContext._outputPipe;
+ _functionContext._outputPipe = new Pipe { NullPipe = true };
+
+ try
+ {
+ RunClause(
+ clause: _runOptimizedCode ? _scriptBlock.CleanBlock : _scriptBlock.UnoptimizedCleanBlock,
+ dollarUnderbar: AutomationNull.Value,
+ inputToProcess: AutomationNull.Value);
+ }
+ finally
+ {
+ _functionContext._outputPipe = oldOutputPipe;
+ ScriptBlock.LogScriptBlockEnd(_scriptBlock, Context.CurrentRunspace.InstanceId);
+ }
}
}
@@ -459,6 +487,7 @@ private void RunClause(Action clause, object dollarUnderbar, ob
{
ExecutionContext.CheckStackDepth();
+ _anyClauseExecuted = true;
Pipe oldErrorOutputPipe = this.Context.ShellFunctionErrorOutputPipe;
// If the script block has a different language mode than the current,
@@ -553,7 +582,7 @@ private void RunClause(Action clause, object dollarUnderbar, ob
}
finally
{
- this.Context.RestoreErrorPipe(oldErrorOutputPipe);
+ Context.ShellFunctionErrorOutputPipe = oldErrorOutputPipe;
if (oldLanguageMode.HasValue)
{
@@ -584,15 +613,12 @@ private void RunClause(Action clause, object dollarUnderbar, ob
}
catch (RuntimeException e)
{
- ManageScriptException(e); // always throws
- // This quiets the compiler which wants to see a return value
- // in all codepaths.
- throw;
+ // This method always throws.
+ ManageScriptException(e);
}
catch (Exception e)
{
- // This cmdlet threw an exception, so
- // wrap it and bubble it up.
+ // This cmdlet threw an exception, so wrap it and bubble it up.
throw ManageInvocationException(e);
}
}
diff --git a/src/System.Management.Automation/engine/debugger/Breakpoint.cs b/src/System.Management.Automation/engine/debugger/Breakpoint.cs
index 5daf3bdc18b6..e51d79f4afa8 100644
--- a/src/System.Management.Automation/engine/debugger/Breakpoint.cs
+++ b/src/System.Management.Automation/engine/debugger/Breakpoint.cs
@@ -531,15 +531,16 @@ internal bool TrySetBreakpoint(string scriptFile, FunctionContext functionContex
// Not found. First, we check if the line/column is before any real code. If so, we'll
// move the breakpoint to the first interesting sequence point (could be a dynamicparam,
- // begin, process, or end block.)
+ // begin, process, end, or clean block.)
if (scriptBlock != null)
{
var ast = scriptBlock.Ast;
var bodyAst = ((IParameterMetadataProvider)ast).Body;
- if ((bodyAst.DynamicParamBlock == null || bodyAst.DynamicParamBlock.Extent.IsAfter(Line, Column)) &&
- (bodyAst.BeginBlock == null || bodyAst.BeginBlock.Extent.IsAfter(Line, Column)) &&
- (bodyAst.ProcessBlock == null || bodyAst.ProcessBlock.Extent.IsAfter(Line, Column)) &&
- (bodyAst.EndBlock == null || bodyAst.EndBlock.Extent.IsAfter(Line, Column)))
+ if ((bodyAst.DynamicParamBlock == null || bodyAst.DynamicParamBlock.Extent.IsAfter(Line, Column))
+ && (bodyAst.BeginBlock == null || bodyAst.BeginBlock.Extent.IsAfter(Line, Column))
+ && (bodyAst.ProcessBlock == null || bodyAst.ProcessBlock.Extent.IsAfter(Line, Column))
+ && (bodyAst.EndBlock == null || bodyAst.EndBlock.Extent.IsAfter(Line, Column))
+ && (bodyAst.CleanBlock == null || bodyAst.CleanBlock.Extent.IsAfter(Line, Column)))
{
SetBreakpoint(functionContext, 0);
return true;
diff --git a/src/System.Management.Automation/engine/hostifaces/Pipeline.cs b/src/System.Management.Automation/engine/hostifaces/Pipeline.cs
index 874691852cbe..c7b962b7668e 100644
--- a/src/System.Management.Automation/engine/hostifaces/Pipeline.cs
+++ b/src/System.Management.Automation/engine/hostifaces/Pipeline.cs
@@ -445,7 +445,7 @@ internal void SetHadErrors(bool status)
///
/// This flag is used to force the redirection. By default it is false to maintain compatibility with
/// V1, but the V2 hosting interface (PowerShell class) sets this flag to true to ensure the global
- /// error output pipe is always set and $ErrorActionPreference when invoking the Pipeline.
+ /// error output pipe is always set and $ErrorActionPreference is checked when invoking the Pipeline.
///
internal bool RedirectShellErrorOutputPipe { get; set; } = false;
diff --git a/src/System.Management.Automation/engine/lang/scriptblock.cs b/src/System.Management.Automation/engine/lang/scriptblock.cs
index 0c6c1d5dff1a..07f53c132ffe 100644
--- a/src/System.Management.Automation/engine/lang/scriptblock.cs
+++ b/src/System.Management.Automation/engine/lang/scriptblock.cs
@@ -1280,7 +1280,42 @@ public Array End()
{
// then pop this pipeline and dispose it...
_context.PopPipelineProcessor(true);
- _pipeline.Dispose();
+ Dispose();
+ }
+ }
+
+ ///
+ /// Clean resources for script commands of this steppable pipeline.
+ ///
+ ///
+ /// The way we handle 'Clean' blocks in a steppable pipeline makes sure that:
+ /// 1. The 'Clean' blocks get to run if any exception is thrown from 'Begin/Process/End'.
+ /// 2. The 'Clean' blocks get to run if 'End' finished successfully.
+ /// However, this is not enough for a steppable pipeline, because the function, where the steppable
+ /// pipeline gets used, may fail (think about a proxy function). And that may lead to the situation
+ /// where "no exception was thrown from the steppable pipeline" but "the steppable pipeline didn't
+ /// run to the end". In that case, 'Clean' won't run unless it's triggered explicitly on the steppable
+ /// pipeline. This method allows a user to do that from the 'Clean' block of the proxy function.
+ ///
+ public void Clean()
+ {
+ if (_pipeline.Commands is null)
+ {
+ // The pipeline commands have been disposed. In this case, 'Clean'
+ // should have already been called on the pipeline processor.
+ return;
+ }
+
+ try
+ {
+ _context.PushPipelineProcessor(_pipeline);
+ _pipeline.DoCleanup();
+ }
+ finally
+ {
+ // then pop this pipeline and dispose it...
+ _context.PopPipelineProcessor(true);
+ Dispose();
}
}
@@ -1293,23 +1328,13 @@ public Array End()
/// When this object is disposed, the contained pipeline should also be disposed.
///
public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
- if (disposing)
- {
- _pipeline.Dispose();
- }
-
+ _pipeline.Dispose();
_disposed = true;
}
diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs
index bcddbee0b242..9956fb29e306 100644
--- a/src/System.Management.Automation/engine/parser/Compiler.cs
+++ b/src/System.Management.Automation/engine/parser/Compiler.cs
@@ -2026,6 +2026,7 @@ internal void Compile(CompiledScriptBlockData scriptBlock, bool optimize)
scriptBlock.BeginBlock = CompileTree(_beginBlockLambda, compileInterpretChoice);
scriptBlock.ProcessBlock = CompileTree(_processBlockLambda, compileInterpretChoice);
scriptBlock.EndBlock = CompileTree(_endBlockLambda, compileInterpretChoice);
+ scriptBlock.CleanBlock = CompileTree(_cleanBlockLambda, compileInterpretChoice);
scriptBlock.LocalsMutableTupleType = LocalVariablesTupleType;
scriptBlock.LocalsMutableTupleCreator = MutableTuple.TupleCreator(LocalVariablesTupleType);
scriptBlock.NameToIndexMap = nameToIndexMap;
@@ -2036,6 +2037,7 @@ internal void Compile(CompiledScriptBlockData scriptBlock, bool optimize)
scriptBlock.UnoptimizedBeginBlock = CompileTree(_beginBlockLambda, compileInterpretChoice);
scriptBlock.UnoptimizedProcessBlock = CompileTree(_processBlockLambda, compileInterpretChoice);
scriptBlock.UnoptimizedEndBlock = CompileTree(_endBlockLambda, compileInterpretChoice);
+ scriptBlock.UnoptimizedCleanBlock = CompileTree(_cleanBlockLambda, compileInterpretChoice);
scriptBlock.UnoptimizedLocalsMutableTupleType = LocalVariablesTupleType;
scriptBlock.UnoptimizedLocalsMutableTupleCreator = MutableTuple.TupleCreator(LocalVariablesTupleType);
}
@@ -2221,6 +2223,7 @@ internal LoopGotoTargets(string label, LabelTarget breakLabel, LabelTarget conti
private Expression> _beginBlockLambda;
private Expression> _processBlockLambda;
private Expression> _endBlockLambda;
+ private Expression> _cleanBlockLambda;
private readonly List _loopTargets = new List();
private bool _generatingWhileOrDoLoop;
@@ -2463,6 +2466,13 @@ public object VisitScriptBlock(ScriptBlockAst scriptBlockAst)
}
_endBlockLambda = CompileNamedBlock(scriptBlockAst.EndBlock, funcName, rootForDefiningTypesAndUsings);
+ rootForDefiningTypesAndUsings = null;
+ }
+
+ if (scriptBlockAst.CleanBlock != null)
+ {
+ _cleanBlockLambda = CompileNamedBlock(scriptBlockAst.CleanBlock, funcName + "", rootForDefiningTypesAndUsings);
+ rootForDefiningTypesAndUsings = null;
}
return null;
diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs
index 3afc55e98613..c5f9d81f4b84 100644
--- a/src/System.Management.Automation/engine/parser/Parser.cs
+++ b/src/System.Management.Automation/engine/parser/Parser.cs
@@ -724,12 +724,13 @@ internal static bool TryParseAsConstantHashtable(string input, out Hashtable res
ParseError[] parseErrors;
var ast = Parser.ParseInput(input, out throwAwayTokens, out parseErrors);
- if ((ast == null) ||
- parseErrors.Length > 0 ||
- ast.BeginBlock != null ||
- ast.ProcessBlock != null ||
- ast.DynamicParamBlock != null ||
- ast.EndBlock.Traps != null)
+ if (ast == null
+ || parseErrors.Length > 0
+ || ast.BeginBlock != null
+ || ast.ProcessBlock != null
+ || ast.CleanBlock != null
+ || ast.DynamicParamBlock != null
+ || ast.EndBlock.Traps != null)
{
return false;
}
@@ -1713,9 +1714,9 @@ private ScriptBlockAst NamedBlockListRule(Token lCurly, List
NamedBlockAst beginBlock = null;
NamedBlockAst processBlock = null;
NamedBlockAst endBlock = null;
- IScriptExtent startExtent = lCurly != null
- ? lCurly.Extent
- : paramBlockAst?.Extent;
+ NamedBlockAst cleanBlock = null;
+
+ IScriptExtent startExtent = lCurly?.Extent ?? paramBlockAst?.Extent;
IScriptExtent endExtent = null;
IScriptExtent extent = null;
IScriptExtent scriptBlockExtent = null;
@@ -1757,6 +1758,7 @@ private ScriptBlockAst NamedBlockListRule(Token lCurly, List
case TokenKind.Begin:
case TokenKind.Process:
case TokenKind.End:
+ case TokenKind.Clean:
break;
}
@@ -1797,6 +1799,10 @@ private ScriptBlockAst NamedBlockListRule(Token lCurly, List
{
endBlock = new NamedBlockAst(extent, TokenKind.End, statementBlock, false);
}
+ else if (blockNameToken.Kind == TokenKind.Clean && cleanBlock == null)
+ {
+ cleanBlock = new NamedBlockAst(extent, TokenKind.Clean, statementBlock, false);
+ }
else if (blockNameToken.Kind == TokenKind.Dynamicparam && dynamicParamBlock == null)
{
dynamicParamBlock = new NamedBlockAst(extent, TokenKind.Dynamicparam, statementBlock, false);
@@ -1818,7 +1824,14 @@ private ScriptBlockAst NamedBlockListRule(Token lCurly, List
CompleteScriptBlockBody(lCurly, ref extent, out scriptBlockExtent);
return_script_block_ast:
- return new ScriptBlockAst(scriptBlockExtent, usingStatements, paramBlockAst, beginBlock, processBlock, endBlock,
+ return new ScriptBlockAst(
+ scriptBlockExtent,
+ usingStatements,
+ paramBlockAst,
+ beginBlock,
+ processBlock,
+ endBlock,
+ cleanBlock,
dynamicParamBlock);
}
diff --git a/src/System.Management.Automation/engine/parser/SemanticChecks.cs b/src/System.Management.Automation/engine/parser/SemanticChecks.cs
index 927b2c33ae8b..b792530193bc 100644
--- a/src/System.Management.Automation/engine/parser/SemanticChecks.cs
+++ b/src/System.Management.Automation/engine/parser/SemanticChecks.cs
@@ -406,10 +406,11 @@ public override AstVisitAction VisitFunctionMember(FunctionMemberAst functionMem
ParserStrings.ParamBlockNotAllowedInMethod);
}
- if (body.BeginBlock != null ||
- body.ProcessBlock != null ||
- body.DynamicParamBlock != null ||
- !body.EndBlock.Unnamed)
+ if (body.BeginBlock != null
+ || body.ProcessBlock != null
+ || body.CleanBlock != null
+ || body.DynamicParamBlock != null
+ || !body.EndBlock.Unnamed)
{
_parser.ReportError(Parser.ExtentFromFirstOf(body.DynamicParamBlock, body.BeginBlock, body.ProcessBlock, body.EndBlock),
nameof(ParserStrings.NamedBlockNotAllowedInMethod),
diff --git a/src/System.Management.Automation/engine/parser/VariableAnalysis.cs b/src/System.Management.Automation/engine/parser/VariableAnalysis.cs
index b38954ebfc62..09b336099cb4 100644
--- a/src/System.Management.Automation/engine/parser/VariableAnalysis.cs
+++ b/src/System.Management.Automation/engine/parser/VariableAnalysis.cs
@@ -945,25 +945,11 @@ public object VisitScriptBlock(ScriptBlockAst scriptBlockAst)
{
_currentBlock = _entryBlock;
- if (scriptBlockAst.DynamicParamBlock != null)
- {
- scriptBlockAst.DynamicParamBlock.Accept(this);
- }
-
- if (scriptBlockAst.BeginBlock != null)
- {
- scriptBlockAst.BeginBlock.Accept(this);
- }
-
- if (scriptBlockAst.ProcessBlock != null)
- {
- scriptBlockAst.ProcessBlock.Accept(this);
- }
-
- if (scriptBlockAst.EndBlock != null)
- {
- scriptBlockAst.EndBlock.Accept(this);
- }
+ scriptBlockAst.DynamicParamBlock?.Accept(this);
+ scriptBlockAst.BeginBlock?.Accept(this);
+ scriptBlockAst.ProcessBlock?.Accept(this);
+ scriptBlockAst.EndBlock?.Accept(this);
+ scriptBlockAst.CleanBlock?.Accept(this);
_currentBlock.FlowsTo(_exitBlock);
diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs
index 1f03eb68df5e..b40a8e66a1e8 100644
--- a/src/System.Management.Automation/engine/parser/ast.cs
+++ b/src/System.Management.Automation/engine/parser/ast.cs
@@ -818,6 +818,46 @@ public class ScriptBlockAst : Ast, IParameterMetadataProvider
NamedBlockAst processBlock,
NamedBlockAst endBlock,
NamedBlockAst dynamicParamBlock)
+ : this(
+ extent,
+ usingStatements,
+ attributes,
+ paramBlock,
+ beginBlock,
+ processBlock,
+ endBlock,
+ cleanBlock: null,
+ dynamicParamBlock)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// This construction uses explicitly named begin/process/end/clean blocks.
+ ///
+ /// The extent of the script block.
+ /// The list of using statments, may be null.
+ /// The set of attributes for the script block.
+ /// The ast for the param block, may be null.
+ /// The ast for the begin block, may be null.
+ /// The ast for the process block, may be null.
+ /// The ast for the end block, may be null.
+ /// The ast for the clean block, may be null.
+ /// The ast for the dynamicparam block, may be null.
+ ///
+ /// If is null.
+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "param")]
+ public ScriptBlockAst(
+ IScriptExtent extent,
+ IEnumerable usingStatements,
+ IEnumerable attributes,
+ ParamBlockAst paramBlock,
+ NamedBlockAst beginBlock,
+ NamedBlockAst processBlock,
+ NamedBlockAst endBlock,
+ NamedBlockAst cleanBlock,
+ NamedBlockAst dynamicParamBlock)
: base(extent)
{
SetUsingStatements(usingStatements);
@@ -856,6 +896,12 @@ public class ScriptBlockAst : Ast, IParameterMetadataProvider
SetParent(endBlock);
}
+ if (cleanBlock != null)
+ {
+ this.CleanBlock = cleanBlock;
+ SetParent(cleanBlock);
+ }
+
if (dynamicParamBlock != null)
{
this.DynamicParamBlock = dynamicParamBlock;
@@ -888,6 +934,35 @@ public class ScriptBlockAst : Ast, IParameterMetadataProvider
{
}
+ ///
+ /// Initializes a new instance of the class.
+ /// This construction uses explicitly named begin/process/end/clean blocks.
+ ///
+ /// The extent of the script block.
+ /// The list of using statments, may be null.
+ /// The ast for the param block, may be null.
+ /// The ast for the begin block, may be null.
+ /// The ast for the process block, may be null.
+ /// The ast for the end block, may be null.
+ /// The ast for the clean block, may be null.
+ /// The ast for the dynamicparam block, may be null.
+ ///
+ /// If is null.
+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "param")]
+ public ScriptBlockAst(
+ IScriptExtent extent,
+ IEnumerable usingStatements,
+ ParamBlockAst paramBlock,
+ NamedBlockAst beginBlock,
+ NamedBlockAst processBlock,
+ NamedBlockAst endBlock,
+ NamedBlockAst cleanBlock,
+ NamedBlockAst dynamicParamBlock)
+ : this(extent, usingStatements, null, paramBlock, beginBlock, processBlock, endBlock, cleanBlock, dynamicParamBlock)
+ {
+ }
+
///
/// Construct a ScriptBlockAst that uses explicitly named begin/process/end blocks.
///
@@ -911,6 +986,33 @@ public class ScriptBlockAst : Ast, IParameterMetadataProvider
{
}
+ ///
+ /// Initializes a new instance of the class.
+ /// This construction uses explicitly named begin/process/end/clean blocks.
+ ///
+ /// The extent of the script block.
+ /// The ast for the param block, may be null.
+ /// The ast for the begin block, may be null.
+ /// The ast for the process block, may be null.
+ /// The ast for the end block, may be null.
+ /// The ast for the clean block, may be null.
+ /// The ast for the dynamicparam block, may be null.
+ ///
+ /// If is null.
+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "param")]
+ public ScriptBlockAst(
+ IScriptExtent extent,
+ ParamBlockAst paramBlock,
+ NamedBlockAst beginBlock,
+ NamedBlockAst processBlock,
+ NamedBlockAst endBlock,
+ NamedBlockAst cleanBlock,
+ NamedBlockAst dynamicParamBlock)
+ : this(extent, null, paramBlock, beginBlock, processBlock, endBlock, cleanBlock, dynamicParamBlock)
+ {
+ }
+
///
/// Construct a ScriptBlockAst that does not use explicitly named blocks.
///
@@ -1115,6 +1217,11 @@ private void SetUsingStatements(IEnumerable usingStatements)
///
public NamedBlockAst EndBlock { get; }
+ ///
+ /// Gets the ast representing the clean block for a script block, or null if no clean block was specified.
+ ///
+ public NamedBlockAst CleanBlock { get; }
+
///
/// The ast representing the dynamicparam block for a script block, or null if no dynamicparam block was specified.
///
@@ -1194,17 +1301,25 @@ public override Ast Copy()
var newBeginBlock = CopyElement(this.BeginBlock);
var newProcessBlock = CopyElement(this.ProcessBlock);
var newEndBlock = CopyElement(this.EndBlock);
+ var newCleanBlock = CopyElement(this.CleanBlock);
var newDynamicParamBlock = CopyElement(this.DynamicParamBlock);
var newAttributes = CopyElements(this.Attributes);
var newUsingStatements = CopyElements(this.UsingStatements);
- var scriptBlockAst = new ScriptBlockAst(this.Extent, newUsingStatements, newAttributes, newParamBlock, newBeginBlock, newProcessBlock,
- newEndBlock, newDynamicParamBlock)
+ return new ScriptBlockAst(
+ this.Extent,
+ newUsingStatements,
+ newAttributes,
+ newParamBlock,
+ newBeginBlock,
+ newProcessBlock,
+ newEndBlock,
+ newCleanBlock,
+ newDynamicParamBlock)
{
IsConfiguration = this.IsConfiguration,
ScriptRequirements = this.ScriptRequirements
};
- return scriptBlockAst;
}
internal string ToStringForSerialization()
@@ -1367,17 +1482,27 @@ internal override AstVisitAction InternalVisit(AstVisitor visitor)
}
}
- if (action == AstVisitAction.Continue && ParamBlock != null)
- action = ParamBlock.InternalVisit(visitor);
- if (action == AstVisitAction.Continue && DynamicParamBlock != null)
- action = DynamicParamBlock.InternalVisit(visitor);
- if (action == AstVisitAction.Continue && BeginBlock != null)
- action = BeginBlock.InternalVisit(visitor);
- if (action == AstVisitAction.Continue && ProcessBlock != null)
- action = ProcessBlock.InternalVisit(visitor);
- if (action == AstVisitAction.Continue && EndBlock != null)
- action = EndBlock.InternalVisit(visitor);
+ if (action == AstVisitAction.Continue)
+ {
+ _ = VisitAndShallContinue(ParamBlock) &&
+ VisitAndShallContinue(DynamicParamBlock) &&
+ VisitAndShallContinue(BeginBlock) &&
+ VisitAndShallContinue(ProcessBlock) &&
+ VisitAndShallContinue(EndBlock) &&
+ VisitAndShallContinue(CleanBlock);
+ }
+
return visitor.CheckForPostAction(this, action);
+
+ bool VisitAndShallContinue(Ast ast)
+ {
+ if (ast is not null)
+ {
+ action = ast.InternalVisit(visitor);
+ }
+
+ return action == AstVisitAction.Continue;
+ }
}
#endregion Visitors
@@ -1581,9 +1706,12 @@ bool IParameterMetadataProvider.UsesCmdletBinding()
internal PipelineAst GetSimplePipeline(bool allowMultiplePipelines, out string errorId, out string errorMsg)
{
- if (BeginBlock != null || ProcessBlock != null || DynamicParamBlock != null)
+ if (BeginBlock != null
+ || ProcessBlock != null
+ || CleanBlock != null
+ || DynamicParamBlock != null)
{
- errorId = "CanConvertOneClauseOnly";
+ errorId = nameof(AutomationExceptions.CanConvertOneClauseOnly);
errorMsg = AutomationExceptions.CanConvertOneClauseOnly;
return null;
}
@@ -1749,7 +1877,7 @@ internal static bool UsesCmdletBinding(IEnumerable parameters)
public class NamedBlockAst : Ast
{
///
- /// Construct the ast for a begin, process, end, or dynamic param block.
+ /// Construct the ast for a begin, process, end, clean, or dynamic param block.
///
///
/// The extent of the block. If is false, the extent includes
@@ -1761,6 +1889,7 @@ public class NamedBlockAst : Ast
///
///
///
+ ///
///
///
///
@@ -1779,8 +1908,7 @@ public NamedBlockAst(IScriptExtent extent, TokenKind blockName, StatementBlockAs
{
// Validate the block name. If the block is unnamed, it must be an End block (for a function)
// or Process block (for a filter).
- if (!blockName.HasTrait(TokenFlags.ScriptBlockBlockName)
- || (unnamed && (blockName == TokenKind.Begin || blockName == TokenKind.Dynamicparam)))
+ if (HasInvalidBlockName(blockName, unnamed))
{
throw PSTraceSource.NewArgumentException(nameof(blockName));
}
@@ -1838,6 +1966,7 @@ public NamedBlockAst(IScriptExtent extent, TokenKind blockName, StatementBlockAs
///
///
///
+ ///
///
///
///
@@ -1877,6 +2006,14 @@ public override Ast Copy()
return new NamedBlockAst(this.Extent, this.BlockKind, statementBlock, this.Unnamed);
}
+ private static bool HasInvalidBlockName(TokenKind blockName, bool unnamed)
+ {
+ return !blockName.HasTrait(TokenFlags.ScriptBlockBlockName)
+ || (unnamed
+ && blockName != TokenKind.Process
+ && blockName != TokenKind.End);
+ }
+
// Used by the debugger for command breakpoints
internal IScriptExtent OpenCurlyExtent { get; }
diff --git a/src/System.Management.Automation/engine/parser/token.cs b/src/System.Management.Automation/engine/parser/token.cs
index 893ec9fc8e06..43a3e86ebb73 100644
--- a/src/System.Management.Automation/engine/parser/token.cs
+++ b/src/System.Management.Automation/engine/parser/token.cs
@@ -588,6 +588,9 @@ public enum TokenKind
/// The 'default' keyword
Default = 169,
+ /// The 'clean' keyword.
+ Clean = 170,
+
#endregion Keywords
}
@@ -659,7 +662,7 @@ public enum TokenFlags
Keyword = 0x00000010,
///
- /// The token one of the keywords that is a part of a script block: 'begin', 'process', 'end', or 'dynamicparam'.
+ /// The token is one of the keywords that is a part of a script block: 'begin', 'process', 'end', 'clean', or 'dynamicparam'.
///
ScriptBlockBlockName = 0x00000020,
@@ -948,6 +951,7 @@ public static class TokenTraits
/* Hidden */ TokenFlags.Keyword,
/* Base */ TokenFlags.Keyword,
/* Default */ TokenFlags.Keyword,
+ /* Clean */ TokenFlags.Keyword | TokenFlags.ScriptBlockBlockName,
#endregion Flags for keywords
};
@@ -1147,6 +1151,7 @@ public static class TokenTraits
/* Hidden */ "hidden",
/* Base */ "base",
/* Default */ "default",
+ /* Clean */ "clean",
#endregion Text for keywords
};
@@ -1154,10 +1159,12 @@ public static class TokenTraits
#if DEBUG
static TokenTraits()
{
- Diagnostics.Assert(s_staticTokenFlags.Length == ((int)TokenKind.Default + 1),
- "Table size out of sync with enum - _staticTokenFlags");
- Diagnostics.Assert(s_tokenText.Length == ((int)TokenKind.Default + 1),
- "Table size out of sync with enum - _tokenText");
+ Diagnostics.Assert(
+ s_staticTokenFlags.Length == ((int)TokenKind.Clean + 1),
+ "Table size out of sync with enum - _staticTokenFlags");
+ Diagnostics.Assert(
+ s_tokenText.Length == ((int)TokenKind.Clean + 1),
+ "Table size out of sync with enum - _tokenText");
// Some random assertions to make sure the enum and the traits are in sync
Diagnostics.Assert(GetTraits(TokenKind.Begin) == (TokenFlags.Keyword | TokenFlags.ScriptBlockBlockName),
"Table out of sync with enum - flags Begin");
diff --git a/src/System.Management.Automation/engine/parser/tokenizer.cs b/src/System.Management.Automation/engine/parser/tokenizer.cs
index 161e53cef0c7..43046c9d29e6 100644
--- a/src/System.Management.Automation/engine/parser/tokenizer.cs
+++ b/src/System.Management.Automation/engine/parser/tokenizer.cs
@@ -635,23 +635,23 @@ internal class Tokenizer
/*A*/ "configuration", "public", "private", "static", /*A*/
/*B*/ "interface", "enum", "namespace", "module", /*B*/
/*C*/ "type", "assembly", "command", "hidden", /*C*/
- /*D*/ "base", "default", /*D*/
+ /*D*/ "base", "default", "clean", /*D*/
};
private static readonly TokenKind[] s_keywordTokenKind = new TokenKind[] {
- /*1*/ TokenKind.ElseIf, TokenKind.If, TokenKind.Else, TokenKind.Switch, /*1*/
- /*2*/ TokenKind.Foreach, TokenKind.From, TokenKind.In, TokenKind.For, /*2*/
- /*3*/ TokenKind.While, TokenKind.Until, TokenKind.Do, TokenKind.Try, /*3*/
- /*4*/ TokenKind.Catch, TokenKind.Finally, TokenKind.Trap, TokenKind.Data, /*4*/
- /*5*/ TokenKind.Return, TokenKind.Continue, TokenKind.Break, TokenKind.Exit, /*5*/
- /*6*/ TokenKind.Throw, TokenKind.Begin, TokenKind.Process, TokenKind.End, /*6*/
- /*7*/ TokenKind.Dynamicparam, TokenKind.Function, TokenKind.Filter, TokenKind.Param, /*7*/
- /*8*/ TokenKind.Class, TokenKind.Define, TokenKind.Var, TokenKind.Using, /*8*/
- /*9*/ TokenKind.Workflow, TokenKind.Parallel, TokenKind.Sequence, TokenKind.InlineScript, /*9*/
- /*A*/ TokenKind.Configuration, TokenKind.Public, TokenKind.Private, TokenKind.Static, /*A*/
- /*B*/ TokenKind.Interface, TokenKind.Enum, TokenKind.Namespace, TokenKind.Module, /*B*/
- /*C*/ TokenKind.Type, TokenKind.Assembly, TokenKind.Command, TokenKind.Hidden, /*C*/
- /*D*/ TokenKind.Base, TokenKind.Default, /*D*/
+ /*1*/ TokenKind.ElseIf, TokenKind.If, TokenKind.Else, TokenKind.Switch, /*1*/
+ /*2*/ TokenKind.Foreach, TokenKind.From, TokenKind.In, TokenKind.For, /*2*/
+ /*3*/ TokenKind.While, TokenKind.Until, TokenKind.Do, TokenKind.Try, /*3*/
+ /*4*/ TokenKind.Catch, TokenKind.Finally, TokenKind.Trap, TokenKind.Data, /*4*/
+ /*5*/ TokenKind.Return, TokenKind.Continue, TokenKind.Break, TokenKind.Exit, /*5*/
+ /*6*/ TokenKind.Throw, TokenKind.Begin, TokenKind.Process, TokenKind.End, /*6*/
+ /*7*/ TokenKind.Dynamicparam, TokenKind.Function, TokenKind.Filter, TokenKind.Param, /*7*/
+ /*8*/ TokenKind.Class, TokenKind.Define, TokenKind.Var, TokenKind.Using, /*8*/
+ /*9*/ TokenKind.Workflow, TokenKind.Parallel, TokenKind.Sequence, TokenKind.InlineScript, /*9*/
+ /*A*/ TokenKind.Configuration, TokenKind.Public, TokenKind.Private, TokenKind.Static, /*A*/
+ /*B*/ TokenKind.Interface, TokenKind.Enum, TokenKind.Namespace, TokenKind.Module, /*B*/
+ /*C*/ TokenKind.Type, TokenKind.Assembly, TokenKind.Command, TokenKind.Hidden, /*C*/
+ /*D*/ TokenKind.Base, TokenKind.Default, TokenKind.Clean, /*D*/
};
internal static readonly string[] _operatorText = new string[] {
@@ -699,8 +699,16 @@ static Tokenizer()
Diagnostics.Assert(s_keywordText.Length == s_keywordTokenKind.Length, "Keyword table sizes must match");
Diagnostics.Assert(_operatorText.Length == s_operatorTokenKind.Length, "Operator table sizes must match");
+ bool isCleanBlockFeatureEnabled = ExperimentalFeature.IsEnabled(ExperimentalFeature.PSCleanBlockFeatureName);
+
for (int i = 0; i < s_keywordText.Length; ++i)
{
+ if (!isCleanBlockFeatureEnabled && s_keywordText[i] == "clean")
+ {
+ // Skip adding the 'clean' keyword when the feature is disabled.
+ continue;
+ }
+
s_keywordTable.Add(s_keywordText[i], s_keywordTokenKind[i]);
}
diff --git a/src/System.Management.Automation/engine/pipeline.cs b/src/System.Management.Automation/engine/pipeline.cs
index 78fb19dbc653..185e60e072f3 100644
--- a/src/System.Management.Automation/engine/pipeline.cs
+++ b/src/System.Management.Automation/engine/pipeline.cs
@@ -66,7 +66,6 @@ internal class PipelineProcessor : IDisposable
public void Dispose()
{
Dispose(true);
- GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
@@ -267,9 +266,12 @@ internal int Add(CommandProcessorBase commandProcessor)
internal void AddRedirectionPipe(PipelineProcessor pipelineProcessor)
{
- if (pipelineProcessor == null) throw PSTraceSource.NewArgumentNullException(nameof(pipelineProcessor));
- if (_redirectionPipes == null)
- _redirectionPipes = new List();
+ if (pipelineProcessor is null)
+ {
+ throw PSTraceSource.NewArgumentNullException(nameof(pipelineProcessor));
+ }
+
+ _redirectionPipes ??= new List();
_redirectionPipes.Add(pipelineProcessor);
}
@@ -347,18 +349,15 @@ private int AddCommand(CommandProcessorBase commandProcessor, int readFromComman
}
else
{
- CommandProcessorBase prevcommandProcessor = _commands[readFromCommand - 1] as CommandProcessorBase;
- if (prevcommandProcessor == null || prevcommandProcessor.CommandRuntime == null)
- {
- // "PipelineProcessor.AddCommand(): previous request object == null"
- throw PSTraceSource.NewInvalidOperationException();
- }
+ var prevcommandProcessor = _commands[readFromCommand - 1] as CommandProcessorBase;
+ ValidateCommandProcessorNotNull(prevcommandProcessor, errorMessage: null);
+
+ Pipe UpstreamPipe = (readErrorQueue)
+ ? prevcommandProcessor.CommandRuntime.ErrorOutputPipe
+ : prevcommandProcessor.CommandRuntime.OutputPipe;
- Pipe UpstreamPipe = (readErrorQueue) ?
- prevcommandProcessor.CommandRuntime.ErrorOutputPipe : prevcommandProcessor.CommandRuntime.OutputPipe;
if (UpstreamPipe == null)
{
- // "PipelineProcessor.AddCommand(): UpstreamPipe == null"
throw PSTraceSource.NewInvalidOperationException();
}
@@ -381,11 +380,8 @@ private int AddCommand(CommandProcessorBase commandProcessor, int readFromComman
for (int i = 0; i < _commands.Count; i++)
{
prevcommandProcessor = _commands[i];
- if (prevcommandProcessor == null || prevcommandProcessor.CommandRuntime == null)
- {
- // "PipelineProcessor.AddCommand(): previous request object == null"
- throw PSTraceSource.NewInvalidOperationException();
- }
+ ValidateCommandProcessorNotNull(prevcommandProcessor, errorMessage: null);
+
// check whether the error output is already claimed
if (prevcommandProcessor.CommandRuntime.ErrorOutputPipe.DownstreamCmdlet != null)
continue;
@@ -470,194 +466,305 @@ internal Array SynchronousExecuteEnumerate(object input)
throw new PipelineStoppedException();
}
- ExceptionDispatchInfo toRethrowInfo;
+ bool pipelineSucceeded = false;
+ ExceptionDispatchInfo toRethrowInfo = null;
+ CommandProcessorBase commandRequestingUpstreamCommandsToStop = null;
+
try
{
- CommandProcessorBase commandRequestingUpstreamCommandsToStop = null;
try
{
- // If the caller specified an input object array,
- // we run assuming there is an incoming "stream"
- // of objects. This will prevent the one default call
- // to ProcessRecord on the first command.
- Start(input != AutomationNull.Value);
+ try
+ {
+ // If the caller specified an input object array, we run assuming there is an incoming "stream"
+ // of objects. This will prevent the one default call to ProcessRecord on the first command.
+ Start(incomingStream: input != AutomationNull.Value);
- // Start has already validated firstcommandProcessor
- CommandProcessorBase firstCommandProcessor = _commands[0];
+ // Start has already validated firstcommandProcessor
+ CommandProcessorBase firstCommandProcessor = _commands[0];
- // Add any input to the first command.
- if (ExternalInput != null)
+ // Add any input to the first command.
+ if (ExternalInput is not null)
+ {
+ firstCommandProcessor.CommandRuntime.InputPipe.ExternalReader = ExternalInput;
+ }
+
+ Inject(input, enumerate: true);
+ }
+ catch (PipelineStoppedException)
{
- firstCommandProcessor.CommandRuntime.InputPipe.ExternalReader
- = ExternalInput;
+ if (_firstTerminatingError?.SourceException is StopUpstreamCommandsException exception)
+ {
+ _firstTerminatingError = null;
+ commandRequestingUpstreamCommandsToStop = exception.RequestingCommandProcessor;
+ }
+ else
+ {
+ throw;
+ }
}
- Inject(input, enumerate: true);
+ DoCompleteCore(commandRequestingUpstreamCommandsToStop);
+ pipelineSucceeded = true;
}
- catch (PipelineStoppedException)
+ finally
{
- StopUpstreamCommandsException stopUpstreamCommandsException =
- _firstTerminatingError != null
- ? _firstTerminatingError.SourceException as StopUpstreamCommandsException
- : null;
- if (stopUpstreamCommandsException == null)
- {
- throw;
- }
- else
- {
- _firstTerminatingError = null;
- commandRequestingUpstreamCommandsToStop = stopUpstreamCommandsException.RequestingCommandProcessor;
- }
+ // Clean up resources for script commands, no matter the pipeline succeeded or not.
+ // This method catches and handles all exceptions inside, so it will never throw.
+ Clean();
}
- DoCompleteCore(commandRequestingUpstreamCommandsToStop);
-
- // By this point, we are sure all commandProcessors hosted by the current pipelineProcess are done execution,
- // so if there are any redirection pipelineProcessors associated with any of those commandProcessors, we should
- // call DoComplete on them.
- if (_redirectionPipes != null)
+ if (pipelineSucceeded)
{
- foreach (PipelineProcessor redirectPipelineProcessor in _redirectionPipes)
+ // Now, we are sure all 'commandProcessors' hosted by the current 'pipelineProcessor' are done execution,
+ // so if there are any redirection 'pipelineProcessors' associated with any of those 'commandProcessors',
+ // they must have successfully executed 'StartStepping' and 'Step', and thus we should call 'DoComplete'
+ // on them for completeness.
+ if (_redirectionPipes is not null)
{
- redirectPipelineProcessor.DoCompleteCore(null);
+ foreach (PipelineProcessor redirectPipelineProcessor in _redirectionPipes)
+ {
+ // The 'Clean' block for each 'commandProcessor' might still write to a pipe that is associated
+ // with the redirection 'pipelineProcessor' (e.g. a redirected error pipe), which would trigger
+ // the call to 'pipelineProcessor.Step'.
+ // It's possible (though very unlikely) that the call to 'pipelineProcessor.Step' failed with an
+ // exception, and in such case, the 'pipelineProcessor' would have been disposed, and therefore
+ // the call to 'DoComplete' will simply return, because '_commands' was already set to null.
+ redirectPipelineProcessor.DoCompleteCore(null);
+ }
}
- }
- return RetrieveResults();
+ // The 'Clean' blocks write nothing to the output pipe, so the results won't be affected by them.
+ return RetrieveResults();
+ }
}
catch (RuntimeException e)
{
- // The error we want to report is the first terminating error
- // which occurred during pipeline execution, regardless
- // of whether other errors occurred afterward.
- toRethrowInfo = _firstTerminatingError ?? ExceptionDispatchInfo.Capture(e);
- this.LogExecutionException(toRethrowInfo.SourceException);
- }
- // NTRAID#Windows Out Of Band Releases-929020-2006/03/14-JonN
- catch (System.Runtime.InteropServices.InvalidComObjectException comException)
- {
- // The error we want to report is the first terminating error
- // which occurred during pipeline execution, regardless
- // of whether other errors occurred afterward.
- if (_firstTerminatingError != null)
- {
- toRethrowInfo = _firstTerminatingError;
- }
- else
- {
- string message = StringUtil.Format(ParserStrings.InvalidComObjectException, comException.Message);
- var rte = new RuntimeException(message, comException);
- rte.SetErrorId("InvalidComObjectException");
- toRethrowInfo = ExceptionDispatchInfo.Capture(rte);
- }
-
- this.LogExecutionException(toRethrowInfo.SourceException);
+ toRethrowInfo = GetFirstError(e);
}
finally
{
DisposeCommands();
}
- // By rethrowing the exception outside of the handler,
- // we allow the CLR on X64/IA64 to free from the stack
- // the exception records related to this exception.
+ // By rethrowing the exception outside of the handler, we allow the CLR on X64/IA64 to free from
+ // the stack the exception records related to this exception.
- // The only reason we should get here is if
- // an exception should be rethrown.
+ // The only reason we should get here is if an exception should be rethrown.
Diagnostics.Assert(toRethrowInfo != null, "Alternate protocol path failure");
toRethrowInfo.Throw();
- return null; // UNREACHABLE
+
+ // UNREACHABLE
+ return null;
+ }
+
+ private ExceptionDispatchInfo GetFirstError(RuntimeException e)
+ {
+ // The error we want to report is the first terminating error which occurred during pipeline execution,
+ // regardless of whether other errors occurred afterward.
+ var firstError = _firstTerminatingError ?? ExceptionDispatchInfo.Capture(e);
+ LogExecutionException(firstError.SourceException);
+ return firstError;
+ }
+
+ private void ThrowFirstErrorIfExisting(bool logException)
+ {
+ if (_firstTerminatingError != null)
+ {
+ if (logException)
+ {
+ LogExecutionException(_firstTerminatingError.SourceException);
+ }
+
+ _firstTerminatingError.Throw();
+ }
}
private void DoCompleteCore(CommandProcessorBase commandRequestingUpstreamCommandsToStop)
{
- // Call DoComplete() for all the commands. DoComplete() will internally call Complete()
+ if (_commands is null)
+ {
+ // This could happen to a redirection pipeline, either for an expression (e.g. 1 > a.txt)
+ // or for a command (e.g. command > a.txt).
+ // An exception may be thrown from the call to 'StartStepping' or 'Step' on the pipeline,
+ // which causes the pipeline commands to be disposed.
+ return;
+ }
+
+ // Call DoComplete() for all the commands, which will internally call Complete()
MshCommandRuntime lastCommandRuntime = null;
- if (_commands != null)
+ for (int i = 0; i < _commands.Count; i++)
{
- for (int i = 0; i < _commands.Count; i++)
- {
- CommandProcessorBase commandProcessor = _commands[i];
+ CommandProcessorBase commandProcessor = _commands[i];
- if (commandProcessor == null)
- {
- // "null command " + i
- throw PSTraceSource.NewInvalidOperationException();
- }
+ if (commandProcessor is null)
+ {
+ // An internal error that should not happen.
+ throw PSTraceSource.NewInvalidOperationException();
+ }
- if (object.ReferenceEquals(commandRequestingUpstreamCommandsToStop, commandProcessor))
- {
- commandRequestingUpstreamCommandsToStop = null;
- continue; // do not call DoComplete/EndProcessing on the command that initiated stopping
- }
+ if (object.ReferenceEquals(commandRequestingUpstreamCommandsToStop, commandProcessor))
+ {
+ // Do not call DoComplete/EndProcessing on the command that initiated stopping.
+ commandRequestingUpstreamCommandsToStop = null;
+ continue;
+ }
- if (commandRequestingUpstreamCommandsToStop != null)
- {
- continue; // do not call DoComplete/EndProcessing on commands that were stopped upstream
- }
+ if (commandRequestingUpstreamCommandsToStop is not null)
+ {
+ // Do not call DoComplete/EndProcessing on commands that were stopped upstream.
+ continue;
+ }
- try
+ try
+ {
+ commandProcessor.DoComplete();
+ }
+ catch (PipelineStoppedException)
+ {
+ if (_firstTerminatingError?.SourceException is StopUpstreamCommandsException exception)
{
- commandProcessor.DoComplete();
+ _firstTerminatingError = null;
+ commandRequestingUpstreamCommandsToStop = exception.RequestingCommandProcessor;
}
- catch (PipelineStoppedException)
+ else
{
- StopUpstreamCommandsException stopUpstreamCommandsException =
- _firstTerminatingError != null
- ? _firstTerminatingError.SourceException as StopUpstreamCommandsException
- : null;
- if (stopUpstreamCommandsException == null)
- {
- throw;
- }
- else
- {
- _firstTerminatingError = null;
- commandRequestingUpstreamCommandsToStop = stopUpstreamCommandsException.RequestingCommandProcessor;
- }
+ throw;
}
+ }
- EtwActivity.SetActivityId(commandProcessor.PipelineActivityId);
-
- // Log a command stopped event
- MshLog.LogCommandLifecycleEvent(
- commandProcessor.Command.Context,
- CommandState.Stopped,
- commandProcessor.Command.MyInvocation);
+ EtwActivity.SetActivityId(commandProcessor.PipelineActivityId);
- // Log the execution of a command (not script chunks, as they
- // are not commands in and of themselves)
- if (commandProcessor.CommandInfo.CommandType != CommandTypes.Script)
- {
- commandProcessor.CommandRuntime.PipelineProcessor.LogExecutionComplete(
- commandProcessor.Command.MyInvocation, commandProcessor.CommandInfo.Name);
- }
+ // Log a command stopped event
+ MshLog.LogCommandLifecycleEvent(
+ commandProcessor.Command.Context,
+ CommandState.Stopped,
+ commandProcessor.Command.MyInvocation);
- lastCommandRuntime = commandProcessor.CommandRuntime;
+ // Log the execution of a command (not script chunks, as they are not commands in and of themselves).
+ if (commandProcessor.CommandInfo.CommandType != CommandTypes.Script)
+ {
+ LogExecutionComplete(commandProcessor.Command.MyInvocation, commandProcessor.CommandInfo.Name);
}
+
+ lastCommandRuntime = commandProcessor.CommandRuntime;
}
// Log the pipeline completion.
- if (lastCommandRuntime != null)
+ if (lastCommandRuntime is not null)
{
// Only log the pipeline completion if this wasn't a nested pipeline, as
// pipeline state in transcription is associated with the toplevel pipeline
- if ((this.LocalPipeline == null) || (!this.LocalPipeline.IsNested))
+ if (LocalPipeline is null || !LocalPipeline.IsNested)
{
lastCommandRuntime.PipelineProcessor.LogPipelineComplete();
}
}
// If a terminating error occurred, report it now.
- if (_firstTerminatingError != null)
+ // This pipeline could have been stopped asynchronously, by 'Ctrl+c' manually or
+ // 'PowerShell.Stop' programatically. We need to check and see if that's the case.
+ // An example:
+ // - 'Start-Sleep' is running in this pipeline, and 'pipelineProcessor.Stop' gets
+ // called on a different thread, which sets a 'PipelineStoppedException' object
+ // to '_firstTerminatingError' and runs 'StopProcessing' on 'Start-Sleep'.
+ // - The 'StopProcessing' will cause 'Start-Sleep' to return from 'ProcessRecord'
+ // call, and thus the pipeline execution will move forward to run 'DoComplete'
+ // for the 'Start-Sleep' command and thus the code flow will reach here.
+ // For this given example, we need to check '_firstTerminatingError' and throw out
+ // the 'PipelineStoppedException' if the pipeline was indeed being stopped.
+ ThrowFirstErrorIfExisting(logException: true);
+ }
+
+ ///
+ /// Clean up resources for script commands in this pipeline processor.
+ ///
+ ///
+ /// Exception from a 'Clean' block is not allowed to propagate up and terminate the pipeline
+ /// so that other 'Clean' blocks can run without being affected. Therefore, this method will
+ /// catch and handle all exceptions inside, and it will never throw.
+ ///
+ private void Clean()
+ {
+ if (!_executionStarted || _commands is null)
{
- this.LogExecutionException(_firstTerminatingError.SourceException);
- _firstTerminatingError.Throw();
+ // Simply return if the pipeline execution wasn't even started, or the commands of
+ // the pipeline have already been disposed.
+ return;
+ }
+
+ // So far, if '_firstTerminatingError' is not null, then it must be a terminating error
+ // thrown from one of 'Begin/Process/End' blocks. There can be terminating error thrown
+ // from 'Clean' block as well, which needs to be handled in this method.
+ // In order to capture the subsequent first terminating error thrown from 'Clean', we
+ // need to forget the previous '_firstTerminatingError' value before calling 'DoClean'
+ // on each command processor, so we have to save the old value here and restore later.
+ ExceptionDispatchInfo oldFirstTerminatingError = _firstTerminatingError;
+
+ // Suspend a stopping pipeline by setting 'IsStopping' to false and restore it afterwards.
+ bool oldIsStopping = ExceptionHandlingOps.SuspendStoppingPipelineImpl(LocalPipeline);
+
+ try
+ {
+ foreach (CommandProcessorBase commandProcessor in _commands)
+ {
+ if (commandProcessor is null || !commandProcessor.HasCleanBlock)
+ {
+ continue;
+ }
+
+ try
+ {
+ // Forget the terminating error we saw before, so a terminating error thrown
+ // from the subsequent 'Clean' block can be recorded and handled properly.
+ _firstTerminatingError = null;
+ commandProcessor.DoCleanup();
+ }
+ catch (RuntimeException e)
+ {
+ // Retrieve and report the terminating error that was thrown in the 'Clean' block.
+ ExceptionDispatchInfo firstError = GetFirstError(e);
+ commandProcessor.ReportCleanupError(firstError.SourceException);
+ }
+ catch (Exception ex)
+ {
+ // Theoretically, only 'RuntimeException' could be thrown out, but we catch
+ // all and log them here just to be safe.
+ // Skip special flow control exceptions and log others.
+ if (ex is not FlowControlException && ex is not HaltCommandException)
+ {
+ MshLog.LogCommandHealthEvent(commandProcessor.Context, ex, Severity.Warning);
+ }
+ }
+ }
+ }
+ finally
+ {
+ _firstTerminatingError = oldFirstTerminatingError;
+ ExceptionHandlingOps.RestoreStoppingPipelineImpl(LocalPipeline, oldIsStopping);
}
}
+ ///
+ /// Clean up resources for the script commands of a steppable pipeline.
+ ///
+ ///
+ /// The way we handle 'Clean' blocks in 'StartStepping', 'Step', and 'DoComplete' makes sure that:
+ /// 1. The 'Clean' blocks get to run if any exception is thrown from the pipeline execution.
+ /// 2. The 'Clean' blocks get to run if the pipeline runs to the end successfully.
+ /// However, this is not enough for a steppable pipeline, because the function, where the steppable
+ /// pipeline gets used, may fail (think about a proxy function). And that may lead to the situation
+ /// where "no exception was thrown from the steppable pipeline" but "the steppable pipeline didn't
+ /// run to the end". In that case, 'Clean' won't run unless it's triggered explicitly on the steppable
+ /// pipeline. This method is how we will expose this functionality to 'SteppablePipeline'.
+ ///
+ internal void DoCleanup()
+ {
+ Clean();
+ DisposeCommands();
+ }
+
///
/// Implements DoComplete as a stand-alone function for completing
/// the execution of a steppable pipeline.
@@ -665,98 +772,79 @@ private void DoCompleteCore(CommandProcessorBase commandRequestingUpstreamComman
/// The results of the execution.
internal Array DoComplete()
{
- if (Stopping)
- {
- throw new PipelineStoppedException();
- }
-
if (!_executionStarted)
{
throw PSTraceSource.NewInvalidOperationException(
PipelineStrings.PipelineNotStarted);
}
- ExceptionDispatchInfo toRethrowInfo;
try
{
- DoCompleteCore(null);
+ if (Stopping)
+ {
+ throw new PipelineStoppedException();
+ }
- return RetrieveResults();
- }
- catch (RuntimeException e)
- {
- // The error we want to report is the first terminating error
- // which occurred during pipeline execution, regardless
- // of whether other errors occurred afterward.
- toRethrowInfo = _firstTerminatingError ?? ExceptionDispatchInfo.Capture(e);
- this.LogExecutionException(toRethrowInfo.SourceException);
- }
- // NTRAID#Windows Out Of Band Releases-929020-2006/03/14-JonN
- catch (System.Runtime.InteropServices.InvalidComObjectException comException)
- {
- // The error we want to report is the first terminating error
- // which occurred during pipeline execution, regardless
- // of whether other errors occurred afterward.
- if (_firstTerminatingError != null)
+ ExceptionDispatchInfo toRethrowInfo;
+ try
{
- toRethrowInfo = _firstTerminatingError;
+ DoCompleteCore(null);
+ return RetrieveResults();
}
- else
+ catch (RuntimeException e)
{
- string message = StringUtil.Format(ParserStrings.InvalidComObjectException, comException.Message);
- var rte = new RuntimeException(message, comException);
- rte.SetErrorId("InvalidComObjectException");
- toRethrowInfo = ExceptionDispatchInfo.Capture(rte);
+ toRethrowInfo = GetFirstError(e);
}
- this.LogExecutionException(toRethrowInfo.SourceException);
+ // By rethrowing the exception outside of the handler, we allow the CLR on X64/IA64 to free from the stack
+ // the exception records related to this exception.
+
+ // The only reason we should get here is an exception should be rethrown.
+ Diagnostics.Assert(toRethrowInfo != null, "Alternate protocol path failure");
+ toRethrowInfo.Throw();
+
+ // UNREACHABLE
+ return null;
}
finally
{
+ Clean();
DisposeCommands();
}
-
- // By rethrowing the exception outside of the handler,
- // we allow the CLR on X64/IA64 to free from the stack
- // the exception records related to this exception.
-
- // The only reason we should get here is if
- // an exception should be rethrown.
- Diagnostics.Assert(toRethrowInfo != null, "Alternate protocol path failure");
- toRethrowInfo.Throw();
- return null; // UNREACHABLE
}
///
- /// This routine starts the stepping process. It is optional to
- /// call this but can be useful if you want the begin clauses
- /// of the pipeline to be run even when there may not be any input
- /// to process as is the case for I/O redirection into a file. We
- /// still want the file opened, even if there was nothing to write to it.
+ /// This routine starts the stepping process. It is optional to call this but can be useful
+ /// if you want the begin clauses of the pipeline to be run even when there may not be any
+ /// input to process as is the case for I/O redirection into a file. We still want the file
+ /// opened, even if there was nothing to write to it.
///
/// True if you want to write to this pipeline.
internal void StartStepping(bool expectInput)
{
+ bool startSucceeded = false;
try
{
Start(expectInput);
+ startSucceeded = true;
- // If a terminating error occurred, report it now.
- if (_firstTerminatingError != null)
- {
- _firstTerminatingError.Throw();
- }
+ // Check if this pipeline is being stopped asynchronously.
+ ThrowFirstErrorIfExisting(logException: false);
}
- catch (PipelineStoppedException)
+ catch (Exception e)
{
+ Clean();
DisposeCommands();
- // The error we want to report is the first terminating error
- // which occurred during pipeline execution, regardless
- // of whether other errors occurred afterward.
- if (_firstTerminatingError != null)
+ if (!startSucceeded && e is PipelineStoppedException)
{
- _firstTerminatingError.Throw();
+ // When a terminating error happens during command execution, PowerShell will first save it
+ // to '_firstTerminatingError', and then throw a 'PipelineStoppedException' to tear down the
+ // pipeline. So when the caught exception here is 'PipelineStoppedException', it may not be
+ // the actual original terminating error.
+ // In this case, we want to report the first terminating error which occurred during pipeline
+ // execution, regardless of whether other errors occurred afterward.
+ ThrowFirstErrorIfExisting(logException: false);
}
throw;
@@ -773,35 +861,35 @@ internal void Stop()
// Only call StopProcessing if the pipeline is being stopped
// for the first time
- if (!RecordFailure(new PipelineStoppedException(), null))
+ if (!RecordFailure(new PipelineStoppedException(), command: null))
+ {
return;
+ }
// Retain copy of _commands in case Dispose() is called
List commands = _commands;
- if (commands == null)
+ if (commands is null)
+ {
return;
+ }
// Call StopProcessing() for all the commands.
- for (int i = 0; i < commands.Count; i++)
+ foreach (CommandProcessorBase commandProcessor in commands)
{
- CommandProcessorBase commandProcessor = commands[i];
-
if (commandProcessor == null)
{
throw PSTraceSource.NewInvalidOperationException();
}
-#pragma warning disable 56500
+
try
{
commandProcessor.Command.DoStopProcessing();
}
catch (Exception)
{
- // 2004/04/26-JonN We swallow exceptions
- // which occur during StopProcessing.
+ // We swallow exceptions which occur during StopProcessing.
continue;
}
-#pragma warning restore 56500
}
}
@@ -844,43 +932,35 @@ internal void Stop()
///
internal Array Step(object input)
{
- if (Stopping)
- {
- throw new PipelineStoppedException();
- }
-
+ bool injectSucceeded = false;
try
{
Start(true);
Inject(input, enumerate: false);
+ injectSucceeded = true;
- // If a terminating error occurred, report it now.
- if (_firstTerminatingError != null)
- {
- _firstTerminatingError.Throw();
- }
-
+ // Check if this pipeline is being stopped asynchronously.
+ ThrowFirstErrorIfExisting(logException: false);
return RetrieveResults();
}
- catch (PipelineStoppedException)
+ catch (Exception e)
{
+ Clean();
DisposeCommands();
- // The error we want to report is the first terminating error
- // which occurred during pipeline execution, regardless
- // of whether other errors occurred afterward.
- if (_firstTerminatingError != null)
+ if (!injectSucceeded && e is PipelineStoppedException)
{
- _firstTerminatingError.Throw();
+ // When a terminating error happens during command execution, PowerShell will first save it
+ // to '_firstTerminatingError', and then throw a 'PipelineStoppedException' to tear down the
+ // pipeline. So when the caught exception here is 'PipelineStoppedException', it may not be
+ // the actual original terminating error.
+ // In this case, we want to report the first terminating error which occurred during pipeline
+ // execution, regardless of whether other errors occurred afterward.
+ ThrowFirstErrorIfExisting(logException: false);
}
throw;
}
- catch (Exception)
- {
- DisposeCommands();
- throw;
- }
}
///
@@ -927,7 +1007,9 @@ private void Start(bool incomingStream)
}
if (_executionStarted)
+ {
return;
+ }
if (_commands == null || _commands.Count == 0)
{
@@ -936,12 +1018,7 @@ private void Start(bool incomingStream)
}
CommandProcessorBase firstcommandProcessor = _commands[0];
- if (firstcommandProcessor == null
- || firstcommandProcessor.CommandRuntime == null)
- {
- throw PSTraceSource.NewInvalidOperationException(
- PipelineStrings.PipelineExecuteRequiresAtLeastOneCommand);
- }
+ ValidateCommandProcessorNotNull(firstcommandProcessor, PipelineStrings.PipelineExecuteRequiresAtLeastOneCommand);
// Set the execution scope using the current scope
if (_executionScope == null)
@@ -951,17 +1028,11 @@ private void Start(bool incomingStream)
// add ExternalSuccessOutput to the last command
CommandProcessorBase LastCommandProcessor = _commands[_commands.Count - 1];
- if (LastCommandProcessor == null
- || LastCommandProcessor.CommandRuntime == null)
- {
- // "PipelineProcessor.Start(): LastCommandProcessor == null"
- throw PSTraceSource.NewInvalidOperationException();
- }
+ ValidateCommandProcessorNotNull(LastCommandProcessor, errorMessage: null);
if (ExternalSuccessOutput != null)
{
- LastCommandProcessor.CommandRuntime.OutputPipe.ExternalWriter
- = ExternalSuccessOutput;
+ LastCommandProcessor.CommandRuntime.OutputPipe.ExternalWriter = ExternalSuccessOutput;
}
// add ExternalErrorOutput to all commands whose error
@@ -981,14 +1052,11 @@ private void Start(bool incomingStream)
_executionStarted = true;
- //
// Allocate the pipeline iteration array; note that the pipeline position for
// each command starts at 1 so we need to allocate _commands.Count + 1 items.
- //
int[] pipelineIterationInfo = new int[_commands.Count + 1];
- // Prepare all commands from Engine's side,
- // and make sure they are all valid
+ // Prepare all commands from Engine's side, and make sure they are all valid
for (int i = 0; i < _commands.Count; i++)
{
CommandProcessorBase commandProcessor = _commands[i];
@@ -1000,8 +1068,6 @@ private void Start(bool incomingStream)
// Generate new Activity Id for the thread
Guid pipelineActivityId = EtwActivity.CreateActivityId();
-
- // commandProcess.PipelineActivityId = new Activity id
EtwActivity.SetActivityId(pipelineActivityId);
commandProcessor.PipelineActivityId = pipelineActivityId;
@@ -1011,20 +1077,16 @@ private void Start(bool incomingStream)
CommandState.Started,
commandProcessor.Command.MyInvocation);
- // Telemetry here
- // the type of command should be sent along
- // commandProcessor.CommandInfo.CommandType
+ // Send telemetry that includes the type of command.
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ApplicationType, commandProcessor.Command.CommandInfo.CommandType.ToString());
#if LEGACYTELEMETRY
Microsoft.PowerShell.Telemetry.Internal.TelemetryAPI.TraceExecutedCommand(commandProcessor.Command.CommandInfo, commandProcessor.Command.CommandOrigin);
#endif
- // Log the execution of a command (not script chunks, as they
- // are not commands in and of themselves)
+ // Log the execution of a command (not script chunks, as they are not commands in and of themselves)
if (commandProcessor.CommandInfo.CommandType != CommandTypes.Script)
{
- commandProcessor.CommandRuntime.PipelineProcessor.LogExecutionInfo(
- commandProcessor.Command.MyInvocation, commandProcessor.CommandInfo.Name);
+ LogExecutionInfo(commandProcessor.Command.MyInvocation, commandProcessor.CommandInfo.Name);
}
InvocationInfo myInfo = commandProcessor.Command.MyInvocation;
@@ -1057,8 +1119,7 @@ private void Start(bool incomingStream)
}
///
- /// Add ExternalErrorOutput to all commands whose error
- /// output is not yet claimed.
+ /// Add ExternalErrorOutput to all commands whose error output is not yet claimed.
///
private void SetExternalErrorOutput()
{
@@ -1067,14 +1128,12 @@ private void SetExternalErrorOutput()
for (int i = 0; i < _commands.Count; i++)
{
CommandProcessorBase commandProcessor = _commands[i];
- Pipe UpstreamPipe =
- commandProcessor.CommandRuntime.ErrorOutputPipe;
+ Pipe errorPipe = commandProcessor.CommandRuntime.ErrorOutputPipe;
// check whether a cmdlet is consuming the error pipe
- if (!UpstreamPipe.IsRedirected)
+ if (!errorPipe.IsRedirected)
{
- UpstreamPipe.ExternalWriter =
- ExternalErrorOutput;
+ errorPipe.ExternalWriter = ExternalErrorOutput;
}
}
}
@@ -1085,14 +1144,9 @@ private void SetExternalErrorOutput()
///
private void SetupParameterVariables()
{
- for (int i = 0; i < _commands.Count; i++)
+ foreach (CommandProcessorBase commandProcessor in _commands)
{
- CommandProcessorBase commandProcessor = _commands[i];
- if (commandProcessor == null || commandProcessor.CommandRuntime == null)
- {
- // "null command " + i
- throw PSTraceSource.NewInvalidOperationException();
- }
+ ValidateCommandProcessorNotNull(commandProcessor, errorMessage: null);
commandProcessor.CommandRuntime.SetupOutVariable();
commandProcessor.CommandRuntime.SetupErrorVariable();
@@ -1102,6 +1156,16 @@ private void SetupParameterVariables()
}
}
+ private static void ValidateCommandProcessorNotNull(CommandProcessorBase commandProcessor, string errorMessage)
+ {
+ if (commandProcessor?.CommandRuntime is null)
+ {
+ throw errorMessage is null
+ ? PSTraceSource.NewInvalidOperationException()
+ : PSTraceSource.NewInvalidOperationException(errorMessage, Array.Empty