PowerShell/src/Microsoft.PowerShell.ConsoleHost/host/msh/Executor.cs
xtqqczze 883ca98dd7
Seal private classes (#15725)
* Seal private classes

* Fix CS0509

* Fix CS0628
2021-07-19 14:09:12 +05:00

740 lines
28 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using Dbg = System.Management.Automation.Diagnostics;
namespace Microsoft.PowerShell
{
/// <summary>
/// Executor wraps a Pipeline instance, and provides helper methods for executing commands in that pipeline. It is used to
/// provide bookkeeping and structure to the use of pipeline in such a way that they can be interrupted and cancelled by a
/// break event handler, and track nesting of pipelines (which happens with interrupted input loops (aka subshells) and use
/// of tab-completion in prompts. The bookkeeping is necessary because the break handler is static and global, and there is
/// no means for tying a break handler to an instance of an object.
///
/// The class' instance methods manage a single pipeline. The class' static methods track the outstanding instances to
/// ensure that only one instance is 'active' (and therefore cancellable) at a time.
/// </summary>
internal class Executor
{
[Flags]
internal enum ExecutionOptions
{
None = 0x0,
AddOutputter = 0x01,
AddToHistory = 0x02,
ReadInputObjects = 0x04
}
/// <summary>
/// Constructs a new instance.
/// </summary>
/// <param name="parent">
/// A reference to the parent ConsoleHost that created this instance.
/// </param>
/// <param name="useNestedPipelines">
/// true if the executor is supposed to use nested pipelines; false if not.
/// </param>
/// <param name="isPromptFunctionExecutor">
/// True if the instance will be used to execute the prompt function, which will delay stopping the pipeline by some
/// milliseconds. This we prevent us from stopping the pipeline so quickly that when the user leans on the ctrl-c key
/// that the prompt "stops working" (because it is being stopped faster than it can run to completion).
/// </param>
internal Executor(ConsoleHost parent, bool useNestedPipelines, bool isPromptFunctionExecutor)
{
Dbg.Assert(parent != null, "parent should not be null");
_parent = parent;
this.useNestedPipelines = useNestedPipelines;
_isPromptFunctionExecutor = isPromptFunctionExecutor;
Reset();
}
#region async
// called on the pipeline thread
private void OutputObjectStreamHandler(object sender, EventArgs e)
{
// e is just an empty instance of EventArgs, so we ignore it. sender is the PipelineReader that raised it's
// DataReady event that calls this handler, which is the PipelineReader for the Output object stream.
PipelineReader<PSObject> reader = (PipelineReader<PSObject>)sender;
// we use NonBlockingRead instead of Read, as Read would block if the reader has no objects. While it would be
// inconsistent for this method to be called when there are no objects, since it will be called synchronously on
// the pipeline thread, blocking in this call until an object is streamed would deadlock the pipeline. So we
// prefer to take no chance of blocking.
Collection<PSObject> objects = reader.NonBlockingRead();
foreach (PSObject obj in objects)
{
_parent.OutputSerializer.Serialize(obj);
}
}
// called on the pipeline thread
private void ErrorObjectStreamHandler(object sender, EventArgs e)
{
// e is just an empty instance of EventArgs, so we ignore it. sender is the PipelineReader that raised it's
// DataReady event that calls this handler, which is the PipelineReader for the Error object stream.
PipelineReader<object> reader = (PipelineReader<object>)sender;
// we use NonBlockingRead instead of Read, as Read would block if the reader has no objects. While it would be
// inconsistent for this method to be called when there are no objects, since it will be called synchronously on
// the pipeline thread, blocking in this call until an object is streamed would deadlock the pipeline. So we
// prefer to take no chance of blocking.
Collection<object> objects = reader.NonBlockingRead();
foreach (object obj in objects)
{
_parent.ErrorSerializer.Serialize(obj);
}
}
/// <summary>
/// This method handles the failure in executing pipeline asynchronously.
/// </summary>
/// <param name="ex"></param>
private void AsyncPipelineFailureHandler(Exception ex)
{
ErrorRecord er = null;
IContainsErrorRecord cer = ex as IContainsErrorRecord;
if (cer != null)
{
er = cer.ErrorRecord;
// Exception inside the error record is ParentContainsErrorRecordException which
// doesn't have stack trace. Replace it with top level exception.
er = new ErrorRecord(er, ex);
}
if (er == null)
{
er = new ErrorRecord(ex, "ConsoleHostAsyncPipelineFailure", ErrorCategory.NotSpecified, null);
}
_parent.ErrorSerializer.Serialize(er);
}
private sealed class PipelineFinishedWaitHandle
{
internal PipelineFinishedWaitHandle(Pipeline p)
{
p.StateChanged += PipelineStateChangedHandler;
}
internal void Wait()
{
_eventHandle.WaitOne();
}
private void PipelineStateChangedHandler(object sender, PipelineStateEventArgs e)
{
if (
e.PipelineStateInfo.State == PipelineState.Completed
|| e.PipelineStateInfo.State == PipelineState.Failed
|| e.PipelineStateInfo.State == PipelineState.Stopped)
{
_eventHandle.Set();
}
}
private readonly System.Threading.ManualResetEvent _eventHandle = new System.Threading.ManualResetEvent(false);
}
internal void ExecuteCommandAsync(string command, out Exception exceptionThrown, ExecutionOptions options)
{
Dbg.Assert(!useNestedPipelines, "can't async invoke a nested pipeline");
Dbg.Assert(!string.IsNullOrEmpty(command), "command should have a value");
bool addToHistory = (options & ExecutionOptions.AddToHistory) > 0;
Pipeline tempPipeline = _parent.RunspaceRef.CreatePipeline(command, addToHistory, false);
ExecuteCommandAsyncHelper(tempPipeline, out exceptionThrown, options);
}
/// <summary>
/// Executes a pipeline in the console when we are running asnyc.
/// </summary>
/// <param name="tempPipeline">
/// The pipeline to execute.
/// </param>
/// <param name="exceptionThrown">
/// Any exception thrown trying to run the pipeline.
/// </param>
/// <param name="options">
/// The options to use to execute the pipeline.
/// </param>
internal void ExecuteCommandAsyncHelper(Pipeline tempPipeline, out Exception exceptionThrown, ExecutionOptions options)
{
Dbg.Assert(!_isPromptFunctionExecutor, "should not async invoke the prompt");
exceptionThrown = null;
Executor oldCurrent = CurrentExecutor;
CurrentExecutor = this;
lock (_instanceStateLock)
{
Dbg.Assert(_pipeline == null, "no other pipeline should exist");
_pipeline = tempPipeline;
}
try
{
if ((options & ExecutionOptions.AddOutputter) > 0 && _parent.OutputFormat == Serialization.DataFormat.Text)
{
// Tell the script command to merge it's output and error streams
if (tempPipeline.Commands.Count == 1)
{
tempPipeline.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
}
// then add out-default to the pipeline to render everything...
Command outDefault = new Command("Out-Default", /* isScript */false, /* useLocalScope */ true);
tempPipeline.Commands.Add(outDefault);
}
tempPipeline.Output.DataReady += OutputObjectStreamHandler;
tempPipeline.Error.DataReady += ErrorObjectStreamHandler;
PipelineFinishedWaitHandle pipelineWaiter = new PipelineFinishedWaitHandle(tempPipeline);
// close the input pipeline so the command will do something
// if we are not reading input
if ((options & Executor.ExecutionOptions.ReadInputObjects) == 0)
{
tempPipeline.Input.Close();
}
tempPipeline.InvokeAsync();
if ((options & ExecutionOptions.ReadInputObjects) > 0 && Console.IsInputRedirected)
{
// read input objects from stdin
WrappedDeserializer des = new WrappedDeserializer(_parent.InputFormat, "Input", _parent.ConsoleIn.Value);
while (!des.AtEnd)
{
object o = des.Deserialize();
if (o == null)
{
break;
}
try
{
tempPipeline.Input.Write(o);
}
catch (PipelineClosedException)
{
// This exception can occurs when input is closed. This can happen
// for various reasons. For ex:Command in the pipeline is invalid and
// command discovery throws exception which closes the pipeline and
// hence the Input pipe.
break;
}
}
des.End();
}
tempPipeline.Input.Close();
pipelineWaiter.Wait();
// report error if pipeline failed
if (tempPipeline.PipelineStateInfo.State == PipelineState.Failed && tempPipeline.PipelineStateInfo.Reason != null)
{
if (_parent.OutputFormat == Serialization.DataFormat.Text)
{
// Report the exception using normal error reporting
exceptionThrown = tempPipeline.PipelineStateInfo.Reason;
}
else
{
// serialize the error record
AsyncPipelineFailureHandler(tempPipeline.PipelineStateInfo.Reason);
}
}
}
catch (Exception e)
{
exceptionThrown = e;
}
finally
{
// Once we have the results, or an exception is thrown, we throw away the pipeline.
_parent.ui.ResetProgress();
CurrentExecutor = oldCurrent;
Reset();
}
}
#endregion async
internal Pipeline CreatePipeline()
{
if (useNestedPipelines)
{
return _parent.RunspaceRef.CreateNestedPipeline();
}
else
{
return _parent.RunspaceRef.CreatePipeline();
}
}
internal Pipeline CreatePipeline(string command, bool addToHistory)
{
Dbg.Assert(!string.IsNullOrEmpty(command), "command should have a value");
return _parent.RunspaceRef.CreatePipeline(command, addToHistory, useNestedPipelines);
}
/// <summary>
/// All calls to the Runspace to execute a command line must be done with this function, which properly synchronizes
/// access to the running pipeline between the main thread and the break handler thread. This synchronization is
/// necessary so that executions can be aborted with Ctrl-C (including evaluation of the prompt and collection of
/// command-completion candidates.
///
/// On any given Executor instance, ExecuteCommand should be called at most once at a time by any one thread. It is NOT
/// reentrant.
/// </summary>
/// <param name="command">
/// The command line to be executed. Must be non-null.
/// </param>
/// <param name="exceptionThrown">
/// Receives the Exception thrown by the execution of the command, if any. If no exception is thrown, then set to null.
/// Can be tested to see if the execution was successful or not.
/// </param>
/// <param name="options">
/// options to govern the execution
/// </param>
/// <returns>
/// the object stream resulting from the execution. May be null.
/// </returns>
internal Collection<PSObject> ExecuteCommand(string command, out Exception exceptionThrown, ExecutionOptions options)
{
Dbg.Assert(!string.IsNullOrEmpty(command), "command should have a value");
// Experimental:
// Check for implicit remoting commands that can be batched, and execute as batched if able.
if (ExperimentalFeature.IsEnabled("PSImplicitRemotingBatching"))
{
var addOutputter = ((options & ExecutionOptions.AddOutputter) > 0);
if (addOutputter &&
!_parent.RunspaceRef.IsRunspaceOverridden &&
_parent.RunspaceRef.Runspace.ExecutionContext.Modules != null &&
_parent.RunspaceRef.Runspace.ExecutionContext.Modules.IsImplicitRemotingModuleLoaded &&
Utils.TryRunAsImplicitBatch(command, _parent.RunspaceRef.Runspace))
{
exceptionThrown = null;
return null;
}
}
Pipeline tempPipeline = CreatePipeline(command, (options & ExecutionOptions.AddToHistory) > 0);
return ExecuteCommandHelper(tempPipeline, out exceptionThrown, options);
}
private static Command GetOutDefaultCommand(bool endOfStatement)
{
return new Command(command: "Out-Default",
isScript: false,
useLocalScope: true,
mergeUnclaimedPreviousErrorResults: true)
{
IsEndOfStatement = endOfStatement
};
}
internal Collection<PSObject> ExecuteCommandHelper(Pipeline tempPipeline, out Exception exceptionThrown, ExecutionOptions options)
{
Dbg.Assert(tempPipeline != null, "command should have a value");
exceptionThrown = null;
Collection<PSObject> results = null;
if ((options & ExecutionOptions.AddOutputter) > 0)
{
if (tempPipeline.Commands.Count < 2)
{
if (tempPipeline.Commands.Count == 1)
{
// Tell the script command to merge it's output and error streams.
tempPipeline.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
}
// Add Out-Default to the pipeline to render.
tempPipeline.Commands.Add(GetOutDefaultCommand(endOfStatement: false));
}
else
{
// For multiple commands/scripts we need to insert Out-Default at the end of each statement.
CommandCollection executeCommands = new CommandCollection();
foreach (var cmd in tempPipeline.Commands)
{
executeCommands.Add(cmd);
if (cmd.IsEndOfStatement)
{
// End of statement needs to pipe to Out-Default.
cmd.IsEndOfStatement = false;
executeCommands.Add(GetOutDefaultCommand(endOfStatement: true));
}
}
var lastCmd = executeCommands.Last();
if (!((lastCmd.CommandText != null) &&
(lastCmd.CommandText.Equals("Out-Default", StringComparison.OrdinalIgnoreCase)))
)
{
// Ensure pipeline output goes to Out-Default.
executeCommands.Add(GetOutDefaultCommand(endOfStatement: false));
}
tempPipeline.Commands.Clear();
foreach (var cmd in executeCommands)
{
tempPipeline.Commands.Add(cmd);
}
}
}
Executor oldCurrent = CurrentExecutor;
CurrentExecutor = this;
lock (_instanceStateLock)
{
Dbg.Assert(_pipeline == null, "no other pipeline should exist");
_pipeline = tempPipeline;
}
try
{
// blocks until all results are retrieved.
results = tempPipeline.Invoke();
}
catch (Exception e)
{
exceptionThrown = e;
}
finally
{
// Once we have the results, or an exception is thrown, we throw away the pipeline.
_parent.ui.ResetProgress();
CurrentExecutor = oldCurrent;
Reset();
}
return results;
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Needed by ProfileTests as mentioned in bug 140572")]
internal Collection<PSObject> ExecuteCommand(string command)
{
Collection<PSObject> result = null;
Exception e = null;
do
{
result = ExecuteCommand(command, out e, ExecutionOptions.None);
if (e != null)
{
break;
}
if (result == null)
{
break;
}
} while (false);
return result;
}
/// <summary>
/// Executes a command (by calling this.ExecuteCommand), and coerces the first result object to a string. Any Exception
/// thrown in the course of execution is returned thru the exceptionThrown parameter.
/// </summary>
/// <param name="command">
/// The command to execute. May be any valid monad command.
/// </param>
/// <param name="exceptionThrown">
/// Receives the Exception thrown by the execution of the command, if any. If no exception is thrown, then set to null.
/// Can be tested to see if the execution was successful or not.
/// </param>
/// <returns>
/// The string representation of the first result object returned, or null if an exception was thrown or no objects were
/// returned by the command.
/// </returns>
internal string ExecuteCommandAndGetResultAsString(string command, out Exception exceptionThrown)
{
exceptionThrown = null;
string result = null;
do
{
Collection<PSObject> streamResults = ExecuteCommand(command, out exceptionThrown, ExecutionOptions.None);
if (exceptionThrown != null)
{
break;
}
if (streamResults == null || streamResults.Count == 0)
{
break;
}
// we got back one or more objects. Pick off the first result.
if (streamResults[0] == null)
return string.Empty;
// And convert the base object into a string. We can't use the proxied
// ToString() on the PSObject because there is no default runspace
// available.
PSObject msho = streamResults[0] as PSObject;
if (msho != null)
result = msho.BaseObject.ToString();
else
result = streamResults[0].ToString();
}
while (false);
return result;
}
/// <summary>
/// Executes a command (by calling this.ExecuteCommand), and coerces the first result object to a bool. Any Exception
/// thrown in the course of execution is caught and ignored.
/// </summary>
/// <param name="command">
/// The command to execute. May be any valid monad command.
/// </param>
/// <returns>
/// The Nullable`bool representation of the first result object returned, or null if an exception was thrown or no
/// objects were returned by the command.
/// </returns>
internal bool? ExecuteCommandAndGetResultAsBool(string command)
{
Exception unused = null;
bool? result = ExecuteCommandAndGetResultAsBool(command, out unused);
return result;
}
/// <summary>
/// Executes a command (by calling this.ExecuteCommand), and coerces the first result object to a bool. Any Exception
/// thrown in the course of execution is returned thru the exceptionThrown parameter.
/// </summary>
/// <param name="command">
/// The command to execute. May be any valid monad command.
/// </param>
/// <param name="exceptionThrown">
/// Receives the Exception thrown by the execution of the command, if any. If no exception is thrown, then set to null.
/// Can be tested to see if the execution was successful or not.
/// </param>
/// <returns>
/// The Nullable`bool representation of the first result object returned, or null if an exception was thrown or no
/// objects were returned by the command.
/// </returns>
internal bool? ExecuteCommandAndGetResultAsBool(string command, out Exception exceptionThrown)
{
exceptionThrown = null;
Dbg.Assert(!string.IsNullOrEmpty(command), "command should have a value");
bool? result = null;
do
{
Collection<PSObject> streamResults = ExecuteCommand(command, out exceptionThrown, ExecutionOptions.None);
if (exceptionThrown != null)
{
break;
}
if (streamResults == null || streamResults.Count == 0)
{
break;
}
// we got back one or more objects.
result = (streamResults.Count > 1) || (LanguagePrimitives.IsTrue(streamResults[0]));
}
while (false);
return result;
}
/// <summary>
/// Cancels execution of the current instance. If the current instance is not running, then does nothing. Called in
/// response to a break handler, by the static Executor.Cancel method.
/// </summary>
private void Cancel()
{
// if there's a pipeline running, stop it.
lock (_instanceStateLock)
{
if (_pipeline != null && !_cancelled)
{
_cancelled = true;
if (_isPromptFunctionExecutor)
{
System.Threading.Thread.Sleep(100);
}
_pipeline.Stop();
}
}
}
internal void BlockCommandOutput()
{
RemotePipeline remotePipeline = _pipeline as RemotePipeline;
if (remotePipeline != null)
{
// Waits until queued data is handled.
remotePipeline.DrainIncomingData();
// Blocks any new data.
remotePipeline.SuspendIncomingData();
}
}
internal void ResumeCommandOutput()
{
RemotePipeline remotePipeline = _pipeline as RemotePipeline;
if (remotePipeline != null)
{
// Resumes data flow.
remotePipeline.ResumeIncomingData();
}
}
/// <summary>
/// Resets the instance to its post-ctor state. Does not cancel execution.
/// </summary>
private void Reset()
{
lock (_instanceStateLock)
{
_pipeline = null;
_cancelled = false;
}
}
/// <summary>
/// Makes the given instance the "current" instance, that is, the instance that will receive a Cancel call if the break
/// handler is triggered and calls the static Cancel method.
/// </summary>
/// <value>
/// The instance to make current. Null is allowed.
/// </value>
/// <remarks>
/// Here are some state-transition cases to illustrate the use of CurrentExecutor
///
/// null is current
/// p1.ExecuteCommand
/// set p1 as current
/// promptforparams
/// tab complete
/// p2.ExecuteCommand
/// set p2 as current
/// p2.Execute completes
/// restore old current to p1
/// p1.Execute completes
/// restore null as current
///
/// Here's another case:
/// null is current
/// p1.ExecuteCommand
/// set p1 as current
/// ShouldProcess - suspend
/// EnterNestedPrompt
/// set null as current so that break does not exit the subshell
/// evaluate prompt
/// p2.ExecuteCommand
/// set p2 as current
/// Execute completes
/// restore null as current
/// nested loop exit
/// restore p1 as current
///
/// Summary:
/// ExecuteCommand always saves/sets/restores CurrentExecutor
/// Host.EnterNestedPrompt always saves/clears/restores CurrentExecutor
/// </remarks>
internal static Executor CurrentExecutor
{
get
{
Executor result = null;
lock (s_staticStateLock)
{
result = s_currentExecutor;
}
return result;
}
set
{
lock (s_staticStateLock)
{
// null is acceptable.
s_currentExecutor = value;
}
}
}
/// <summary>
/// Cancels the execution of the current instance (the instance last passed to PushCurrentExecutor), if any. If no
/// instance is Current, then does nothing.
/// </summary>
internal static void CancelCurrentExecutor()
{
Executor temp = null;
lock (s_staticStateLock)
{
temp = s_currentExecutor;
}
if (temp != null)
{
temp.Cancel();
}
}
// These statics are threadsafe, as there can be only one instance of ConsoleHost in a process at a time, and access
// to currentExecutor is guarded by staticStateLock, and static initializers are run by the CLR at program init time.
private static Executor s_currentExecutor;
private static readonly object s_staticStateLock = new object();
private readonly ConsoleHost _parent;
private Pipeline _pipeline;
private bool _cancelled;
internal bool useNestedPipelines;
private readonly object _instanceStateLock = new object();
private readonly bool _isPromptFunctionExecutor;
}
} // namespace