MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections;5using System.Collections.Generic;6using System.Management.Automation.Runspaces;7using System.Management.Automation.Tracing;8using System.Reflection;9using System.Runtime.ExceptionServices;10using System.Threading;11using Microsoft.PowerShell.Telemetry;12 13using Dbg = System.Management.Automation.Diagnostics;14 15#pragma warning disable 1634, 1691 // Stops compiler from warning about unknown warnings16 17namespace System.Management.Automation.Internal18{19 /// <summary>20 /// Loads InternalCommand objects and executes them.21 /// </summary>22 /// <remarks>23 /// The PipelineProcessor class is not thread-safe, so methods such as24 /// AddCommand and SynchronousExecute should not be called25 /// simultaneously. While SynchronousExecute is running, it may access26 /// ExternalInput, ExternalSuccessOutput and ExternalErrorOutput, and27 /// those objects are thread-safe.28 /// </remarks>29 internal class PipelineProcessor : IDisposable30 {31 #region private_members32 33 private readonly CancellationTokenSource _pipelineStopTokenSource = new CancellationTokenSource();34 private List<CommandProcessorBase> _commands = new List<CommandProcessorBase>();35 private List<PipelineProcessor> _redirectionPipes;36 private PipelineReader<object> _externalInputPipe;37 private PipelineWriter _externalSuccessOutput;38 private PipelineWriter _externalErrorOutput;39 private bool _executionStarted = false;40 private bool _stopping = false;41 private SessionStateScope _executionScope;42 43 private ExceptionDispatchInfo _firstTerminatingError = null;44 45 private bool _linkedSuccessOutput = false;46 private bool _linkedErrorOutput = false;47 48 private NativeCommandProcessor _lastNativeCommand;49 50 private bool _haveReportedNativePipeUsage;51 52#if !CORECLR // Impersonation Not Supported On CSS53 // This is the security context when the pipeline was allocated54 internal System.Security.SecurityContext SecurityContext =55 System.Security.SecurityContext.Capture();56#endif57 #endregion private_members58 59 #region IDispose60 61 private bool _disposed = false;62 63 /// <summary>64 /// When the command is complete, PipelineProcessor will be65 /// disposed.66 /// </summary>67 /// <remarks>68 /// This is only public because it implements an interface method.69 /// The class itself is internal.70 /// We use the standard IDispose pattern.71 /// </remarks>72 public void Dispose()73 {74 Dispose(true);75 }76 77 private void Dispose(bool disposing)78 {79 if (_disposed)80 return;81 82 if (disposing)83 {84 DisposeCommands();85 _localPipeline = null;86 _externalSuccessOutput = null;87 _externalErrorOutput = null;88 _executionScope = null;89 _eventLogBuffer = null;90 _pipelineStopTokenSource.Dispose();91#if !CORECLR // Impersonation Not Supported On CSS92 SecurityContext.Dispose();93 SecurityContext = null;94#endif95 }96 97 _disposed = true;98 }99 100 #endregion IDispose101 102 #region Execution Logging103 104 private bool _executionFailed = false;105 106 internal List<CommandProcessorBase> Commands107 {108 get { return _commands; }109 }110 111 internal bool ExecutionFailed112 {113 get114 {115 return _executionFailed;116 }117 118 set119 {120 _executionFailed = value;121 }122 }123 124 /// <summary>125 /// Gets the CancellationToken that is signaled when the pipeline is stopping.126 /// </summary>127 internal CancellationToken PipelineStopToken => _pipelineStopTokenSource.Token;128 129 internal void LogExecutionInfo(InvocationInfo invocationInfo, string text)130 {131 string message = StringUtil.Format(PipelineStrings.PipelineExecutionInformation, GetCommand(invocationInfo), text);132 Log(message, invocationInfo, PipelineExecutionStatus.Started);133 }134 135 internal void LogExecutionComplete(InvocationInfo invocationInfo, string text)136 {137 string message = StringUtil.Format(PipelineStrings.PipelineExecutionInformation, GetCommand(invocationInfo), text);138 Log(message, invocationInfo, PipelineExecutionStatus.Complete);139 }140 141 internal void LogPipelineComplete()142 {143 Log(null, null, PipelineExecutionStatus.PipelineComplete);144 }145 146 internal void LogExecutionParameterBinding(InvocationInfo invocationInfo, string parameterName, string parameterValue)147 {148 string message = StringUtil.Format(PipelineStrings.PipelineExecutionParameterBinding, GetCommand(invocationInfo), parameterName, parameterValue);149 Log(message, invocationInfo, PipelineExecutionStatus.ParameterBinding);150 }151 152 internal void LogExecutionError(InvocationInfo invocationInfo, ErrorRecord errorRecord)153 {154 if (errorRecord == null)155 return;156 157 string message = StringUtil.Format(PipelineStrings.PipelineExecutionNonTerminatingError, GetCommand(invocationInfo), errorRecord.ToString());158 Log(message, invocationInfo, PipelineExecutionStatus.Error);159 }160 161 private bool _terminatingErrorLogged = false;162 163 internal void LogExecutionException(Exception exception)164 {165 _executionFailed = true;166 167 // Only log one terminating error for pipeline execution.168 if (_terminatingErrorLogged)169 return;170 171 _terminatingErrorLogged = true;172 173 if (exception == null)174 return;175 176 string message = StringUtil.Format(PipelineStrings.PipelineExecutionTerminatingError, GetCommand(exception), exception.Message);177 Log(message, null, PipelineExecutionStatus.Error);178 }179 180 private static string GetCommand(InvocationInfo invocationInfo)181 {182 if (invocationInfo == null)183 return string.Empty;184 185 if (invocationInfo.MyCommand != null)186 {187 return invocationInfo.MyCommand.Name;188 }189 190 return string.Empty;191 }192 193 private static string GetCommand(Exception exception)194 {195 IContainsErrorRecord icer = exception as IContainsErrorRecord;196 if (icer != null && icer.ErrorRecord != null)197 return GetCommand(icer.ErrorRecord.InvocationInfo);198 199 return string.Empty;200 }201 202 private void Log(string logElement, InvocationInfo invocation, PipelineExecutionStatus pipelineExecutionStatus)203 {204 System.Management.Automation.Host.PSHostUserInterface hostInterface = null;205 if (this.LocalPipeline != null)206 {207 hostInterface = this.LocalPipeline.Runspace.GetExecutionContext.EngineHostInterface.UI;208 }209 210 // Acknowledge command completion211 if (hostInterface != null)212 {213 if (pipelineExecutionStatus == PipelineExecutionStatus.Complete)214 {215 hostInterface.TranscribeCommandComplete(invocation);216 return;217 }218 else if (pipelineExecutionStatus == PipelineExecutionStatus.PipelineComplete)219 {220 hostInterface.TranscribePipelineComplete();221 return;222 }223 }224 225 // Log the cmdlet invocation execution details if we didn't have an associated script line with it.226 if ((invocation == null) || string.IsNullOrEmpty(invocation.Line))227 {228 hostInterface?.TranscribeCommand(logElement, invocation);229 }230 231 if (_needToLog && !string.IsNullOrEmpty(logElement))232 {233 _eventLogBuffer ??= new List<string>();234 _eventLogBuffer.Add(logElement);235 }236 }237 238 private void LogToEventLog()239 {240 // We check to see if there is anything in the buffer before we flush it.241 // Flushing the empty buffer causes a measurable performance degradation.242 if (_commands?.Count > 0 && _eventLogBuffer?.Count > 0)243 {244 InternalCommand firstCmd = _commands[0].Command;245 MshLog.LogPipelineExecutionDetailEvent(246 firstCmd.Context,247 _eventLogBuffer,248 firstCmd.MyInvocation);249 }250 251 // Clear the log buffer after writing the event.252 _eventLogBuffer?.Clear();253 }254 255 private bool _needToLog = false;256 private List<string> _eventLogBuffer;257 258 #endregion259 260 #region public_methods261 262 /// <summary>263 /// Add a single InternalCommand to the end of the pipeline.264 /// </summary>265 /// <returns>Results from last pipeline stage.</returns>266 /// <exception cref="InvalidOperationException">267 /// see AddCommand268 /// </exception>269 /// <exception cref="ObjectDisposedException"></exception>270 internal int Add(CommandProcessorBase commandProcessor)271 {272 if (commandProcessor is NativeCommandProcessor nativeCommand)273 {274 if (_lastNativeCommand is not null)275 {276 // Only report experimental feature usage once per pipeline.277 if (!_haveReportedNativePipeUsage)278 {279 ApplicationInsightsTelemetry.SendExperimentalUseData("PSNativeCommandPreserveBytePipe", "p");280 _haveReportedNativePipeUsage = true;281 }282 283 _lastNativeCommand.DownStreamNativeCommand = nativeCommand;284 nativeCommand.UpstreamIsNativeCommand = true;285 }286 287 _lastNativeCommand = nativeCommand;288 }289 else290 {291 _lastNativeCommand = null;292 }293 294 commandProcessor.CommandRuntime.PipelineProcessor = this;295 return AddCommand(commandProcessor, _commands.Count, readErrorQueue: false);296 }297 298 internal void AddRedirectionPipe(PipelineProcessor pipelineProcessor)299 {300 if (pipelineProcessor is null)301 {302 throw PSTraceSource.NewArgumentNullException(nameof(pipelineProcessor));303 }304 305 _redirectionPipes ??= new List<PipelineProcessor>();306 _redirectionPipes.Add(pipelineProcessor);307 }308 309 // 2004/02/28-JSnover (from spec review) ReadFromErrorQueue310 // should be an int or enum to allow for more queues311 // 2005/03/08-JonN: This is an internal API312 /// <summary>313 /// Add a command to the pipeline.314 /// </summary>315 /// <param name="commandProcessor"></param>316 /// <param name="readFromCommand">Reference number of command from which to read, 0 for none.</param>317 /// <param name="readErrorQueue">Read from error queue of command readFromCommand.</param>318 /// <returns>Reference number of this command for use in readFromCommand.</returns>319 /// <exception cref="ObjectDisposedException"></exception>320 /// <exception cref="ArgumentException">321 /// FirstCommandCannotHaveInput: <paramref name="readFromCommand"/> must be zero322 /// for the first command in the pipe323 /// InvalidCommandNumber: there is no command numbered <paramref name="readFromCommand"/>324 /// A command can only read from earlier commands; this prevents circular queues325 /// </exception>326 /// <exception cref="InvalidOperationException">327 /// ExecutionAlreadyStarted: pipeline has already started or completed328 /// PipeAlreadyTaken: the downstream pipe of command <paramref name="readFromCommand"/>329 /// is already taken330 /// </exception>331 private int AddCommand(CommandProcessorBase commandProcessor, int readFromCommand, bool readErrorQueue)332 {333 if (commandProcessor == null)334 {335 throw PSTraceSource.NewArgumentNullException(nameof(commandProcessor));336 }337 338 if (_commands == null)339 {340 // "_commands == null"341 throw PSTraceSource.NewInvalidOperationException();342 }343 344 if (_disposed)345 {346 throw PSTraceSource.NewObjectDisposedException("PipelineProcessor");347 }348 349 if (_executionStarted)350 {351 throw PSTraceSource.NewInvalidOperationException(352 PipelineStrings.ExecutionAlreadyStarted);353 }354 355 if (commandProcessor.AddedToPipelineAlready)356 {357 throw PSTraceSource.NewInvalidOperationException(358 PipelineStrings.CommandProcessorAlreadyUsed);359 }360 361 if (_commands.Count == 0)362 {363 if (readFromCommand != 0)364 {365 // "First command cannot have input"366 throw PSTraceSource.NewArgumentException(367 nameof(readFromCommand),368 PipelineStrings.FirstCommandCannotHaveInput);369 }370 371 commandProcessor.AddedToPipelineAlready = true;372 }373 // 2003/08/11-JonN Subsequent commands must have predecessor374 else if (readFromCommand > _commands.Count || readFromCommand <= 0)375 {376 // "invalid command number"377 throw PSTraceSource.NewArgumentException(378 nameof(readFromCommand),379 PipelineStrings.InvalidCommandNumber);380 }381 else382 {383 var prevcommandProcessor = _commands[readFromCommand - 1] as CommandProcessorBase;384 ValidateCommandProcessorNotNull(prevcommandProcessor, errorMessage: null);385 386 Pipe UpstreamPipe = (readErrorQueue)387 ? prevcommandProcessor.CommandRuntime.ErrorOutputPipe388 : prevcommandProcessor.CommandRuntime.OutputPipe;389 390 if (UpstreamPipe == null)391 {392 throw PSTraceSource.NewInvalidOperationException();393 }394 395 if (UpstreamPipe.DownstreamCmdlet != null)396 {397 throw PSTraceSource.NewInvalidOperationException(398 PipelineStrings.PipeAlreadyTaken);399 }400 401 commandProcessor.AddedToPipelineAlready = true;402 403 commandProcessor.CommandRuntime.InputPipe = UpstreamPipe;404 UpstreamPipe.DownstreamCmdlet = commandProcessor;405 406 // 2004/09/14-JonN This code could be moved to SynchronousExecute407 // if this setting needed to bind at a later time408 // than AddCommand.409 if (commandProcessor.CommandRuntime.MergeUnclaimedPreviousErrorResults)410 {411 for (int i = 0; i < _commands.Count; i++)412 {413 prevcommandProcessor = _commands[i];414 ValidateCommandProcessorNotNull(prevcommandProcessor, errorMessage: null);415 416 // check whether the error output is already claimed417 if (prevcommandProcessor.CommandRuntime.ErrorOutputPipe.DownstreamCmdlet != null)418 continue;419 if (prevcommandProcessor.CommandRuntime.ErrorOutputPipe.ExternalWriter != null)420 continue;421 422 // Set the upstream cmdlet's error output to go down423 // the same pipe as the downstream cmdlet's input424 prevcommandProcessor.CommandRuntime.ErrorOutputPipe = UpstreamPipe;425 }426 }427 }428 429 _commands.Add(commandProcessor);430 431 // We will log event(s) about the pipeline execution details if any command in the pipeline requests that.432 _needToLog |= commandProcessor.CommandRuntime.LogPipelineExecutionDetail;433 434 // We give the Command a pointer back to the435 // PipelineProcessor so that it can check whether the436 // command has been stopped.437 commandProcessor.CommandRuntime.PipelineProcessor = this;438 439 return _commands.Count;440 }441 442 // 2005/03/08-JonN: This is an internal API443 /// <summary>444 /// Execute the accumulated commands and clear the pipeline.445 /// SynchronousExecute does not return until all commands have446 /// completed. There is no asynchronous variant; instead, once the447 /// pipeline is set up, the caller can spawn a thread and call448 /// SynchronousExecute from that thread. This does not mean that449 /// PipelineProcessor is thread-safe; once SynchronousExecute is450 /// running, PipelineProcessor should not be accessed through any451 /// other means. This variant of the routine looks at it's input452 /// object to see if it's enumerable or not.453 /// </summary>454 /// <param name="input">455 /// Input objects for first stage. If this is AutomationNull.Value, the456 /// first cmdlet is the beginning of the pipeline.457 /// </param>458 /// <returns>459 /// Results from last pipeline stage. This will be empty if460 /// ExternalSuccessOutput is set.461 /// </returns>462 /// <exception cref="InvalidOperationException">463 /// ExecutionAlreadyStarted: pipeline has already started or completed464 /// </exception>465 /// <exception cref="InvalidOperationException">466 /// PipelineExecuteRequiresAtLeastOneCommand467 /// </exception>468 /// <exception cref="CmdletInvocationException">469 /// A cmdlet encountered a terminating error470 /// </exception>471 /// <exception cref="PipelineStoppedException">472 /// The pipeline was stopped asynchronously473 /// </exception>474 /// <exception cref="ActionPreferenceStopException">475 /// The ActionPreference.Stop or ActionPreference.Inquire policy476 /// triggered a terminating error.477 /// </exception>478 /// <exception cref="ParameterBindingException">479 /// If any parameters fail to bind,480 /// or481 /// If any mandatory parameters are missing.482 /// </exception>483 /// <exception cref="MetadataException">484 /// If there is an error generating the metadata for dynamic parameters.485 /// </exception>486 /// <exception cref="ExtendedTypeSystemException">487 /// An error occurred clearing the error variable.488 /// </exception>489 /// <exception cref="HaltCommandException">490 /// HaltCommandException will cause the command491 /// to stop, but should not be reported as an error.492 /// </exception>493 internal Array SynchronousExecuteEnumerate(object input)494 {495 if (Stopping)496 {497 throw new PipelineStoppedException();498 }499 500 bool pipelineSucceeded = false;501 ExceptionDispatchInfo toRethrowInfo = null;502 CommandProcessorBase commandRequestingUpstreamCommandsToStop = null;503 504 try505 {506 try507 {508 try509 {510 // If the caller specified an input object array, we run assuming there is an incoming "stream"511 // of objects. This will prevent the one default call to ProcessRecord on the first command.512 Start(incomingStream: input != AutomationNull.Value);513 514 // Start has already validated firstcommandProcessor515 CommandProcessorBase firstCommandProcessor = _commands[0];516 517 // Add any input to the first command.518 if (ExternalInput is not null)519 {520 firstCommandProcessor.CommandRuntime.InputPipe.ExternalReader = ExternalInput;521 }522 523 Inject(input, enumerate: true);524 }525 catch (PipelineStoppedException)526 {527 if (_firstTerminatingError?.SourceException is StopUpstreamCommandsException exception)528 {529 _firstTerminatingError = null;530 commandRequestingUpstreamCommandsToStop = exception.RequestingCommandProcessor;531 }532 else533 {534 throw;535 }536 }537 538 DoCompleteCore(commandRequestingUpstreamCommandsToStop);539 pipelineSucceeded = true;540 }541 finally542 {543 // Clean up resources for script commands, no matter the pipeline succeeded or not.544 // This method catches and handles all exceptions inside, so it will never throw.545 Clean();546 }547 548 if (pipelineSucceeded)549 {550 // Now, we are sure all 'commandProcessors' hosted by the current 'pipelineProcessor' are done execution,551 // so if there are any redirection 'pipelineProcessors' associated with any of those 'commandProcessors',552 // they must have successfully executed 'StartStepping' and 'Step', and thus we should call 'DoComplete'553 // on them for completeness.554 if (_redirectionPipes is not null)555 {556 foreach (PipelineProcessor redirectPipelineProcessor in _redirectionPipes)557 {558 // The 'Clean' block for each 'commandProcessor' might still write to a pipe that is associated559 // with the redirection 'pipelineProcessor' (e.g. a redirected error pipe), which would trigger560 // the call to 'pipelineProcessor.Step'.561 // It's possible (though very unlikely) that the call to 'pipelineProcessor.Step' failed with an562 // exception, and in such case, the 'pipelineProcessor' would have been disposed, and therefore563 // the call to 'DoComplete' will simply return, because '_commands' was already set to null.564 redirectPipelineProcessor.DoCompleteCore(null);565 }566 }567 568 // The 'Clean' blocks write nothing to the output pipe, so the results won't be affected by them.569 return RetrieveResults();570 }571 }572 catch (RuntimeException e)573 {574 toRethrowInfo = GetFirstError(e);575 }576 finally577 {578 DisposeCommands();579 }580 581 // By rethrowing the exception outside of the handler, we allow the CLR on X64/IA64 to free from582 // the stack the exception records related to this exception.583 584 // The only reason we should get here is if an exception should be rethrown.585 Diagnostics.Assert(toRethrowInfo != null, "Alternate protocol path failure");586 toRethrowInfo.Throw();587 588 // UNREACHABLE589 return null;590 }591 592 private ExceptionDispatchInfo GetFirstError(RuntimeException e)593 {594 // The error we want to report is the first terminating error which occurred during pipeline execution,595 // regardless of whether other errors occurred afterward.596 var firstError = _firstTerminatingError ?? ExceptionDispatchInfo.Capture(e);597 LogExecutionException(firstError.SourceException);598 return firstError;599 }600 601 private void ThrowFirstErrorIfExisting(bool logException)602 {603 if (_firstTerminatingError != null)604 {605 if (logException)606 {607 LogExecutionException(_firstTerminatingError.SourceException);608 }609 610 _firstTerminatingError.Throw();611 }612 }613 614 private void DoCompleteCore(CommandProcessorBase commandRequestingUpstreamCommandsToStop)615 {616 if (_commands is null)617 {618 // This could happen to a redirection pipeline, either for an expression (e.g. 1 > a.txt)619 // or for a command (e.g. command > a.txt).620 // An exception may be thrown from the call to 'StartStepping' or 'Step' on the pipeline,621 // which causes the pipeline commands to be disposed.622 return;623 }624 625 // Call DoComplete() for all the commands, which will internally call Complete()626 MshCommandRuntime lastCommandRuntime = null;627 628 for (int i = 0; i < _commands.Count; i++)629 {630 CommandProcessorBase commandProcessor = _commands[i];631 632 if (commandProcessor is null)633 {634 // An internal error that should not happen.635 throw PSTraceSource.NewInvalidOperationException();636 }637 638 if (object.ReferenceEquals(commandRequestingUpstreamCommandsToStop, commandProcessor))639 {640 // Do not call DoComplete/EndProcessing on the command that initiated stopping.641 commandRequestingUpstreamCommandsToStop = null;642 continue;643 }644 645 if (commandRequestingUpstreamCommandsToStop is not null)646 {647 // Do not call DoComplete/EndProcessing on commands that were stopped upstream.648 continue;649 }650 651 try652 {653 commandProcessor.DoComplete();654 }655 catch (PipelineStoppedException)656 {657 if (_firstTerminatingError?.SourceException is StopUpstreamCommandsException exception)658 {659 _firstTerminatingError = null;660 commandRequestingUpstreamCommandsToStop = exception.RequestingCommandProcessor;661 }662 else663 {664 throw;665 }666 }667 668 EtwActivity.SetActivityId(commandProcessor.PipelineActivityId);669 670 // Log a command stopped event671 MshLog.LogCommandLifecycleEvent(672 commandProcessor.Command.Context,673 CommandState.Stopped,674 commandProcessor.Command.MyInvocation);675 676 // Log the execution of a command (not script chunks, as they are not commands in and of themselves).677 if (commandProcessor.CommandInfo.CommandType != CommandTypes.Script)678 {679 LogExecutionComplete(commandProcessor.Command.MyInvocation, commandProcessor.CommandInfo.Name);680 }681 682 lastCommandRuntime = commandProcessor.CommandRuntime;683 }684 685 // Log the pipeline completion.686 if (lastCommandRuntime is not null)687 {688 // Only log the pipeline completion if this wasn't a nested pipeline, as689 // pipeline state in transcription is associated with the toplevel pipeline690 if (LocalPipeline is null || !LocalPipeline.IsNested)691 {692 lastCommandRuntime.PipelineProcessor.LogPipelineComplete();693 }694 }695 696 // If a terminating error occurred, report it now.697 // This pipeline could have been stopped asynchronously, by 'Ctrl+c' manually or698 // 'PowerShell.Stop' programatically. We need to check and see if that's the case.699 // An example:700 // - 'Start-Sleep' is running in this pipeline, and 'pipelineProcessor.Stop' gets701 // called on a different thread, which sets a 'PipelineStoppedException' object702 // to '_firstTerminatingError' and runs 'StopProcessing' on 'Start-Sleep'.703 // - The 'StopProcessing' will cause 'Start-Sleep' to return from 'ProcessRecord'704 // call, and thus the pipeline execution will move forward to run 'DoComplete'705 // for the 'Start-Sleep' command and thus the code flow will reach here.706 // For this given example, we need to check '_firstTerminatingError' and throw out707 // the 'PipelineStoppedException' if the pipeline was indeed being stopped.708 ThrowFirstErrorIfExisting(logException: true);709 }710 711 /// <summary>712 /// Clean up resources for script commands in this pipeline processor.713 /// </summary>714 /// <remarks>715 /// Exception from a 'Clean' block is not allowed to propagate up and terminate the pipeline716 /// so that other 'Clean' blocks can run without being affected. Therefore, this method will717 /// catch and handle all exceptions inside, and it will never throw.718 /// </remarks>719 private void Clean()720 {721 if (!_executionStarted || _commands is null)722 {723 // Simply return if the pipeline execution wasn't even started, or the commands of724 // the pipeline have already been disposed.725 return;726 }727 728 // So far, if '_firstTerminatingError' is not null, then it must be a terminating error729 // thrown from one of 'Begin/Process/End' blocks. There can be terminating error thrown730 // from 'Clean' block as well, which needs to be handled in this method.731 // In order to capture the subsequent first terminating error thrown from 'Clean', we732 // need to forget the previous '_firstTerminatingError' value before calling 'DoClean'733 // on each command processor, so we have to save the old value here and restore later.734 ExceptionDispatchInfo oldFirstTerminatingError = _firstTerminatingError;735 736 // Suspend a stopping pipeline by setting 'IsStopping' to false and restore it afterwards.737 bool oldIsStopping = ExceptionHandlingOps.SuspendStoppingPipelineImpl(LocalPipeline);738 739 try740 {741 foreach (CommandProcessorBase commandProcessor in _commands)742 {743 if (commandProcessor is null || !commandProcessor.HasCleanBlock)744 {745 continue;746 }747 748 try749 {750 // Forget the terminating error we saw before, so a terminating error thrown751 // from the subsequent 'Clean' block can be recorded and handled properly.752 _firstTerminatingError = null;753 commandProcessor.DoCleanup();754 }755 catch (RuntimeException e)756 {757 // Retrieve and report the terminating error that was thrown in the 'Clean' block.758 ExceptionDispatchInfo firstError = GetFirstError(e);759 commandProcessor.ReportCleanupError(firstError.SourceException);760 }761 catch (Exception ex)762 {763 // Theoretically, only 'RuntimeException' could be thrown out, but we catch764 // all and log them here just to be safe.765 // Skip special flow control exceptions and log others.766 if (ex is not FlowControlException && ex is not HaltCommandException)767 {768 MshLog.LogCommandHealthEvent(commandProcessor.Context, ex, Severity.Warning);769 }770 }771 }772 }773 finally774 {775 _firstTerminatingError = oldFirstTerminatingError;776 ExceptionHandlingOps.RestoreStoppingPipelineImpl(LocalPipeline, oldIsStopping);777 }778 }779 780 /// <summary>781 /// Clean up resources for the script commands of a steppable pipeline.782 /// </summary>783 /// <remarks>784 /// The way we handle 'Clean' blocks in 'StartStepping', 'Step', and 'DoComplete' makes sure that:785 /// 1. The 'Clean' blocks get to run if any exception is thrown from the pipeline execution.786 /// 2. The 'Clean' blocks get to run if the pipeline runs to the end successfully.787 /// However, this is not enough for a steppable pipeline, because the function, where the steppable788 /// pipeline gets used, may fail (think about a proxy function). And that may lead to the situation789 /// where "no exception was thrown from the steppable pipeline" but "the steppable pipeline didn't790 /// run to the end". In that case, 'Clean' won't run unless it's triggered explicitly on the steppable791 /// pipeline. This method is how we will expose this functionality to 'SteppablePipeline'.792 /// </remarks>793 internal void DoCleanup()794 {795 Clean();796 DisposeCommands();797 }798 799 /// <summary>800 /// Implements DoComplete as a stand-alone function for completing801 /// the execution of a steppable pipeline.802 /// </summary>803 /// <returns>The results of the execution.</returns>804 internal Array DoComplete()805 {806 if (!_executionStarted)807 {808 throw PSTraceSource.NewInvalidOperationException(809 PipelineStrings.PipelineNotStarted);810 }811 812 try813 {814 if (Stopping)815 {816 throw new PipelineStoppedException();817 }818 819 ExceptionDispatchInfo toRethrowInfo;820 try821 {822 DoCompleteCore(null);823 return RetrieveResults();824 }825 catch (RuntimeException e)826 {827 toRethrowInfo = GetFirstError(e);828 }829 830 // By rethrowing the exception outside of the handler, we allow the CLR on X64/IA64 to free from the stack831 // the exception records related to this exception.832 833 // The only reason we should get here is an exception should be rethrown.834 Diagnostics.Assert(toRethrowInfo != null, "Alternate protocol path failure");835 toRethrowInfo.Throw();836 837 // UNREACHABLE838 return null;839 }840 finally841 {842 Clean();843 DisposeCommands();844 }845 }846 847 /// <summary>848 /// This routine starts the stepping process. It is optional to call this but can be useful849 /// if you want the begin clauses of the pipeline to be run even when there may not be any850 /// input to process as is the case for I/O redirection into a file. We still want the file851 /// opened, even if there was nothing to write to it.852 /// </summary>853 /// <param name="expectInput">True if you want to write to this pipeline.</param>854 internal void StartStepping(bool expectInput)855 {856 bool startSucceeded = false;857 try858 {859 Start(expectInput);860 startSucceeded = true;861 862 // Check if this pipeline is being stopped asynchronously.863 ThrowFirstErrorIfExisting(logException: false);864 }865 catch (Exception e)866 {867 Clean();868 DisposeCommands();869 870 if (!startSucceeded && e is PipelineStoppedException)871 {872 // When a terminating error happens during command execution, PowerShell will first save it873 // to '_firstTerminatingError', and then throw a 'PipelineStoppedException' to tear down the874 // pipeline. So when the caught exception here is 'PipelineStoppedException', it may not be875 // the actual original terminating error.876 // In this case, we want to report the first terminating error which occurred during pipeline877 // execution, regardless of whether other errors occurred afterward.878 ThrowFirstErrorIfExisting(logException: false);879 }880 881 throw;882 }883 }884 885 /// <summary>886 /// Request that the pipeline execution should stop. Unlike other887 /// methods of PipelineProcessor, this method can be called888 /// asynchronously.889 /// </summary>890 internal void Stop()891 {892 // Only call StopProcessing if the pipeline is being stopped893 // for the first time894 895 if (!RecordFailure(new PipelineStoppedException(), command: null))896 {897 return;898 }899 900 // Retain copy of _commands in case Dispose() is called901 List<CommandProcessorBase> commands = _commands;902 if (commands is null)903 {904 return;905 }906 907 _pipelineStopTokenSource.Cancel();908 909 // Call StopProcessing() for all the commands.910 foreach (CommandProcessorBase commandProcessor in commands)911 {912 if (commandProcessor == null)913 {914 throw PSTraceSource.NewInvalidOperationException();915 }916 917 try918 {919 commandProcessor.Command.DoStopProcessing();920 }921 catch (Exception)922 {923 // We swallow exceptions which occur during StopProcessing.924 continue;925 }926 }927 }928 929 #endregion public_methods930 931 #region private_methods932 933 /// <summary>934 /// Partially execute the pipeline, and retrieve the output935 /// after the input objects have been entered into the pipe.936 /// </summary>937 /// <param name="input">938 /// Array of input objects for first stage939 /// </param>940 /// <returns>941 /// Results from last pipeline stage. This will be empty if942 /// ExternalSuccessOutput is set.943 /// </returns>944 /// <exception cref="InvalidOperationException">945 /// PipelineExecuteRequiresAtLeastOneCommand946 /// </exception>947 /// <exception cref="PipelineStoppedException">948 /// The pipeline has already been stopped, or a cmdlet encountered949 /// a terminating error950 /// </exception>951 /// <exception cref="ParameterBindingException">952 /// If any parameters fail to bind,953 /// or954 /// If any mandatory parameters are missing.955 /// </exception>956 /// <exception cref="MetadataException">957 /// If there is an error generating the metadata for dynamic parameters.958 /// </exception>959 /// <exception cref="PipelineStoppedException">960 /// The pipeline has already been stopped,961 /// or a terminating error occurred.962 /// </exception>963 /// <exception cref="ExtendedTypeSystemException">964 /// An error occurred clearing the error variable.965 /// </exception>966 internal Array Step(object input)967 {968 bool injectSucceeded = false;969 try970 {971 Start(true);972 Inject(input, enumerate: false);973 injectSucceeded = true;974 975 // Check if this pipeline is being stopped asynchronously.976 ThrowFirstErrorIfExisting(logException: false);977 return RetrieveResults();978 }979 catch (Exception e)980 {981 Clean();982 DisposeCommands();983 984 if (!injectSucceeded && e is PipelineStoppedException)985 {986 // When a terminating error happens during command execution, PowerShell will first save it987 // to '_firstTerminatingError', and then throw a 'PipelineStoppedException' to tear down the988 // pipeline. So when the caught exception here is 'PipelineStoppedException', it may not be989 // the actual original terminating error.990 // In this case, we want to report the first terminating error which occurred during pipeline991 // execution, regardless of whether other errors occurred afterward.992 ThrowFirstErrorIfExisting(logException: false);993 }994 995 throw;996 }997 }998 999 /// <summary>1000 /// Prepares the pipeline for execution.1001 /// </summary>1002 /// <param name="incomingStream">1003 /// Input objects are expected, so do not close the first command.1004 /// This will prevent the one default call to ProcessRecord1005 /// on the first command.1006 /// </param>1007 /// <remarks>1008 /// Start must always be called in a context where terminating errors will1009 /// be caught and result in DisposeCommands.1010 /// </remarks>1011 /// <exception cref="InvalidOperationException">1012 /// PipelineExecuteRequiresAtLeastOneCommand1013 /// </exception>1014 /// <exception cref="ParameterBindingException">1015 /// If any parameters fail to bind,1016 /// or1017 /// If any mandatory parameters are missing.1018 /// </exception>1019 /// <exception cref="MetadataException">1020 /// If there is an error generating the metadata for dynamic parameters.1021 /// </exception>1022 /// <exception cref="PipelineStoppedException">1023 /// The pipeline has already been stopped,1024 /// or a terminating error occurred in a downstream cmdlet.1025 /// </exception>1026 /// <exception cref="ExtendedTypeSystemException">1027 /// An error occurred clearing the error variable.1028 /// </exception>1029 private void Start(bool incomingStream)1030 {1031 // Every call to Step or SynchronousExecute will call Start.1032 if (_disposed)1033 {1034 throw PSTraceSource.NewObjectDisposedException("PipelineProcessor");1035 }1036 1037 if (Stopping)1038 {1039 throw new PipelineStoppedException();1040 }1041 1042 if (_executionStarted)1043 {1044 return;1045 }1046 1047 if (_commands == null || _commands.Count == 0)1048 {1049 throw PSTraceSource.NewInvalidOperationException(1050 PipelineStrings.PipelineExecuteRequiresAtLeastOneCommand);1051 }1052 1053 CommandProcessorBase firstcommandProcessor = _commands[0];1054 ValidateCommandProcessorNotNull(firstcommandProcessor, PipelineStrings.PipelineExecuteRequiresAtLeastOneCommand);1055 1056 // Set the execution scope using the current scope1057 _executionScope ??= firstcommandProcessor.Context.EngineSessionState.CurrentScope;1058 1059 // add ExternalSuccessOutput to the last command1060 CommandProcessorBase LastCommandProcessor = _commands[_commands.Count - 1];1061 ValidateCommandProcessorNotNull(LastCommandProcessor, errorMessage: null);1062 1063 if (ExternalSuccessOutput != null)1064 {1065 LastCommandProcessor.CommandRuntime.OutputPipe.ExternalWriter = ExternalSuccessOutput;1066 }1067 1068 // add ExternalErrorOutput to all commands whose error1069 // output is not yet claimed1070 SetExternalErrorOutput();1071 1072 if (ExternalInput == null && !incomingStream)1073 {1074 // no upstream cmdlet from the first command1075 firstcommandProcessor.CommandRuntime.IsClosed = true;1076 }1077 1078 // We want the value of PSDefaultParameterValues before possibly changing to the commands scopes.1079 // This ensures we use the value from the caller's scope, not the callee's scope.1080 IDictionary psDefaultParameterValues =1081 firstcommandProcessor.Context.GetVariableValue(SpecialVariables.PSDefaultParameterValuesVarPath, false) as IDictionary;1082 1083 _executionStarted = true;1084 1085 // Allocate the pipeline iteration array; note that the pipeline position for1086 // each command starts at 1 so we need to allocate _commands.Count + 1 items.1087 int[] pipelineIterationInfo = new int[_commands.Count + 1];1088 1089 // Prepare all commands from Engine's side, and make sure they are all valid1090 for (int i = 0; i < _commands.Count; i++)1091 {1092 CommandProcessorBase commandProcessor = _commands[i];1093 if (commandProcessor == null)1094 {1095 // "null command " + i1096 throw PSTraceSource.NewInvalidOperationException();1097 }1098 1099 // Generate new Activity Id for the thread1100 Guid pipelineActivityId = EtwActivity.CreateActivityId();1101 EtwActivity.SetActivityId(pipelineActivityId);1102 commandProcessor.PipelineActivityId = pipelineActivityId;1103 1104 // Log a command started event1105 MshLog.LogCommandLifecycleEvent(1106 commandProcessor.Context,1107 CommandState.Started,1108 commandProcessor.Command.MyInvocation);1109 1110#if LEGACYTELEMETRY1111 Microsoft.PowerShell.Telemetry.Internal.TelemetryAPI.TraceExecutedCommand(commandProcessor.Command.CommandInfo, commandProcessor.Command.CommandOrigin);1112#endif1113 1114 // Log the execution of a command (not script chunks, as they are not commands in and of themselves)1115 if (commandProcessor.CommandInfo.CommandType != CommandTypes.Script)1116 {1117 LogExecutionInfo(commandProcessor.Command.MyInvocation, commandProcessor.CommandInfo.Name);1118 }1119 1120 InvocationInfo myInfo = commandProcessor.Command.MyInvocation;1121 myInfo.PipelinePosition = i + 1;1122 myInfo.PipelineLength = _commands.Count;1123 myInfo.PipelineIterationInfo = pipelineIterationInfo;1124 myInfo.ExpectingInput = commandProcessor.IsPipelineInputExpected();1125 commandProcessor.DoPrepare(psDefaultParameterValues);1126 }1127 1128 // Clear ErrorVariable as appropriate1129 SetupParameterVariables();1130 1131 // Prepare all commands from Command's side.1132 // Note that DoPrepare() and DoBegin() should NOT be combined1133 // in a single for loop.1134 // Reason: Encoding of commandline parameters happen1135 // as part of DoPrepare(). If they are combined,1136 // the first command's DoBegin() will be called before1137 // the next command's DoPrepare(). Since BeginProcessing()1138 // can write objects to the downstream commandlet,1139 // it will end up calling DoExecute() (from Pipe.Add())1140 // before DoPrepare.1141 for (int i = 0; i < _commands.Count; i++)1142 {1143 CommandProcessorBase commandProcessor = _commands[i];1144 1145 commandProcessor.DoBegin();1146 }1147 }1148 1149 /// <summary>1150 /// Add ExternalErrorOutput to all commands whose error output is not yet claimed.1151 /// </summary>1152 private void SetExternalErrorOutput()1153 {1154 if (ExternalErrorOutput != null)1155 {1156 for (int i = 0; i < _commands.Count; i++)1157 {1158 CommandProcessorBase commandProcessor = _commands[i];1159 Pipe errorPipe = commandProcessor.CommandRuntime.ErrorOutputPipe;1160 1161 // check whether a cmdlet is consuming the error pipe1162 if (!errorPipe.IsRedirected)1163 {1164 errorPipe.ExternalWriter = ExternalErrorOutput;1165 }1166 }1167 }1168 }1169 1170 /// <summary>1171 /// Clear ErrorVariable as appropriate.1172 /// </summary>1173 private void SetupParameterVariables()1174 {1175 foreach (CommandProcessorBase commandProcessor in _commands)1176 {1177 ValidateCommandProcessorNotNull(commandProcessor, errorMessage: null);1178 1179 commandProcessor.CommandRuntime.SetupOutVariable();1180 commandProcessor.CommandRuntime.SetupErrorVariable();1181 commandProcessor.CommandRuntime.SetupWarningVariable();1182 commandProcessor.CommandRuntime.SetupPipelineVariable();1183 commandProcessor.CommandRuntime.SetupInformationVariable();1184 }1185 }1186 1187 private static void ValidateCommandProcessorNotNull(CommandProcessorBase commandProcessor, string errorMessage)1188 {1189 if (commandProcessor?.CommandRuntime is null)1190 {1191 throw errorMessage is null1192 ? PSTraceSource.NewInvalidOperationException()1193 : PSTraceSource.NewInvalidOperationException(errorMessage, Array.Empty<object>());1194 }1195 }1196 1197 /// <summary>1198 /// Partially execute the pipeline. The output remains in1199 /// the pipes.1200 /// </summary>