Add clean block to script block as a peer to begin, process, and end to allow easy resource cleanup (#15177)

This commit is contained in:
Dongbo Wang 2021-10-11 14:49:09 -07:00 committed by GitHub
parent fa4bfb447e
commit a32700a1c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 3055 additions and 644 deletions

View file

@ -2823,13 +2823,14 @@ function Get-PSImplicitRemotingClientSideParameters
$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)

View file

@ -233,6 +233,13 @@ namespace System.Management.Automation.Internal
{
}
/// <summary>
/// When overridden in the derived class, performs clean-up after the command execution.
/// </summary>
internal virtual void DoCleanResource()
{
}
#endregion Override
/// <summary>

View file

@ -529,7 +529,7 @@ namespace System.Management.Automation
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;

View file

@ -875,8 +875,11 @@ process
end
{{{5}}}
clean
{{{6}}}
<#
{6}
{7}
#>
",
GetDecl(),
@ -885,6 +888,7 @@ end
GetBeginBlock(),
GetProcessBlock(),
GetEndBlock(),
GetCleanBlock(),
CodeGeneration.EscapeBlockCommentContent(helpComment));
return result;
@ -1063,6 +1067,11 @@ end
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 @@ end
";
}
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

View file

@ -309,13 +309,11 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
// 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 @@ namespace System.Management.Automation
}
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

View file

@ -5,8 +5,7 @@ using System.Collections;
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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
commandInfo);
throw new CmdletInvocationException(errorRecord);
}
HasCleanBlock = scriptCommand.ScriptBlock.HasCleanBlock;
}
CommandInfo = commandInfo;
@ -87,6 +89,11 @@ namespace System.Management.Automation
/// <value></value>
internal CommandInfo CommandInfo { get; set; }
/// <summary>
/// Gets whether the command has a 'Clean' block defined.
/// </summary>
internal bool HasCleanBlock { get; }
/// <summary>
/// This indicates whether this command processor is created from
/// a script file.
@ -371,13 +378,10 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
// 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 @@ namespace System.Management.Automation
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,44 +624,119 @@ namespace System.Management.Automation
// 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;
}
// Restore the previous session state
if (_previousCommandSessionState != null)
{
Context.EngineSessionState = _previousCommandSessionState;
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;
Pipe oldErrorOutputPipe = _context.ShellFunctionErrorOutputPipe;
CommandProcessorBase oldCurrentCommandProcessor = _context.CurrentCommandProcessor;
try
{
if (RedirectShellErrorOutputPipe || _context.ShellFunctionErrorOutputPipe is not null)
{
_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;
}
/// <summary>
@ -777,23 +845,16 @@ namespace System.Management.Automation
{
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 @@ namespace System.Management.Automation
}
// 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 @@ namespace System.Management.Automation
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();
}

View file

@ -783,11 +783,6 @@ namespace System.Management.Automation
return oldPipe;
}
internal void RestoreErrorPipe(Pipe pipe)
{
ShellFunctionErrorOutputPipe = pipe;
}
/// <summary>
/// Reset all of the redirection book keeping variables. This routine should be called when starting to
/// execute a script.
@ -840,15 +835,13 @@ namespace System.Management.Automation
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;

View file

@ -23,6 +23,7 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
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<ExperimentalFeature>(engineFeatures);

View file

@ -942,10 +942,8 @@ namespace System.Management.Automation
// 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 @@ namespace System.Management.Automation
}
// Log a command health event
MshLog.LogCommandHealthEvent(
Context,
e,
@ -3765,8 +3762,10 @@ namespace System.Management.Automation
{
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 @@ namespace System.Management.Automation
internal void RemoveVariableListsInPipe()
{
if (_outVarList != null)
if (_outVarList != null && !OutputPipe.IgnoreOutVariableList)
{
this.OutputPipe.RemoveVariableList(VariableStreamKind.Output, _outVarList);
}

View file

@ -109,6 +109,13 @@ namespace System.Management.Automation.Internal
/// </summary>
internal int OutBufferCount { get; set; } = 0;
/// <summary>
/// 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.
/// </summary>
internal bool IgnoreOutVariableList { get; set; }
/// <summary>
/// If true, then all input added to this pipe will simply be discarded...
/// </summary>

View file

@ -247,6 +247,30 @@ namespace System.Management.Automation
return commandMetadata.GetEndBlock();
}
/// <summary>
/// This method constructs a string representing the clean block of the command
/// specified by <paramref name="commandMetadata"/>. The returned string only contains the
/// script, it is not enclosed in "clean { }".
/// </summary>
/// <param name="commandMetadata">
/// An instance of CommandMetadata representing a command.
/// </param>
/// <returns>
/// A string representing the end block of the command.
/// </returns>
/// <exception cref="ArgumentNullException">
/// If <paramref name="commandMetadata"/> is null.
/// </exception>
public static string GetClean(CommandMetadata commandMetadata)
{
if (commandMetadata == null)
{
throw PSTraceSource.NewArgumentNullException(nameof(commandMetadata));
}
return commandMetadata.GetCleanBlock();
}
private static T GetProperty<T>(PSObject obj, string property) where T : class
{
T result = null;

View file

@ -6,6 +6,7 @@ using System.Collections.Generic;
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 @@ namespace System.Management.Automation
protected bool _dontUseScopeCommandOrigin;
/// <summary>
/// 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...
/// </summary>
protected bool _rethrowExitException;
@ -237,6 +238,7 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
if (_scriptBlock.HasEndBlock)
{
var endBlock = _runOptimizedCode ? _scriptBlock.EndBlock : _scriptBlock.UnoptimizedEndBlock;
if (this.CommandRuntime.InputPipe.ExternalReader == null)
{
if (IsPipelineInputExpected())
@ -433,7 +435,33 @@ namespace System.Management.Automation
}
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 @@ namespace System.Management.Automation
{
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 @@ namespace System.Management.Automation
}
finally
{
this.Context.RestoreErrorPipe(oldErrorOutputPipe);
Context.ShellFunctionErrorOutputPipe = oldErrorOutputPipe;
if (oldLanguageMode.HasValue)
{
@ -584,15 +613,12 @@ namespace System.Management.Automation
}
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);
}
}

View file

@ -531,15 +531,16 @@ namespace System.Management.Automation
// 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;

View file

@ -445,7 +445,7 @@ namespace System.Management.Automation.Runspaces
///
/// 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.
/// </summary>
internal bool RedirectShellErrorOutputPipe { get; set; } = false;

View file

@ -1280,7 +1280,42 @@ namespace System.Management.Automation
{
// then pop this pipeline and dispose it...
_context.PopPipelineProcessor(true);
_pipeline.Dispose();
Dispose();
}
}
/// <summary>
/// Clean resources for script commands of this steppable pipeline.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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 @@ namespace System.Management.Automation
/// When this object is disposed, the contained pipeline should also be disposed.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_pipeline.Dispose();
}
_pipeline.Dispose();
_disposed = true;
}

View file

@ -2026,6 +2026,7 @@ namespace System.Management.Automation.Language
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 @@ namespace System.Management.Automation.Language
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 @@ namespace System.Management.Automation.Language
private Expression<Action<FunctionContext>> _beginBlockLambda;
private Expression<Action<FunctionContext>> _processBlockLambda;
private Expression<Action<FunctionContext>> _endBlockLambda;
private Expression<Action<FunctionContext>> _cleanBlockLambda;
private readonly List<LoopGotoTargets> _loopTargets = new List<LoopGotoTargets>();
private bool _generatingWhileOrDoLoop;
@ -2463,6 +2466,13 @@ namespace System.Management.Automation.Language
}
_endBlockLambda = CompileNamedBlock(scriptBlockAst.EndBlock, funcName, rootForDefiningTypesAndUsings);
rootForDefiningTypesAndUsings = null;
}
if (scriptBlockAst.CleanBlock != null)
{
_cleanBlockLambda = CompileNamedBlock(scriptBlockAst.CleanBlock, funcName + "<Clean>", rootForDefiningTypesAndUsings);
rootForDefiningTypesAndUsings = null;
}
return null;

View file

@ -724,12 +724,13 @@ namespace System.Management.Automation.Language
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 @@ namespace System.Management.Automation.Language
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 @@ namespace System.Management.Automation.Language
case TokenKind.Begin:
case TokenKind.Process:
case TokenKind.End:
case TokenKind.Clean:
break;
}
@ -1797,6 +1799,10 @@ namespace System.Management.Automation.Language
{
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 @@ namespace System.Management.Automation.Language
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);
}

View file

@ -406,10 +406,11 @@ namespace System.Management.Automation.Language
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),

View file

@ -945,25 +945,11 @@ namespace System.Management.Automation.Language
{
_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);

View file

@ -818,6 +818,46 @@ namespace System.Management.Automation.Language
NamedBlockAst processBlock,
NamedBlockAst endBlock,
NamedBlockAst dynamicParamBlock)
: this(
extent,
usingStatements,
attributes,
paramBlock,
beginBlock,
processBlock,
endBlock,
cleanBlock: null,
dynamicParamBlock)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScriptBlockAst"/> class.
/// This construction uses explicitly named begin/process/end/clean blocks.
/// </summary>
/// <param name="extent">The extent of the script block.</param>
/// <param name="usingStatements">The list of using statments, may be null.</param>
/// <param name="attributes">The set of attributes for the script block.</param>
/// <param name="paramBlock">The ast for the param block, may be null.</param>
/// <param name="beginBlock">The ast for the begin block, may be null.</param>
/// <param name="processBlock">The ast for the process block, may be null.</param>
/// <param name="endBlock">The ast for the end block, may be null.</param>
/// <param name="cleanBlock">The ast for the clean block, may be null.</param>
/// <param name="dynamicParamBlock">The ast for the dynamicparam block, may be null.</param>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> is null.
/// </exception>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "param")]
public ScriptBlockAst(
IScriptExtent extent,
IEnumerable<UsingStatementAst> usingStatements,
IEnumerable<AttributeAst> attributes,
ParamBlockAst paramBlock,
NamedBlockAst beginBlock,
NamedBlockAst processBlock,
NamedBlockAst endBlock,
NamedBlockAst cleanBlock,
NamedBlockAst dynamicParamBlock)
: base(extent)
{
SetUsingStatements(usingStatements);
@ -856,6 +896,12 @@ namespace System.Management.Automation.Language
SetParent(endBlock);
}
if (cleanBlock != null)
{
this.CleanBlock = cleanBlock;
SetParent(cleanBlock);
}
if (dynamicParamBlock != null)
{
this.DynamicParamBlock = dynamicParamBlock;
@ -888,6 +934,35 @@ namespace System.Management.Automation.Language
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScriptBlockAst"/> class.
/// This construction uses explicitly named begin/process/end/clean blocks.
/// </summary>
/// <param name="extent">The extent of the script block.</param>
/// <param name="usingStatements">The list of using statments, may be null.</param>
/// <param name="paramBlock">The ast for the param block, may be null.</param>
/// <param name="beginBlock">The ast for the begin block, may be null.</param>
/// <param name="processBlock">The ast for the process block, may be null.</param>
/// <param name="endBlock">The ast for the end block, may be null.</param>
/// <param name="cleanBlock">The ast for the clean block, may be null.</param>
/// <param name="dynamicParamBlock">The ast for the dynamicparam block, may be null.</param>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> is null.
/// </exception>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "param")]
public ScriptBlockAst(
IScriptExtent extent,
IEnumerable<UsingStatementAst> usingStatements,
ParamBlockAst paramBlock,
NamedBlockAst beginBlock,
NamedBlockAst processBlock,
NamedBlockAst endBlock,
NamedBlockAst cleanBlock,
NamedBlockAst dynamicParamBlock)
: this(extent, usingStatements, null, paramBlock, beginBlock, processBlock, endBlock, cleanBlock, dynamicParamBlock)
{
}
/// <summary>
/// Construct a ScriptBlockAst that uses explicitly named begin/process/end blocks.
/// </summary>
@ -911,6 +986,33 @@ namespace System.Management.Automation.Language
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ScriptBlockAst"/> class.
/// This construction uses explicitly named begin/process/end/clean blocks.
/// </summary>
/// <param name="extent">The extent of the script block.</param>
/// <param name="paramBlock">The ast for the param block, may be null.</param>
/// <param name="beginBlock">The ast for the begin block, may be null.</param>
/// <param name="processBlock">The ast for the process block, may be null.</param>
/// <param name="endBlock">The ast for the end block, may be null.</param>
/// <param name="cleanBlock">The ast for the clean block, may be null.</param>
/// <param name="dynamicParamBlock">The ast for the dynamicparam block, may be null.</param>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> is null.
/// </exception>
[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)
{
}
/// <summary>
/// Construct a ScriptBlockAst that does not use explicitly named blocks.
/// </summary>
@ -1115,6 +1217,11 @@ namespace System.Management.Automation.Language
/// </summary>
public NamedBlockAst EndBlock { get; }
/// <summary>
/// Gets the ast representing the clean block for a script block, or null if no clean block was specified.
/// </summary>
public NamedBlockAst CleanBlock { get; }
/// <summary>
/// The ast representing the dynamicparam block for a script block, or null if no dynamicparam block was specified.
/// </summary>
@ -1194,17 +1301,25 @@ namespace System.Management.Automation.Language
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 @@ namespace System.Management.Automation.Language
}
}
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 @@ namespace System.Management.Automation.Language
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 @@ namespace System.Management.Automation.Language
public class NamedBlockAst : Ast
{
/// <summary>
/// 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.
/// </summary>
/// <param name="extent">
/// The extent of the block. If <paramref name="unnamed"/> is false, the extent includes
@ -1761,6 +1889,7 @@ namespace System.Management.Automation.Language
/// <item><see cref="TokenKind.Begin"/></item>
/// <item><see cref="TokenKind.Process"/></item>
/// <item><see cref="TokenKind.End"/></item>
/// <item><see cref="TokenKind.Clean"/></item>
/// <item><see cref="TokenKind.Dynamicparam"/></item>
/// </list>
/// </param>
@ -1779,8 +1908,7 @@ namespace System.Management.Automation.Language
{
// 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 @@ namespace System.Management.Automation.Language
/// <item><see cref="TokenKind.Begin"/></item>
/// <item><see cref="TokenKind.Process"/></item>
/// <item><see cref="TokenKind.End"/></item>
/// <item><see cref="TokenKind.Clean"/></item>
/// <item><see cref="TokenKind.Dynamicparam"/></item>
/// </list>
/// </summary>
@ -1877,6 +2006,14 @@ namespace System.Management.Automation.Language
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; }

View file

@ -588,6 +588,9 @@ namespace System.Management.Automation.Language
/// <summary>The 'default' keyword</summary>
Default = 169,
/// <summary>The 'clean' keyword.</summary>
Clean = 170,
#endregion Keywords
}
@ -659,7 +662,7 @@ namespace System.Management.Automation.Language
Keyword = 0x00000010,
/// <summary>
/// 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'.
/// </summary>
ScriptBlockBlockName = 0x00000020,
@ -948,6 +951,7 @@ namespace System.Management.Automation.Language
/* Hidden */ TokenFlags.Keyword,
/* Base */ TokenFlags.Keyword,
/* Default */ TokenFlags.Keyword,
/* Clean */ TokenFlags.Keyword | TokenFlags.ScriptBlockBlockName,
#endregion Flags for keywords
};
@ -1147,6 +1151,7 @@ namespace System.Management.Automation.Language
/* Hidden */ "hidden",
/* Base */ "base",
/* Default */ "default",
/* Clean */ "clean",
#endregion Text for keywords
};
@ -1154,10 +1159,12 @@ namespace System.Management.Automation.Language
#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");

View file

@ -635,23 +635,23 @@ namespace System.Management.Automation.Language
/*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 @@ namespace System.Management.Automation.Language
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]);
}

File diff suppressed because it is too large Load diff

View file

@ -12,11 +12,9 @@ using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
using System.Management.Automation.Tracing;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
#if LEGACYTELEMETRY
using Microsoft.PowerShell.Telemetry.Internal;
#endif
@ -35,6 +33,7 @@ namespace System.Management.Automation
Begin,
Process,
End,
Clean,
ProcessBlockOnly,
}
@ -247,6 +246,7 @@ namespace System.Management.Automation
if (scriptBlockAst.BeginBlock != null
|| scriptBlockAst.ProcessBlock != null
|| scriptBlockAst.CleanBlock != null
|| scriptBlockAst.ParamBlock != null
|| scriptBlockAst.DynamicParamBlock != null
|| scriptBlockAst.ScriptRequirements != null
@ -316,6 +316,8 @@ namespace System.Management.Automation
internal Dictionary<string, int> NameToIndexMap { get; set; }
#region Named Blocks
internal Action<FunctionContext> DynamicParamBlock { get; set; }
internal Action<FunctionContext> UnoptimizedDynamicParamBlock { get; set; }
@ -332,6 +334,12 @@ namespace System.Management.Automation
internal Action<FunctionContext> UnoptimizedEndBlock { get; set; }
internal Action<FunctionContext> CleanBlock { get; set; }
internal Action<FunctionContext> UnoptimizedCleanBlock { get; set; }
#endregion Named Blocks
internal IScriptExtent[] SequencePoints { get; set; }
private RuntimeDefinedParameterDictionary _runtimeDefinedParameterDictionary;
@ -753,7 +761,7 @@ namespace System.Management.Automation
{
errorHandler ??= (static _ => null);
if (HasBeginBlock || HasProcessBlock)
if (HasBeginBlock || HasProcessBlock || HasCleanBlock)
{
return errorHandler(AutomationExceptions.CanConvertOneClauseOnly);
}
@ -891,7 +899,10 @@ namespace System.Management.Automation
Parser parser = new Parser();
var ast = AstInternal;
if (HasBeginBlock || HasProcessBlock || ast.Body.ParamBlock != null)
if (HasBeginBlock
|| HasProcessBlock
|| HasCleanBlock
|| ast.Body.ParamBlock is not null)
{
Ast errorAst = ast.Body.BeginBlock ?? (Ast)ast.Body.ProcessBlock ?? ast.Body.ParamBlock;
parser.ReportError(
@ -974,6 +985,11 @@ namespace System.Management.Automation
InvocationInfo invocationInfo,
params object[] args)
{
if (clauseToInvoke == ScriptBlockClauseToInvoke.Clean)
{
throw new PSNotSupportedException(ParserStrings.InvokingCleanBlockNotSupported);
}
if ((clauseToInvoke == ScriptBlockClauseToInvoke.Begin && !HasBeginBlock)
|| (clauseToInvoke == ScriptBlockClauseToInvoke.Process && !HasProcessBlock)
|| (clauseToInvoke == ScriptBlockClauseToInvoke.End && !HasEndBlock))
@ -991,7 +1007,7 @@ namespace System.Management.Automation
throw new PipelineStoppedException();
}
// Validate at the arguments are consistent. The only public API that gets you here never sets createLocalScope to false...
// Validate that the arguments are consistent. The only public API that gets you here never sets createLocalScope to false...
Diagnostics.Assert(
createLocalScope || functionsToDefine == null,
"When calling ScriptBlock.InvokeWithContext(), if 'functionsToDefine' != null then 'createLocalScope' must be true");
@ -1195,7 +1211,7 @@ namespace System.Management.Automation
_sequencePoints = SequencePoints,
};
ScriptBlock.LogScriptBlockStart(this, context.CurrentRunspace.InstanceId);
LogScriptBlockStart(this, context.CurrentRunspace.InstanceId);
try
{
@ -1203,7 +1219,7 @@ namespace System.Management.Automation
}
finally
{
ScriptBlock.LogScriptBlockEnd(this, context.CurrentRunspace.InstanceId);
LogScriptBlockEnd(this, context.CurrentRunspace.InstanceId);
}
}
catch (TargetInvocationException tie)
@ -1362,7 +1378,7 @@ namespace System.Management.Automation
private Action<FunctionContext> GetCodeToInvoke(ref bool optimized, ScriptBlockClauseToInvoke clauseToInvoke)
{
if (clauseToInvoke == ScriptBlockClauseToInvoke.ProcessBlockOnly
&& (HasBeginBlock || (HasEndBlock && HasProcessBlock)))
&& (HasBeginBlock || HasCleanBlock || (HasEndBlock && HasProcessBlock)))
{
throw PSTraceSource.NewInvalidOperationException(AutomationExceptions.ScriptBlockInvokeOnOneClauseOnly);
}
@ -1379,6 +1395,8 @@ namespace System.Management.Automation
return _scriptBlockData.ProcessBlock;
case ScriptBlockClauseToInvoke.End:
return _scriptBlockData.EndBlock;
case ScriptBlockClauseToInvoke.Clean:
return _scriptBlockData.CleanBlock;
default:
return HasProcessBlock ? _scriptBlockData.ProcessBlock : _scriptBlockData.EndBlock;
}
@ -1392,6 +1410,8 @@ namespace System.Management.Automation
return _scriptBlockData.UnoptimizedProcessBlock;
case ScriptBlockClauseToInvoke.End:
return _scriptBlockData.UnoptimizedEndBlock;
case ScriptBlockClauseToInvoke.Clean:
return _scriptBlockData.UnoptimizedCleanBlock;
default:
return HasProcessBlock ? _scriptBlockData.UnoptimizedProcessBlock : _scriptBlockData.UnoptimizedEndBlock;
}
@ -2147,11 +2167,17 @@ namespace System.Management.Automation
internal Action<FunctionContext> UnoptimizedEndBlock { get => _scriptBlockData.UnoptimizedEndBlock; }
internal Action<FunctionContext> CleanBlock { get => _scriptBlockData.CleanBlock; }
internal Action<FunctionContext> UnoptimizedCleanBlock { get => _scriptBlockData.UnoptimizedCleanBlock; }
internal bool HasBeginBlock { get => AstInternal.Body.BeginBlock != null; }
internal bool HasProcessBlock { get => AstInternal.Body.ProcessBlock != null; }
internal bool HasEndBlock { get => AstInternal.Body.EndBlock != null; }
internal bool HasCleanBlock { get => AstInternal.Body.CleanBlock != null; }
}
[Serializable]
@ -2197,11 +2223,13 @@ namespace System.Management.Automation
private readonly bool _useLocalScope;
private readonly bool _runOptimized;
private readonly bool _rethrowExitException;
private MshCommandRuntime _commandRuntime;
private readonly MutableTuple _localsTuple;
private bool _exitWasCalled;
private readonly FunctionContext _functionContext;
private MshCommandRuntime _commandRuntime;
private bool _exitWasCalled;
private bool _anyClauseExecuted;
public PSScriptCmdlet(ScriptBlock scriptBlock, bool useNewScope, bool fromScriptFile, ExecutionContext context)
{
_scriptBlock = scriptBlock;
@ -2291,6 +2319,34 @@ namespace System.Management.Automation
}
}
internal override void DoCleanResource()
{
if (_scriptBlock.HasCleanBlock && _anyClauseExecuted)
{
// The 'Clean' block doesn't write any output to pipeline, so we use a 'NullPipe' here and
// disallow the output to be collected by an 'out' variable. However, the error, warning,
// and information records should still be collectable by the corresponding variables.
Pipe oldOutputPipe = _commandRuntime.OutputPipe;
_functionContext._outputPipe = _commandRuntime.OutputPipe = new Pipe
{
NullPipe = true,
IgnoreOutVariableList = true,
};
try
{
RunClause(
clause: _runOptimized ? _scriptBlock.CleanBlock : _scriptBlock.UnoptimizedCleanBlock,
dollarUnderbar: AutomationNull.Value,
inputToProcess: AutomationNull.Value);
}
finally
{
_functionContext._outputPipe = _commandRuntime.OutputPipe = oldOutputPipe;
}
}
}
private void EnterScope()
{
_commandRuntime.SetVariableListsInPipe();
@ -2303,6 +2359,7 @@ namespace System.Management.Automation
private void RunClause(Action<FunctionContext> clause, object dollarUnderbar, object inputToProcess)
{
_anyClauseExecuted = true;
Pipe oldErrorOutputPipe = this.Context.ShellFunctionErrorOutputPipe;
// If the script block has a different language mode than the current,
@ -2356,9 +2413,9 @@ namespace System.Management.Automation
}
finally
{
this.Context.RestoreErrorPipe(oldErrorOutputPipe);
Context.ShellFunctionErrorOutputPipe = oldErrorOutputPipe;
// Set the language mode
// Restore the language mode
if (oldLanguageMode.HasValue)
{
Context.LanguageMode = oldLanguageMode.Value;
@ -2518,9 +2575,6 @@ namespace System.Management.Automation
commandRuntime = null;
currentObjectInPipeline = null;
_input.Clear();
// _scriptBlock = null;
// _localsTuple = null;
// _functionContext = null;
base.InternalDispose(true);
_disposed = true;

View file

@ -1578,23 +1578,33 @@ namespace System.Management.Automation
internal static bool SuspendStoppingPipeline(ExecutionContext context)
{
LocalPipeline lpl = (LocalPipeline)context.CurrentRunspace.GetCurrentlyRunningPipeline();
if (lpl != null)
var localPipeline = (LocalPipeline)context.CurrentRunspace.GetCurrentlyRunningPipeline();
return SuspendStoppingPipelineImpl(localPipeline);
}
internal static void RestoreStoppingPipeline(ExecutionContext context, bool oldIsStopping)
{
var localPipeline = (LocalPipeline)context.CurrentRunspace.GetCurrentlyRunningPipeline();
RestoreStoppingPipelineImpl(localPipeline, oldIsStopping);
}
internal static bool SuspendStoppingPipelineImpl(LocalPipeline localPipeline)
{
if (localPipeline is not null)
{
bool oldIsStopping = lpl.Stopper.IsStopping;
lpl.Stopper.IsStopping = false;
bool oldIsStopping = localPipeline.Stopper.IsStopping;
localPipeline.Stopper.IsStopping = false;
return oldIsStopping;
}
return false;
}
internal static void RestoreStoppingPipeline(ExecutionContext context, bool oldIsStopping)
internal static void RestoreStoppingPipelineImpl(LocalPipeline localPipeline, bool oldIsStopping)
{
LocalPipeline lpl = (LocalPipeline)context.CurrentRunspace.GetCurrentlyRunningPipeline();
if (lpl != null)
if (localPipeline is not null)
{
lpl.Stopper.IsStopping = oldIsStopping;
localPipeline.Stopper.IsStopping = oldIsStopping;
}
}
@ -2855,6 +2865,11 @@ namespace System.Management.Automation
ScriptBlock sb = expression as ScriptBlock;
if (sb != null)
{
if (sb.HasCleanBlock)
{
throw new PSNotSupportedException(ParserStrings.ForEachNotSupportCleanBlock);
}
Pipe outputPipe = new Pipe(result);
if (sb.HasBeginBlock)
{

View file

@ -497,7 +497,7 @@ The correct form is: foreach ($a in $b) {...}</value>
<value>Script command clause '{0}' has already been defined.</value>
</data>
<data name="MissingNamedBlocks" xml:space="preserve">
<value>unexpected token '{0}', expected 'begin', 'process', 'end', or 'dynamicparam'.</value>
<value>unexpected token '{0}', expected 'begin', 'process', 'end', 'clean', or 'dynamicparam'.</value>
</data>
<data name="MissingEndCurlyBrace" xml:space="preserve">
<value>Missing closing '}' in statement block or type definition.</value>
@ -1126,6 +1126,9 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent
<data name="ForEachTypeConversionFailed" xml:space="preserve">
<value>Unable to convert input to the target type [{0}] passed to the ForEach() operator. Please check the specified type and try running your script again.</value>
</data>
<data name="ForEachNotSupportCleanBlock" xml:space="preserve">
<value>Script block with a 'clean' block is not supported by the 'ForEach' method.</value>
</data>
<data name="NumberToReturnMustBeGreaterThanZero" xml:space="preserve">
<value>The 'numberToReturn' value provided to the third argument of the Where() operator must be greater than zero. Please correct the argument's value and try running your script again.</value>
</data>
@ -1479,4 +1482,7 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent
<data name="BackgroundOperatorInPipelineChain" xml:space="preserve">
<value>Background operators can only be used at the end of a pipeline chain.</value>
</data>
<data name="InvokingCleanBlockNotSupported" xml:space="preserve">
<value>Directly invoking the 'clean' block of a script block is not supported.</value>
</data>
</root>

View file

@ -67,6 +67,8 @@ Describe "ParserTests (admin\monad\tests\monad\src\engine\core\ParserTests.cs)"
}
end {}
clean {}
}
'@
$functionDefinition>$functionDefinitionFile

View file

@ -148,17 +148,21 @@ Describe 'named blocks parsing' -Tags "CI" {
ShouldBeParseError 'begin' MissingNamedStatementBlock 5
ShouldBeParseError 'process' MissingNamedStatementBlock 7
ShouldBeParseError 'end' MissingNamedStatementBlock 3
ShouldBeParseError 'clean' MissingNamedStatementBlock 5
ShouldBeParseError 'dynamicparam' MissingNamedStatementBlock 12
ShouldBeParseError 'begin process {}' MissingNamedStatementBlock 6 -CheckColumnNumber
ShouldBeParseError 'end process {}' MissingNamedStatementBlock 4 -CheckColumnNumber
ShouldBeParseError 'clean process {}' MissingNamedStatementBlock 6 -CheckColumnNumber
ShouldBeParseError 'dynamicparam process {}' MissingNamedStatementBlock 13 -CheckColumnNumber
ShouldBeParseError 'process begin {}' MissingNamedStatementBlock 8 -CheckColumnNumber
ShouldBeParseError 'begin process end' MissingNamedStatementBlock,MissingNamedStatementBlock,MissingNamedStatementBlock 6,14,18 -CheckColumnNumber
ShouldBeParseError 'begin process end clean' MissingNamedStatementBlock, MissingNamedStatementBlock, MissingNamedStatementBlock, MissingNamedStatementBlock 6, 14, 18, 24 -CheckColumnNumber
Test-Ast 'begin' 'begin' 'begin'
Test-Ast 'begin end' 'begin end' 'begin' 'end'
Test-Ast 'begin end process' 'begin end process' 'begin' 'end' 'process'
Test-Ast 'begin {} end' 'begin {} end' 'begin {}' 'end'
Test-Ast 'begin process end clean' 'begin process end clean' 'begin' 'clean' 'end' 'process'
Test-Ast 'begin {} process end clean {}' 'begin {} process end clean {}' 'begin {}' 'clean {}' 'end' 'process'
}
#

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,591 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe 'Function Pipeline Behaviour' -Tag 'CI' {
BeforeAll {
$filePath = "$TestDrive\output.txt"
if (Test-Path $filePath) {
Remove-Item $filePath -Force
}
}
Context "'Clean' block runs when any other named blocks run" {
AfterEach {
if (Test-Path $filePath) {
Remove-Item $filePath -Force
}
}
It "'Clean' block executes only if at least one of the other named blocks executed" {
## The 'Clean' block is for cleanup purpose. When none of other named blocks execute,
## there is no point to execute the 'Clean' block, so it will be skipped in this case.
function test-1 {
clean { 'clean-redirected-output' > $filePath }
}
function test-2 {
End { 'end' }
clean { 'clean-redirected-output' > $filePath }
}
## The 'Clean' block is skipped.
test-1 | Should -BeNullOrEmpty
Test-Path -Path $filePath | Should -BeFalse
## The 'Clean' block runs.
test-2 | Should -BeExactly 'end'
Test-Path -Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean-redirected-output'
}
It "'Clean' block is skipped when the command doesn't run due to no input from upstream command" {
function test-1 ([switch] $WriteOutput) {
Process {
if ($WriteOutput) {
Write-Output 'process'
} else {
Write-Verbose -Verbose 'process'
}
}
}
function test-2 {
Process { Write-Output "test-2: $_" }
clean { Write-Warning 'test-2-clean-warning' }
}
## No output from 'test-1.Process', so 'test-2.Process' didn't run, and thus 'test-2.Clean' was skipped.
test-1 | test-2 *>&1 | Should -BeNullOrEmpty
## Output from 'test-1.Process' would trigger 'test-2.Process' to run, and thus 'test-2.Clean' would run.
$output = test-1 -WriteOutput | test-2 *>&1
$output | Should -Be @('test-2: process', 'test-2-clean-warning')
}
It "'Clean' block is skipped when the command doesn't run due to terminating error from upstream Process block" {
function test-1 ([switch] $ThrowException) {
Process {
if ($ThrowException) {
throw 'process'
} else {
Write-Output 'process'
}
}
}
function test-2 {
Process { Write-Output "test-2: $_" }
clean { 'clean-redirected-output' > $filePath }
}
$failure = $null
try { test-1 -ThrowException | test-2 } catch { $failure = $_ }
$failure | Should -Not -BeNullOrEmpty
$failure.Exception.Message | Should -BeExactly 'process'
## 'test-2' didn't run because 'test-1' throws terminating exception, so 'test-2.Clean' didn't run either.
Test-Path -Path $filePath | Should -BeFalse
test-1 | test-2 | Should -BeExactly 'test-2: process'
Test-Path -Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean-redirected-output'
}
It "'Clean' block is skipped when the command doesn't run due to terminating error from upstream Begin block" {
function test-1 {
Begin { throw 'begin' }
End { 'end' }
}
function test-2 {
Begin { 'begin' }
Process { Write-Output "test-2: $_" }
clean { 'clean-redirected-output' > $filePath }
}
$failure = $null
try { test-1 | test-2 } catch { $failure = $_ }
$failure | Should -Not -BeNullOrEmpty
$failure.Exception.Message | Should -BeExactly 'begin'
## 'test-2' didn't run because 'test-1' throws terminating exception, so 'test-2.Clean' didn't run either.
Test-Path -Path $filePath | Should -BeFalse
}
It "'Clean' block runs when '<BlockName>' runs" -TestCases @(
@{ Script = { [CmdletBinding()]param() begin { 'output' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Begin' }
@{ Script = { [CmdletBinding()]param() process { 'output' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Process' }
@{ Script = { [CmdletBinding()]param() end { 'output' } clean { Write-Warning 'clean-warning' } }; BlockName = 'End' }
) {
param($Script, $BlockName)
& $Script -WarningVariable wv | Should -BeExactly 'output'
$wv | Should -BeExactly 'clean-warning'
}
It "'Clean' block runs when '<BlockName>' throws terminating error" -TestCases @(
@{ Script = { [CmdletBinding()]param() begin { throw 'failure' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Begin' }
@{ Script = { [CmdletBinding()]param() process { throw 'failure' } clean { Write-Warning 'clean-warning' } }; BlockName = 'Process' }
@{ Script = { [CmdletBinding()]param() end { throw 'failure' } clean { Write-Warning 'clean-warning' } }; BlockName = 'End' }
) {
param($Script, $BlockName)
$failure = $null
try { & $Script -WarningVariable wv } catch { $failure = $_ }
$failure | Should -Not -BeNullOrEmpty
$failure.Exception.Message | Should -BeExactly 'failure'
$wv | Should -BeExactly 'clean-warning'
}
It "'Clean' block runs in pipeline - simple function" {
function test-1 {
param([switch] $EmitError)
process {
if ($EmitError) {
throw 'test-1-process-error'
} else {
Write-Output 'test-1'
}
}
clean { 'test-1-clean' >> $filePath }
}
function test-2 {
begin { Write-Verbose -Verbose 'test-2-begin' }
process { $_ }
clean { 'test-2-clean' >> $filePath }
}
function test-3 {
end { Write-Verbose -Verbose 'test-3-end' }
clean { 'test-3-clean' >> $filePath }
}
## All command will run, so all 'Clean' blocks will run
test-1 | test-2 | test-3
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @('test-1-clean', 'test-2-clean', 'test-3-clean')
$failure = $null
Remove-Item $filePath -Force
try {
test-1 -EmitError | test-2 | test-3
} catch {
$failure = $_
}
## Exception is thrown from 'test-1.Process'. By that time, the 'test-2.Begin' has run,
## so 'test-2.Clean' will run. However, 'test-3.End' won't run, so 'test-3.Clean' won't run.
$failure | Should -Not -BeNullOrEmpty
$failure.Exception.Message | Should -BeExactly 'test-1-process-error'
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @('test-1-clean', 'test-2-clean')
}
It "'Clean' block runs in pipeline - advanced function" {
function test-1 {
[CmdletBinding()]
param([switch] $EmitError)
process {
if ($EmitError) {
throw 'test-1-process-error'
} else {
Write-Output 'test-1'
}
}
clean { 'test-1-clean' >> $filePath }
}
function test-2 {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
$pipeInput
)
begin { Write-Verbose -Verbose 'test-2-begin' }
process { $pipeInput }
clean { 'test-2-clean' >> $filePath }
}
function test-3 {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
$pipeInput
)
end { Write-Verbose -Verbose 'test-3-end' }
clean { 'test-3-clean' >> $filePath }
}
## All command will run, so all 'Clean' blocks will run
test-1 | test-2 | test-3
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @('test-1-clean', 'test-2-clean', 'test-3-clean')
$failure = $null
Remove-Item $filePath -Force
## Exception will be thrown from 'test-1.Process'. By that time, the 'test-2.Begin' has run,
## so 'test-2.Clean' will run. However, 'test-3.End' won't run, so 'test-3.Clean' won't run.
try {
test-1 -EmitError | test-2 | test-3
} catch {
$failure = $_
}
$failure | Should -Not -BeNullOrEmpty
$failure.Exception.Message | Should -BeExactly 'test-1-process-error'
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @('test-1-clean', 'test-2-clean')
}
It 'does not execute End {} if the pipeline is halted during Process {}' {
# We don't need Should -Not -Throw as if this reaches end{} and throws the test will fail anyway.
1..10 |
& {
begin { "BEGIN" }
process { "PROCESS $_" }
end { "END"; throw "This should not be reached." }
} |
Select-Object -First 3 |
Should -Be @( "BEGIN", "PROCESS 1", "PROCESS 2" )
}
It "still executes 'Clean' block if the pipeline is halted" {
1..10 |
& {
process { $_ }
clean { "Clean block hit" > $filePath }
} |
Select-Object -First 1 |
Should -Be 1
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'Clean block hit'
}
It "Select-Object in pipeline" {
function bar {
process { 'bar_' + $_ } end { 'bar_end' } clean { 'bar_clean' > $filePath }
}
function zoo {
process { 'zoo_' + $_ } end { 'zoo_end' } clean { 'zoo_clean' >> $filePath }
}
1..10 | bar | Select-Object -First 2 | zoo | Should -Be @('zoo_bar_1', 'zoo_bar_2', 'zoo_end')
Test-Path $filePath | Should -BeTrue
$content = Get-Content $filePath
$content | Should -Be @('bar_clean', 'zoo_clean')
}
}
Context 'Streams from Named Blocks' {
It 'Permits output from named block: <Script>' -TestCases @(
@{ Script = { begin { 10 } }; ExpectedResult = 10 }
@{ Script = { process { 15 } }; ExpectedResult = 15 }
@{ Script = { end { 22 } }; ExpectedResult = 22 }
) {
param($Script, $ExpectedResult)
& $Script | Should -Be $ExpectedResult
}
It "Does not allow output from 'Clean' block" {
& { end { } clean { 11 } } | Should -BeNullOrEmpty
}
It "OutVariable should not capture anything from 'Clean' block" {
function test {
[CmdletBinding()]
param()
Begin { 'begin' }
Process { 'process' }
End { 'end' }
clean { 'clean' }
}
test -OutVariable ov | Should -Be @( 'begin', 'process', 'end' )
$ov | Should -Be @( 'begin', 'process', 'end' )
}
It "Other streams can be captured from 'Clean' block" {
function test {
[CmdletBinding()]
param()
End { }
clean {
Write-Output 'clean-output'
Write-Warning 'clean-warning'
Write-Verbose -Verbose 'clean-verbose'
Write-Debug -Debug 'clean-debug'
Write-Information 'clean-information'
}
}
test -OutVariable ov -WarningVariable wv -InformationVariable iv
$ov.Count | Should -Be 0
$wv | Should -BeExactly 'clean-warning'
$iv | Should -BeExactly 'clean-information'
$allStreams = test *>&1
$allStreams | Should -Be @('clean-warning', 'clean-verbose', 'clean-debug', 'clean-information')
}
It 'passes output for begin, then process, then end, then clean' {
$Script = {
clean { Write-Warning 'clean-warning' }
process { "PROCESS" }
begin { "BEGIN" }
end { "END" }
}
$results = & $Script 3>&1
$results | Should -Be @( "BEGIN", "PROCESS", "END", "clean-warning" )
}
}
Context "Steppable pipeline" {
AfterEach {
if (Test-Path $filePath) {
Remove-Item $filePath -Force
}
}
It "'Clean' runs when steppable pipeline runs to the end successfully (<Block> block)" -TestCases @(
@{ Script = { begin { 'BEGIN' } clean { 'clean is hit' > $filePath } }; Block = 'Begin'; ProcessResult = @('BEGIN'); EndResult = $null }
@{ Script = { process { 'PROCESS' } clean { 'clean is hit' > $filePath } }; Block = 'Process'; ProcessResult = @('PROCESS'); EndResult = $null }
@{ Script = { end { 'END' } clean { 'clean is hit' > $filePath } }; Block = 'End'; ProcessResult = $null; EndResult = @('END') }
@{ Script = { begin { 'BEGIN' } process { 'PROCESS' } end { 'END' } clean { 'clean is hit' > $filePath } }; Block = 'All'; ProcessResult = @('BEGIN', 'PROCESS'); EndResult = @('END') }
) {
param($Script, $ProcessResult, $EndResult)
try {
$step = { & $Script }.GetSteppablePipeline()
$step.Begin($false)
$step.Process() | Should -Be $ProcessResult
$step.End() | Should -Be $EndResult
}
finally {
$step.Dispose()
}
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
}
It "'Clean' runs when exception thrown from '<Block>' block" -TestCases @(
@{ Script = { begin { throw 'begin-error' } clean { 'clean is hit' > $filePath } }; Block = 'Process'; ErrorMessage = 'begin-error' }
@{ Script = { process { throw 'process-error' } clean { 'clean is hit' > $filePath } }; Block = 'Process'; ErrorMessage = 'process-error' }
@{ Script = { end { throw 'end-error' } clean { 'clean is hit' > $filePath } }; Block = 'End'; ErrorMessage = 'end-error' }
) {
param($Script, $ErrorMessage)
$failure = $null
$step = { & $Script }.GetSteppablePipeline()
try {
$step.Begin($false)
$step.Process()
$step.End()
} catch {
$failure = $_
}
finally {
$step.Dispose()
}
$failure | Should -Not -BeNullOrEmpty
$failure.Exception.Message | Should -BeExactly $ErrorMessage
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
}
It "'Clean' runs when we explicitly call it on a steppable pipeline" {
$script = { begin { 'begin' > $filePath } clean { 'clean is hit' >> $filePath } }
$step = { & $script }.GetSteppablePipeline()
try {
$step.Begin($false)
$step.Clean()
}
finally {
$step.Dispose()
}
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly @('begin', 'clean is hit')
}
It "Calling 'Clean' on steppable pipeline after it has run automatically upon Exception won't trigger the 'Clean' block to run again" {
$script = { begin { throw 'begin-error' } clean { 'clean is hit' > $filePath } }
$step = { & $script }.GetSteppablePipeline()
$failure = $null
try {
$step.Begin($false)
$step.Process()
$step.End()
}
catch {
$failure = $_
}
$failure | Should -Not -BeNullOrEmpty
$failure.Exception.Message | Should -BeExactly 'begin-error'
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
Remove-Item $filePath -Force -ErrorAction Stop
## The 'Clean' block has already run automatically after the exception was thrown from 'Begin',
## and it won't run again when calling it explicitly.
$step.Clean()
Test-Path $filePath | Should -BeFalse
## Dispose the steppable pipeline.
$step.Dispose()
}
It "Calling 'Clean' on steppable pipeline after it has run automatically upon success won't trigger the 'Clean' block to run again" {
$script = { end { 'END' } clean { 'clean is hit' > $filePath } }
$step = { & $script }.GetSteppablePipeline()
$step.Begin($false)
$step.Process()
$step.End()
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'clean is hit'
Remove-Item $filePath -Force -ErrorAction Stop
## The 'Clean' block has already run automatically after the exception was thrown from 'Begin',
## and it won't run again when calling it explicitly.
$step.Clean()
Test-Path $filePath | Should -BeFalse
## Dispose the steppable pipeline.
$step.Dispose()
}
}
Context "'exit' statement in command" {
AfterEach {
if (Test-Path $filePath) {
Remove-Item $filePath -Force
}
}
It "'Clean' block runs when 'exit' is used in other named blocks" {
pwsh -c "& { process { exit 122 } clean { 'Clean block is hit' > $filePath } }"
$LASTEXITCODE | Should -BeExactly 122
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'Clean block is hit'
}
It "ExitException within 'Clean' block will not be propagated up" {
function test {
end {}
clean {
'Clean block is hit' > $filePath
exit
'more text' >> $filePath
}
}
test
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly 'Clean block is hit'
}
It "Exit from a script file works the same in 'Clean' block" {
$scriptFile = "$TestDrive\script.ps1"
"exit 122" > $scriptFile
function test {
end {}
clean {
& $scriptFile
$LASTEXITCODE > $filePath
}
}
test
Test-Path $filePath | Should -BeTrue
Get-Content $filePath | Should -BeExactly '122'
}
}
Context 'Ctrl-C behavior' {
AfterEach {
if ($pwsh) {
$pwsh.Dispose()
$pwsh = $null
}
}
It 'still executes clean {} when StopProcessing() is triggered mid-pipeline' {
$script = @"
function test {
process { Start-Sleep -Seconds 10 }
clean { Write-Information "CLEAN" }
}
"@
$pwsh = [powershell]::Create()
$pwsh.AddScript($script).Invoke()
$pwsh.Commands.Clear()
$pwsh.AddCommand('test') > $null
$asyncResult = $pwsh.BeginInvoke()
Start-Sleep -Seconds 2
$pwsh.Stop()
{ $pwsh.EndInvoke($asyncResult) } | Should -Throw -ErrorId 'PipelineStoppedException'
$pwsh.Streams.Information[0].MessageData | Should -BeExactly "CLEAN"
}
<#
It 'still completes clean {} execution when StopProcessing() is triggered mid-clean {}' {
$script = @"
function test {
begin {}
process {
"PROCESS"
}
end {}
clean {
Start-Sleep -Seconds 10
Write-Information "CLEAN"
}
}
"@
$pwsh = [powershell]::Create()
$pwsh.AddScript($script).AddStatement().AddCommand('test') > $null
$asyncResult = $pwsh.BeginInvoke()
Start-Sleep -Seconds 2
$pwsh.Stop()
$output = $pwsh.EndInvoke($asyncResult)
$output | Should -Be "PROCESS"
$pwsh.Streams.Information[0].MessageData | Should -BeExactly "CLEAN"
}
#>
}
}