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:
parent
fa4bfb447e
commit
a32700a1c1
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -67,6 +67,8 @@ Describe "ParserTests (admin\monad\tests\monad\src\engine\core\ParserTests.cs)"
|
|||
}
|
||||
|
||||
end {}
|
||||
|
||||
clean {}
|
||||
}
|
||||
'@
|
||||
$functionDefinition>$functionDefinitionFile
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
#
|
||||
|
|
1374
test/powershell/Language/Scripting/CleanBlockErrorHandling.Tests.ps1
Normal file
1374
test/powershell/Language/Scripting/CleanBlockErrorHandling.Tests.ps1
Normal file
File diff suppressed because it is too large
Load diff
591
test/powershell/Language/Scripting/PipelineBehaviour.Tests.ps1
Normal file
591
test/powershell/Language/Scripting/PipelineBehaviour.Tests.ps1
Normal 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"
|
||||
}
|
||||
#>
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue