PowerShell/src/System.Management.Automation/engine/remoting/client/Job.cs
xtqqczze 883ca98dd7
Seal private classes (#15725)
* Seal private classes

* Fix CS0509

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

4538 lines
162 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Management.Automation.Host;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Management.Automation.Remoting;
using System.Management.Automation.Remoting.Internal;
using System.Management.Automation.Runspaces;
using System.Management.Automation.Tracing;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using Microsoft.PowerShell.Commands;
using Dbg = System.Management.Automation.Diagnostics;
// Stops compiler from warning about unknown warnings
#pragma warning disable 1634, 1691
namespace System.Management.Automation
{
/// <summary>
/// Enumeration for job status values. Indicates the status
/// of the result object.
/// </summary>
public enum JobState
{
/// <summary>
/// Execution of command in job not started.
/// </summary>
NotStarted = 0,
/// <summary>
/// Execution of command in progress.
/// </summary>
Running = 1,
/// <summary>
/// Execution of command completed in all
/// computernames/runspaces.
/// </summary>
Completed = 2,
/// <summary>
/// An error was encountered when trying to executed
/// command in one or more computernames/runspaces.
/// </summary>
Failed = 3,
/// <summary>
/// Command execution is cancelled (stopped) in one or more
/// computernames/runspaces.
/// </summary>
Stopped = 4,
/// <summary>
/// Command execution is blocked (on user input host calls etc)
/// </summary>
Blocked = 5,
/// <summary>
/// The job has been suspended.
/// </summary>
Suspended = 6,
/// <summary>
/// The job is a remote job and has been disconnected from the server.
/// </summary>
Disconnected = 7,
/// <summary>
/// Suspend is in progress.
/// </summary>
Suspending = 8,
/// <summary>
/// Stop is in progress.
/// </summary>
Stopping = 9,
/// <summary>
/// Script execution is halted in a debugger stop.
/// </summary>
AtBreakpoint = 10
}
/// <summary>
/// Defines exception which is thrown when state of the PSJob is different
/// from the expected state.
/// </summary>
[Serializable]
public class InvalidJobStateException : SystemException
{
/// <summary>
/// Creates a new instance of InvalidPSJobStateException class.
/// </summary>
public InvalidJobStateException()
: base
(
PSRemotingErrorInvariants.FormatResourceString
(
RemotingErrorIdStrings.InvalidJobStateGeneral
)
)
{
}
/// <summary>
/// Creates a new instance of InvalidPSJobStateException class.
/// </summary>
/// <param name="message">
/// The error message that explains the reason for the exception.
/// </param>
public InvalidJobStateException(string message)
: base(message)
{
}
/// <summary>
/// Creates a new instance of InvalidPSJobStateException class.
/// </summary>
/// <param name="message">
/// The error message that explains the reason for the exception.
/// </param>
/// <param name="innerException">
/// The exception that is the cause of the current exception.
/// </param>
public InvalidJobStateException(string message, Exception innerException)
: base(message, innerException)
{
}
/// <summary>
/// Creates a new instance of InvalidJobStateException class.
/// </summary>
/// <param name="currentState">
/// The Job State at the time of the error.
/// </param>
/// <param name="actionMessage">
/// An additional message that gives more information about the error. Used
/// for context after a generalized error message.
/// </param>
public InvalidJobStateException(JobState currentState, string actionMessage)
: base
(
PSRemotingErrorInvariants.FormatResourceString
(
RemotingErrorIdStrings.InvalidJobStateSpecific, currentState, actionMessage
)
)
{
_currState = currentState;
}
/// <summary>
/// Initializes a new instance of the InvalidPSJobStateException and defines value of
/// CurrentState.
/// </summary>
/// <param name="currentState">Current state of powershell.</param>
internal InvalidJobStateException(JobState currentState)
: base
(
PSRemotingErrorInvariants.FormatResourceString
(
RemotingErrorIdStrings.InvalidJobStateGeneral
)
)
{
_currState = currentState;
}
#region ISerializable Members
// No need to implement GetObjectData
// if all fields are static or [NonSerialized]
/// <summary>
/// Initializes a new instance of the InvalidPSJobStateException
/// class with serialized data.
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the serialized object
/// data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains contextual information
/// about the source or destination.
/// </param>
protected
InvalidJobStateException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
#endregion
/// <summary>
/// Gets CurrentState of the Job.
/// </summary>
public JobState CurrentState
{
get
{
return _currState;
}
}
/// <summary>
/// State of job when exception was thrown.
/// </summary>
[NonSerialized]
private readonly JobState _currState = 0;
}
/// <summary>
/// Type which has information about JobState and Exception
/// ,if any, associated with JobState.
/// </summary>
public sealed class JobStateInfo
{
#region constructors
/// <summary>
/// Constructor for state changes not resulting from an error.
/// </summary>
/// <param name="state">Execution state.</param>
public JobStateInfo(JobState state)
: this(state, null)
{
}
/// <summary>
/// Constructor for state changes with an optional error.
/// </summary>
/// <param name="state">The new state.</param>
/// <param name="reason">A non-null exception if the state change was
/// caused by an error,otherwise; null.
/// </param>
public JobStateInfo(JobState state, Exception reason)
{
State = state;
Reason = reason;
}
/// <summary>
/// Copy constructor to support cloning.
/// </summary>
/// <param name="jobStateInfo">Source information.</param>
/// <throws>
/// ArgumentNullException when <paramref name="jobStateInfo"/> is null.
/// </throws>
internal JobStateInfo(JobStateInfo jobStateInfo)
{
State = jobStateInfo.State;
Reason = jobStateInfo.Reason;
}
#endregion constructors
#region public_properties
/// <summary>
/// The state of the job.
/// </summary>
/// <remarks>
/// This value indicates the state of the job .
/// </remarks>
public JobState State { get; }
/// <summary>
/// The reason for the state change, if caused by an error.
/// </summary>
/// <remarks>
/// The value of this property is non-null if the state
/// changed due to an error. Otherwise, the value of this
/// property is null.
/// </remarks>
public Exception Reason { get; }
#endregion public_properties
/// <summary>
/// Override for ToString()
/// </summary>
/// <returns></returns>
public override string ToString()
{
return State.ToString();
}
/// <summary>
/// Clones this object.
/// </summary>
/// <returns>Cloned object.</returns>
internal JobStateInfo Clone()
{
return new JobStateInfo(this);
}
#region private_fields
#endregion private_fields
}
/// <summary>
/// Event arguments passed to JobStateEvent handlers
/// <see cref="Job.StateChanged"/> event.
/// </summary>
public sealed class JobStateEventArgs : EventArgs
{
#region constructors
/// <summary>
/// Constructor of JobStateEventArgs.
/// </summary>
/// <param name="jobStateInfo">The current state of the job.</param>
public JobStateEventArgs(JobStateInfo jobStateInfo)
: this(jobStateInfo, null)
{
}
/// <summary>
/// Constructor of JobStateEventArgs.
/// </summary>
/// <param name="jobStateInfo">The current state of the job.</param>
/// <param name="previousJobStateInfo">The previous state of the job.</param>
public JobStateEventArgs(JobStateInfo jobStateInfo, JobStateInfo previousJobStateInfo)
{
if (jobStateInfo == null)
{
throw PSTraceSource.NewArgumentNullException(nameof(jobStateInfo));
}
JobStateInfo = jobStateInfo;
PreviousJobStateInfo = previousJobStateInfo;
}
#endregion constructors
#region public_properties
/// <summary>
/// Info about the current state of the job.
/// </summary>
public JobStateInfo JobStateInfo { get; }
/// <summary>
/// Info about the previous state of the job.
/// </summary>
public JobStateInfo PreviousJobStateInfo { get; }
#endregion public_properties
}
/// <summary>
/// Object that must be created by PowerShell to allow reuse of an ID for a job.
/// Also allows setting of the Instance Id so that jobs may be recreated.
/// </summary>
public sealed class JobIdentifier
{
internal JobIdentifier(int id, Guid instanceId)
{
if (id <= 0)
PSTraceSource.NewArgumentException(nameof(id), RemotingErrorIdStrings.JobSessionIdLessThanOne, id);
Id = id;
InstanceId = instanceId;
}
internal int Id { get; }
internal Guid InstanceId { get; private set; }
}
/// <summary>
/// Interface to expose a job debugger.
/// </summary>
#nullable enable
public interface IJobDebugger
{
/// <summary>
/// Job Debugger.
/// </summary>
Debugger? Debugger
{
get;
}
/// <summary>
/// True if job is running asynchronously.
/// </summary>
bool IsAsync
{
get;
set;
}
}
#nullable restore
/// <summary>
/// Represents a command running in background. A job object can internally
/// contain many child job objects.
/// </summary>
public abstract class Job : IDisposable
{
#region Constructor
/// <summary>
/// Default constructor.
/// </summary>
protected Job()
{
Id = System.Threading.Interlocked.Increment(ref s_jobIdSeed);
}
/// <summary>
/// Creates an instance of this class.
/// </summary>
/// <param name="command">Command invoked by this job object.</param>
protected Job(string command)
: this()
{
Command = command;
_name = AutoGenerateJobName();
}
/// <summary>
/// Creates an instance of this class.
/// </summary>
/// <param name="command">Command invoked by this job object.</param>
/// <param name="name">Friendly name for the job object.</param>
protected Job(string command, string name)
: this(command)
{
if (!string.IsNullOrEmpty(name))
{
_name = name;
}
}
/// <summary>
/// Creates an instance of this class.
/// </summary>
/// <param name="command">Command invoked by this job object.</param>
/// <param name="name">Friendly name for the job object.</param>
/// <param name="childJobs">Child jobs of this job object.</param>
protected Job(string command, string name, IList<Job> childJobs)
: this(command, name)
{
_childJobs = childJobs;
}
/// <summary>
/// Creates an instance of this class.
/// </summary>
/// <param name="command">Command invoked by this job object.</param>
/// <param name="name">Friendly name for the job object.</param>
/// <param name="token">Id and InstanceId pair to be used for this job object.</param>
/// <remarks>The JobIdentifier is a token that must be issued by PowerShell to allow
/// reuse of the Id. This is the only way to set either Id or instance Id.</remarks>
protected Job(string command, string name, JobIdentifier token)
{
if (token == null)
throw PSTraceSource.NewArgumentNullException(nameof(token), RemotingErrorIdStrings.JobIdentifierNull);
if (token.Id > s_jobIdSeed)
{
throw PSTraceSource.NewArgumentException(nameof(token), RemotingErrorIdStrings.JobIdNotYetAssigned, token.Id);
}
Command = command;
Id = token.Id;
InstanceId = token.InstanceId;
if (!string.IsNullOrEmpty(name))
{
_name = name;
}
else
{
_name = AutoGenerateJobName();
}
}
/// <summary>
/// Creates an instance of this class.
/// </summary>
/// <param name="command">Command invoked by this job object.</param>
/// <param name="name">Friendly name for the job object.</param>
/// <param name="instanceId">InstanceId to be used for this job object.</param>
/// <remarks>The InstanceId may need to be set to maintain job identity across
/// instances of the process.</remarks>
protected Job(string command, string name, Guid instanceId)
: this(command, name)
{
InstanceId = instanceId;
}
internal static string GetCommandTextFromInvocationInfo(InvocationInfo invocationInfo)
{
if (invocationInfo == null)
{
return null;
}
IScriptExtent scriptExtent = invocationInfo.ScriptPosition;
if ((scriptExtent != null) && (scriptExtent.StartScriptPosition != null) && !string.IsNullOrWhiteSpace(scriptExtent.StartScriptPosition.Line))
{
Dbg.Assert(scriptExtent.StartScriptPosition.ColumnNumber > 0, "Column numbers start at 1");
Dbg.Assert(scriptExtent.StartScriptPosition.ColumnNumber <= scriptExtent.StartScriptPosition.Line.Length, "Column numbers are not greater than the length of a line");
return scriptExtent.StartScriptPosition.Line.AsSpan(scriptExtent.StartScriptPosition.ColumnNumber - 1).Trim().ToString();
}
return invocationInfo.InvocationName;
}
#endregion Constructor
#region Private Members
private ManualResetEvent _finished = new ManualResetEvent(false);
private string _name;
private IList<Job> _childJobs;
internal readonly object syncObject = new object(); // object used for synchronization
// ISSUE: Should Result be public property
private PSDataCollection<PSStreamObject> _results = new PSDataCollection<PSStreamObject>();
private bool _resultsOwner = true;
private PSDataCollection<ErrorRecord> _error = new PSDataCollection<ErrorRecord>();
private bool _errorOwner = true;
private PSDataCollection<ProgressRecord> _progress = new PSDataCollection<ProgressRecord>();
private bool _progressOwner = true;
private PSDataCollection<VerboseRecord> _verbose = new PSDataCollection<VerboseRecord>();
private bool _verboseOwner = true;
private PSDataCollection<WarningRecord> _warning = new PSDataCollection<WarningRecord>();
private bool _warningOwner = true;
private PSDataCollection<DebugRecord> _debug = new PSDataCollection<DebugRecord>();
private bool _debugOwner = true;
private PSDataCollection<InformationRecord> _information = new PSDataCollection<InformationRecord>();
private bool _informationOwner = true;
private PSDataCollection<PSObject> _output = new PSDataCollection<PSObject>();
private bool _outputOwner = true;
/// <summary>
/// Static variable which is incremented to generate id.
/// </summary>
private static int s_jobIdSeed = 0;
private string _jobTypeName = string.Empty;
#endregion Private Members
#region Job Properties
/// <summary>
/// Command Invoked by this Job.
/// </summary>
public string Command { get; }
/// <summary>
/// Status of the command execution.
/// </summary>
public JobStateInfo JobStateInfo { get; private set; } = new JobStateInfo(JobState.NotStarted);
/// <summary>
/// Wait Handle which is signaled when job is finished.
/// This is set when state of the job is set to Completed,
/// Stopped or Failed.
/// </summary>
public WaitHandle Finished
{
get
{
lock (this.syncObject)
{
if (_finished != null)
{
return _finished;
}
else
{
// Damage control mode:
// Somebody is trying to get Finished handle for an already disposed Job instance.
// Return an already triggered handle (disposed job is finished by definition).
// The newly created handle will not be disposed in a deterministic manner
// and in some circumstances can be mistaken for a handle leak.
return new ManualResetEvent(true);
}
}
}
}
/// <summary>
/// Unique identifier for this job.
/// </summary>
public Guid InstanceId { get; } = Guid.NewGuid();
/// <summary>
/// Short identifier for this result which will be
/// recycled and used within a process.
/// </summary>
public int Id { get; }
/// <summary>
/// Name for identifying this job object.
/// </summary>
public string Name
{
get
{
return _name;
}
set
{
AssertNotDisposed();
_name = value;
}
}
/// <summary>
/// List of child jobs contained within this job.
/// </summary>
public IList<Job> ChildJobs
{
get
{
if (_childJobs == null)
{
lock (syncObject)
{
if (_childJobs == null)
{
_childJobs = new List<Job>();
}
}
}
return _childJobs;
}
}
///<summary>
/// Success status of the command execution.
/// </summary>
public abstract string StatusMessage { get; }
/// <summary>
/// Indicates that more data is available in this
/// result object for reading.
/// </summary>
public abstract bool HasMoreData { get; }
/// <summary>
/// Time job was started.
/// </summary>
public DateTime? PSBeginTime { get; protected set; } = null;
/// <summary>
/// Time job stopped.
/// </summary>
public DateTime? PSEndTime { get; protected set; } = null;
/// <summary>
/// Job type name.
/// </summary>
public string PSJobTypeName
{
get
{
return _jobTypeName;
}
protected internal set
{
_jobTypeName = value ?? this.GetType().ToString();
}
}
#region results
/// <summary>
/// Result objects from this job. If this object is not a
/// leaf node (with no children), then this will
/// aggregate the results from all child jobs.
/// </summary>
internal PSDataCollection<PSStreamObject> Results
{
get
{
return _results;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Results");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_resultsOwner = false;
_results = value;
}
}
}
/// <summary>
/// Indicates if a particular Job type uses the
/// internal results collection.
/// </summary>
internal bool UsesResultsCollection { get; set; }
/// <summary>
/// Suppresses forwarding of job output into a cmdlet (like Receive-Job).
/// This flag modifies functionality of <see cref="WriteObject"/> method, so that it doesnt add output-processing to <see cref="Results"/> collection.
/// </summary>
internal bool SuppressOutputForwarding { get; set; }
internal virtual void WriteObject(object outputObject)
{
PSObject pso = (outputObject == null) ? null : PSObject.AsPSObject(outputObject);
this.Output.Add(pso);
if (!SuppressOutputForwarding)
{
this.Results.Add(new PSStreamObject(PSStreamObjectType.Output, pso));
}
}
/// <summary>
/// Allows propagating of terminating exceptions from remote "throw" statement
/// (normally / by default all remote errors are transformed into non-terminating errors.
/// </summary>
internal bool PropagateThrows { get; set; }
private void WriteError(Cmdlet cmdlet, ErrorRecord errorRecord)
{
if (this.PropagateThrows)
{
Exception e = GetExceptionFromErrorRecord(errorRecord);
if (e != null)
throw e;
}
errorRecord.PreserveInvocationInfoOnce = true;
cmdlet.WriteError(errorRecord);
}
private static Exception GetExceptionFromErrorRecord(ErrorRecord errorRecord)
{
if (!(errorRecord.Exception is RuntimeException runtimeException))
return null;
if (!(runtimeException is RemoteException remoteException))
return null;
PSPropertyInfo wasThrownFromThrow =
remoteException.SerializedRemoteException.Properties["WasThrownFromThrowStatement"];
if (wasThrownFromThrow == null || !((bool)wasThrownFromThrow.Value))
return null;
runtimeException.WasThrownFromThrowStatement = true;
return runtimeException;
}
internal virtual void WriteError(ErrorRecord errorRecord)
{
Error.Add(errorRecord);
if (PropagateThrows)
{
Exception exception = GetExceptionFromErrorRecord(errorRecord);
if (exception != null)
{
Results.Add(new PSStreamObject(PSStreamObjectType.Exception, exception));
return;
}
}
Results.Add(new PSStreamObject(PSStreamObjectType.Error, errorRecord));
}
internal void WriteError(ErrorRecord errorRecord, out Exception exceptionThrownOnCmdletThread)
{
this.Error.Add(errorRecord);
this.InvokeCmdletMethodAndWaitForResults<object>(
(Cmdlet cmdlet) =>
{
this.WriteError(cmdlet, errorRecord);
return null;
},
out exceptionThrownOnCmdletThread);
}
internal virtual void WriteWarning(string message)
{
this.Warning.Add(new WarningRecord(message));
this.Results.Add(new PSStreamObject(PSStreamObjectType.Warning, message));
}
internal virtual void WriteVerbose(string message)
{
this.Verbose.Add(new VerboseRecord(message));
this.Results.Add(new PSStreamObject(PSStreamObjectType.Verbose, message));
}
internal virtual void WriteDebug(string message)
{
this.Debug.Add(new DebugRecord(message));
this.Results.Add(new PSStreamObject(PSStreamObjectType.Debug, message));
}
internal virtual void WriteProgress(ProgressRecord progressRecord)
{
if ((progressRecord.ParentActivityId == (-1)) && (_parentActivityId != null))
{
progressRecord = new ProgressRecord(progressRecord) { ParentActivityId = _parentActivityId.Value };
}
Progress.Add(progressRecord);
Results.Add(new PSStreamObject(PSStreamObjectType.Progress, progressRecord));
}
internal virtual void WriteInformation(InformationRecord informationRecord)
{
Information.Add(informationRecord);
Results.Add(new PSStreamObject(PSStreamObjectType.Information, informationRecord));
}
private Lazy<int> _parentActivityId;
internal void SetParentActivityIdGetter(Func<int> parentActivityIdGetter)
{
Dbg.Assert(parentActivityIdGetter != null, "Caller should verify parentActivityIdGetter != null");
_parentActivityId = new Lazy<int>(parentActivityIdGetter);
}
internal bool ShouldContinue(string query, string caption)
{
Exception exceptionThrownOnCmdletThread;
return this.ShouldContinue(query, caption, out exceptionThrownOnCmdletThread);
}
internal bool ShouldContinue(string query, string caption, out Exception exceptionThrownOnCmdletThread)
{
bool methodResult = InvokeCmdletMethodAndWaitForResults(
cmdlet => cmdlet.ShouldContinue(query, caption),
out exceptionThrownOnCmdletThread);
return methodResult;
}
internal virtual void NonblockingShouldProcess(
string verboseDescription,
string verboseWarning,
string caption)
{
InvokeCmdletMethodAndIgnoreResults(
(Cmdlet cmdlet) =>
{
ShouldProcessReason throwAwayProcessReason;
cmdlet.ShouldProcess(
verboseDescription,
verboseWarning,
caption,
out throwAwayProcessReason);
});
}
internal virtual bool ShouldProcess(
string verboseDescription,
string verboseWarning,
string caption,
out ShouldProcessReason shouldProcessReason,
out Exception exceptionThrownOnCmdletThread)
{
ShouldProcessReason closureSafeShouldProcessReason = ShouldProcessReason.None;
bool methodResult = InvokeCmdletMethodAndWaitForResults(
cmdlet => cmdlet.ShouldProcess(
verboseDescription,
verboseWarning,
caption,
out closureSafeShouldProcessReason),
out exceptionThrownOnCmdletThread);
shouldProcessReason = closureSafeShouldProcessReason;
return methodResult;
}
private void InvokeCmdletMethodAndIgnoreResults(Action<Cmdlet> invokeCmdletMethod)
{
object resultsLock = new object();
CmdletMethodInvoker<object> methodInvoker = new CmdletMethodInvoker<object>
{
Action = (Cmdlet cmdlet) => { invokeCmdletMethod(cmdlet); return null; },
Finished = null,
SyncObject = resultsLock
};
Results.Add(new PSStreamObject(PSStreamObjectType.BlockingError, methodInvoker));
}
private T InvokeCmdletMethodAndWaitForResults<T>(Func<Cmdlet, T> invokeCmdletMethodAndReturnResult, out Exception exceptionThrownOnCmdletThread)
{
Dbg.Assert(invokeCmdletMethodAndReturnResult != null, "Caller should verify invokeCmdletMethodAndReturnResult != null");
T methodResult = default(T);
Exception closureSafeExceptionThrownOnCmdletThread = null;
object resultsLock = new object();
using (var gotResultEvent = new ManualResetEventSlim(false))
{
EventHandler<JobStateEventArgs> stateChangedEventHandler =
(object sender, JobStateEventArgs eventArgs) =>
{
if (IsFinishedState(eventArgs.JobStateInfo.State) || eventArgs.JobStateInfo.State == JobState.Stopping)
{
lock (resultsLock)
{
closureSafeExceptionThrownOnCmdletThread = new OperationCanceledException();
}
gotResultEvent.Set();
}
};
this.StateChanged += stateChangedEventHandler;
Interlocked.MemoryBarrier();
try
{
stateChangedEventHandler(null, new JobStateEventArgs(this.JobStateInfo));
if (!gotResultEvent.IsSet)
{
this.SetJobState(JobState.Blocked);
// addition to results column happens here
CmdletMethodInvoker<T> methodInvoker = new CmdletMethodInvoker<T>
{
Action = invokeCmdletMethodAndReturnResult,
Finished = gotResultEvent,
SyncObject = resultsLock
};
PSStreamObjectType objectType = PSStreamObjectType.ShouldMethod;
if (typeof(T) == typeof(object))
objectType = PSStreamObjectType.BlockingError;
Results.Add(new PSStreamObject(objectType, methodInvoker));
gotResultEvent.Wait();
this.SetJobState(JobState.Running);
lock (resultsLock)
{
if (closureSafeExceptionThrownOnCmdletThread == null) // stateChangedEventHandler didn't set the results? = ok to clobber results?
{
closureSafeExceptionThrownOnCmdletThread = methodInvoker.ExceptionThrownOnCmdletThread;
methodResult = methodInvoker.MethodResult;
}
}
}
}
finally
{
this.StateChanged -= stateChangedEventHandler;
}
}
lock (resultsLock)
{
exceptionThrownOnCmdletThread = closureSafeExceptionThrownOnCmdletThread;
return methodResult;
}
}
internal virtual void ForwardAvailableResultsToCmdlet(Cmdlet cmdlet)
{
foreach (PSStreamObject obj in Results.ReadAll())
{
obj.WriteStreamObject(cmdlet);
}
}
internal virtual void ForwardAllResultsToCmdlet(Cmdlet cmdlet)
{
foreach (PSStreamObject obj in this.Results)
{
obj.WriteStreamObject(cmdlet);
}
}
/// <summary>
/// This method is introduce for delaying the loading of streams
/// for a particular job.
/// </summary>
protected virtual void DoLoadJobStreams()
{
}
/// <summary>
/// Unloads job streams information. Enables jobs to
/// clear stream information from memory.
/// </summary>
protected virtual void DoUnloadJobStreams()
{
}
/// <summary>
/// Load the required job streams.
/// </summary>
public void LoadJobStreams()
{
if (_jobStreamsLoaded) return;
lock (syncObject)
{
if (_jobStreamsLoaded) return;
_jobStreamsLoaded = true;
}
try
{
DoLoadJobStreams();
}
catch (Exception e)
{
// third party call-out for platform API
// Therefore it is fine to eat the exception
// here
using (PowerShellTraceSource tracer = PowerShellTraceSourceFactory.GetTraceSource())
{
tracer.TraceException(e);
}
}
}
private bool _jobStreamsLoaded;
/// <summary>
/// Unload the required job streams.
/// </summary>
public void UnloadJobStreams()
{
if (!_jobStreamsLoaded) return;
lock (syncObject)
{
if (!_jobStreamsLoaded) return;
_jobStreamsLoaded = false;
}
try
{
DoUnloadJobStreams();
}
catch (Exception e)
{
// third party call-out for platform API
// Therefore it is fine to eat the exception
// here
using (PowerShellTraceSource tracer = PowerShellTraceSourceFactory.GetTraceSource())
{
tracer.TraceException(e);
}
}
}
/// <summary>
/// Gets or sets the output buffer. Output of job is written
/// into this buffer.
/// </summary>
/// <exception cref="ArgumentNullException">
/// Cannot set to a null value.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public PSDataCollection<PSObject> Output
{
get
{
LoadJobStreams(); // for delayed loading
return _output;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Output");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_outputOwner = false;
_output = value;
_jobStreamsLoaded = true;
}
}
}
/// <summary>
/// Gets or sets the error buffer. Errors of job are written
/// into this buffer.
/// </summary>
/// <exception cref="ArgumentNullException">
/// Cannot set to a null value.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public PSDataCollection<ErrorRecord> Error
{
get
{
LoadJobStreams(); // for delayed loading
return _error;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Error");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_errorOwner = false;
_error = value;
_jobStreamsLoaded = true;
}
}
}
/// <summary>
/// Gets or sets the progress buffer. Progress of job is written
/// into this buffer.
/// </summary>
/// <exception cref="ArgumentNullException">
/// Cannot set to a null value.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public PSDataCollection<ProgressRecord> Progress
{
get
{
LoadJobStreams(); // for delayed loading
return _progress;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Progress");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_progressOwner = false;
_progress = value;
_jobStreamsLoaded = true;
}
}
}
/// <summary>
/// Gets or sets the verbose buffer. Verbose output of job is written to
/// this stream.
/// </summary>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public PSDataCollection<VerboseRecord> Verbose
{
get
{
LoadJobStreams(); // for delayed loading
return _verbose;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Verbose");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_verboseOwner = false;
_verbose = value;
_jobStreamsLoaded = true;
}
}
}
/// <summary>
/// Gets or sets the debug buffer. Debug output of Job is written
/// to this buffer.
/// <exception cref="ArgumentNullException">
/// Cannot set to a null value.
/// </exception>
/// </summary>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public PSDataCollection<DebugRecord> Debug
{
get
{
LoadJobStreams(); // for delayed loading
return _debug;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Debug");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_debugOwner = false;
_debug = value;
}
}
}
/// <summary>
/// Gets or sets the warning buffer. Warnings of job are written to
/// this buffer.
/// </summary>
/// <exception cref="ArgumentNullException">
/// Cannot set to a null value.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public PSDataCollection<WarningRecord> Warning
{
get
{
LoadJobStreams(); // for delayed loading
return _warning;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Warning");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_warningOwner = false;
_warning = value;
_jobStreamsLoaded = true;
}
}
}
/// <summary>
/// Gets or sets the information buffer. Information records of job are written to
/// this buffer.
/// </summary>
/// <exception cref="ArgumentNullException">
/// Cannot set to a null value.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public PSDataCollection<InformationRecord> Information
{
get
{
LoadJobStreams(); // for delayed loading
return _information;
}
set
{
if (value == null)
{
throw PSTraceSource.NewArgumentNullException("Information");
}
lock (syncObject)
{
AssertChangesAreAccepted();
_informationOwner = false;
_information = value;
_jobStreamsLoaded = true;
}
}
}
/// <summary>
/// Indicates a location where this job is running.
/// </summary>
public abstract string Location { get; }
#endregion results
#region Connect/Disconnect
/// <summary>
/// Returns boolean indicating whether the underlying
/// transport for the job (or child jobs) supports
/// connect/disconnect semantics.
/// </summary>
internal virtual bool CanDisconnect
{
get { return false; }
}
/// <summary>
/// Returns runspaces associated with the Job, including
/// child jobs.
/// </summary>
/// <returns>IEnumerable of RemoteRunspaces.</returns>
internal virtual IEnumerable<RemoteRunspace> GetRunspaces()
{
return null;
}
#endregion
#endregion Job Properties
#region Job State and State Change Event
/// <summary>
/// Event raised when state of the job changes.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")]
public event EventHandler<JobStateEventArgs> StateChanged;
/// <summary>
/// Sets Job State.
/// </summary>
/// <param name="state">
/// New State of Job
/// </param>
protected void SetJobState(JobState state)
{
AssertNotDisposed();
SetJobState(state, null);
}
/// <summary>
/// Sets Job State.
/// </summary>
/// <param name="state">
/// New State of Job
/// </param>
/// <param name="reason">
/// Reason associated with the state.
/// </param>
internal void SetJobState(JobState state, Exception reason)
{
using (PowerShellTraceSource tracer = PowerShellTraceSourceFactory.GetTraceSource())
{
AssertNotDisposed();
bool alldone = false;
JobStateInfo previousState = JobStateInfo;
lock (syncObject)
{
JobStateInfo = new JobStateInfo(state, reason);
if (state == JobState.Running)
{
// BeginTime is set only once.
if (PSBeginTime == null)
{
PSBeginTime = DateTime.Now;
}
}
else if (IsFinishedState(state))
{
alldone = true;
// EndTime is set only once.
if (PSEndTime == null)
{
PSEndTime = DateTime.Now;
}
}
}
if (alldone)
{
CloseAllStreams();
if (_processingOutput)
{
try
{
// Still marked as processing output. Send final state changed.
HandleOutputProcessingStateChanged(this, new OutputProcessingStateEventArgs(false));
}
catch (Exception)
{
}
}
}
#pragma warning disable 56500
// Exception raised in the eventhandler are not error in job.
// silently ignore them.
try
{
tracer.WriteMessage("Job", "SetJobState", Guid.Empty, this, "Invoking StateChanged event", null);
StateChanged.SafeInvoke(this, new JobStateEventArgs(JobStateInfo.Clone(), previousState));
}
catch (Exception exception) // ignore non-severe exceptions
{
tracer.WriteMessage("Job", "SetJobState", Guid.Empty, this,
"Some Job StateChange event handler threw an unhandled exception.", null);
tracer.TraceException(exception);
}
// finished needs to be set after StateChanged event
// has been raised
if (alldone)
{
lock (syncObject)
{
if (_finished != null)
{
_finished.Set();
}
}
}
#pragma warning restore 56500
}
}
#endregion Job State and State Change Event
#region Job Public Methods
/// <summary>
/// Stop this job object. If job contains child job, this should
/// stop child job objects also.
/// </summary>
public abstract void StopJob();
#endregion Job Public Methods
#region Private/Internal Methods
/// <summary>
/// Returns the items in results collection
/// after clearing up all the internal
/// structures.
/// </summary>
/// <returns>Collection of stream objects.</returns>
internal Collection<PSStreamObject> ReadAll()
{
Output.Clear();
Error.Clear();
Debug.Clear();
Warning.Clear();
Verbose.Clear();
Progress.Clear();
return Results.ReadAll();
}
/// <summary>
/// Helper function to check if job is finished.
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
internal bool IsFinishedState(JobState state)
{
lock (syncObject)
{
return (state == JobState.Completed || state == JobState.Failed || state == JobState.Stopped);
}
}
internal bool IsPersistentState(JobState state)
{
lock (syncObject)
{
return (IsFinishedState(state) || state == JobState.Disconnected || state == JobState.Suspended);
}
}
/// <summary>
/// Checks if the current instance can accept changes like
/// changing one of the properties like Output, Error etc.
/// If changes are not allowed, throws an exception.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Powershell instance cannot be changed in its
/// current state.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
private void AssertChangesAreAccepted()
{
AssertNotDisposed();
lock (syncObject)
{
if (JobStateInfo.State == JobState.Running)
{
throw new InvalidJobStateException(JobState.Running);
}
}
}
/// <summary>
/// Automatically generate a job name if the user
/// does not supply one.
/// </summary>
/// <returns>Auto generated job name.</returns>
/// <remarks>Since the user can script/program against the
/// job name, the auto generated name will not be
/// localizable</remarks>
protected string AutoGenerateJobName()
{
return "Job" + Id.ToString(System.Globalization.NumberFormatInfo.InvariantInfo);
}
/// <summary>
/// Checks if the current powershell instance is disposed.
/// If disposed, throws ObjectDisposedException.
/// </summary>
/// <exception cref="ObjectDisposedException">
/// Object is disposed.
/// </exception>
internal void AssertNotDisposed()
{
if (_isDisposed)
{
throw PSTraceSource.NewObjectDisposedException("PSJob");
}
}
/// <summary>
/// A helper method to close all the streams.
/// </summary>
internal void CloseAllStreams()
{
// The Complete() method includes raising public notification events that third parties can
// handle and potentially throw exceptions on the notification thread. We don't want to
// propagate those exceptions because it prevents this thread from completing its processing.
if (_resultsOwner) { try { _results.Complete(); } catch (Exception e) { TraceException(e); } }
if (_outputOwner) { try { _output.Complete(); } catch (Exception e) { TraceException(e); } }
if (_errorOwner) { try { _error.Complete(); } catch (Exception e) { TraceException(e); } }
if (_progressOwner) { try { _progress.Complete(); } catch (Exception e) { TraceException(e); } }
if (_verboseOwner) { try { _verbose.Complete(); } catch (Exception e) { TraceException(e); } }
if (_warningOwner) { try { _warning.Complete(); } catch (Exception e) { TraceException(e); } }
if (_debugOwner) { try { _debug.Complete(); } catch (Exception e) { TraceException(e); } }
if (_informationOwner) { try { _information.Complete(); } catch (Exception e) { TraceException(e); } }
}
private static void TraceException(Exception e)
{
using (PowerShellTraceSource tracer = PowerShellTraceSourceFactory.GetTraceSource())
{
tracer.TraceException(e);
}
}
/// <summary>
/// Gets the job for the specified location.
/// </summary>
/// <param name="location">Location to filter on.</param>
/// <returns>Collection of jobs.</returns>
internal List<Job> GetJobsForLocation(string location)
{
List<Job> returnJobList = new List<Job>();
foreach (Job job in ChildJobs)
{
if (string.Equals(job.Location, location, StringComparison.OrdinalIgnoreCase))
{
returnJobList.Add(job);
}
}
return returnJobList;
}
#endregion Private/Internal Methods
#region IDisposable Members
/// <summary>
/// Dispose all managed resources. This will suppress finalizer on the object from getting called by
/// calling System.GC.SuppressFinalize(this).
/// </summary>
public void Dispose()
{
Dispose(true);
// To prevent derived types with finalizers from having to re-implement System.IDisposable to call it,
// unsealed types without finalizers should still call SuppressFinalize.
System.GC.SuppressFinalize(this);
}
/// <summary>
/// Release all the resources.
/// </summary>
/// <param name="disposing">
/// if true, release all the managed objects.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (!_isDisposed)
{
CloseAllStreams();
// release the WaitHandle
lock (syncObject)
{
if (_finished != null)
{
_finished.Dispose();
_finished = null;
}
}
// Only dispose the collections if we've created them...
if (_resultsOwner) _results.Dispose();
if (_outputOwner) _output.Dispose();
if (_errorOwner) _error.Dispose();
if (_debugOwner) _debug.Dispose();
if (_informationOwner) _information.Dispose();
if (_verboseOwner) _verbose.Dispose();
if (_warningOwner) _warning.Dispose();
if (_progressOwner) _progress.Dispose();
_isDisposed = true;
}
}
}
private bool _isDisposed;
#endregion IDisposable Members
#region MonitorOutputProcessing
internal event EventHandler<OutputProcessingStateEventArgs> OutputProcessingStateChanged;
private bool _processingOutput;
/// <summary>
/// MonitorOutputProcessing.
/// </summary>
internal bool MonitorOutputProcessing
{
get;
set;
}
internal void SetMonitorOutputProcessing(IOutputProcessingState outputProcessingState)
{
if (outputProcessingState != null)
{
outputProcessingState.OutputProcessingStateChanged += HandleOutputProcessingStateChanged;
}
}
internal void RemoveMonitorOutputProcessing(IOutputProcessingState outputProcessingState)
{
if (outputProcessingState != null)
{
outputProcessingState.OutputProcessingStateChanged -= HandleOutputProcessingStateChanged;
}
}
private void HandleOutputProcessingStateChanged(object sender, OutputProcessingStateEventArgs e)
{
_processingOutput = e.ProcessingOutput;
OutputProcessingStateChanged.SafeInvoke<OutputProcessingStateEventArgs>(this, e);
}
#endregion
}
/// <summary>
/// Top level job object for remoting. This contains multiple child job
/// objects. Each child job object invokes command on one remote machine.
/// </summary>
/// <remarks>
/// Not removing the prefix "PS" as this signifies powershell specific remoting job
/// </remarks>
internal class PSRemotingJob : Job
{
#region Internal Constructors
/// <summary>
/// Internal constructor for initializing PSRemotingJob using
/// computer names.
/// </summary>
/// <param name="computerNames">names of computers for
/// which the job object is being created</param>
/// <param name="computerNameHelpers">list of helper objects
/// corresponding to the computer names
/// </param>
/// <param name="remoteCommand">remote command corresponding to this
/// job object</param>
/// <param name="name"> a friendly name for the job object
/// </param>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal PSRemotingJob(string[] computerNames,
List<IThrottleOperation> computerNameHelpers, string remoteCommand, string name)
: this(computerNames, computerNameHelpers, remoteCommand, 0, name)
{ }
/// <summary>
/// Internal constructor for initializing job using
/// PSSession objects.
/// </summary>
/// <param name="remoteRunspaceInfos">array of runspace info
/// objects on which the remote command is executed</param>
/// <param name="runspaceHelpers">List of helper objects for the
/// runspaces</param>
/// <param name="remoteCommand"> remote command corresponding to this
/// job object</param>
/// <param name="name">a friendly name for the job object
/// </param>
internal PSRemotingJob(PSSession[] remoteRunspaceInfos,
List<IThrottleOperation> runspaceHelpers, string remoteCommand, string name)
: this(remoteRunspaceInfos, runspaceHelpers, remoteCommand, 0, name)
{ }
/// <summary>
/// Internal constructor for initializing PSRemotingJob using
/// computer names.
/// </summary>
/// <param name="computerNames">names of computers for
/// which the result object is being created</param>
/// <param name="computerNameHelpers">list of helper objects
/// corresponding to the computer names
/// </param>
/// <param name="remoteCommand">remote command corresponding to this
/// result object</param>
/// <param name="throttleLimit">Throttle limit to use.</param>
/// <param name="name">A friendly name for the job object.</param>
internal PSRemotingJob(string[] computerNames,
List<IThrottleOperation> computerNameHelpers, string remoteCommand,
int throttleLimit, string name)
: base(remoteCommand, name)
{
// Create child jobs for each object in the list
foreach (ExecutionCmdletHelperComputerName helper in computerNameHelpers)
{
// Create Child Job and Register for its StateChanged Event
PSRemotingChildJob childJob = new PSRemotingChildJob(remoteCommand,
helper, _throttleManager);
childJob.StateChanged += HandleChildJobStateChanged;
childJob.JobUnblocked += HandleJobUnblocked;
// Add this job to list of childjobs
ChildJobs.Add(childJob);
}
CommonInit(throttleLimit, computerNameHelpers);
}
/// <summary>
/// Internal constructor for initializing job using
/// PSSession objects.
/// </summary>
/// <param name="remoteRunspaceInfos">array of runspace info
/// objects on which the remote command is executed</param>
/// <param name="runspaceHelpers">List of helper objects for the
/// runspaces</param>
/// <param name="remoteCommand"> remote command corresponding to this
/// result object</param>
/// <param name="throttleLimit">Throttle limit to use.</param>
/// <param name="name"></param>
internal PSRemotingJob(PSSession[] remoteRunspaceInfos,
List<IThrottleOperation> runspaceHelpers, string remoteCommand,
int throttleLimit, string name)
: base(remoteCommand, name)
{
// Create child jobs for each object in the list
for (int i = 0; i < remoteRunspaceInfos.Length; i++)
{
ExecutionCmdletHelperRunspace helper = (ExecutionCmdletHelperRunspace)runspaceHelpers[i];
// Create Child Job object and Register for its state changed event
PSRemotingChildJob job = new PSRemotingChildJob(remoteCommand,
helper, _throttleManager);
job.StateChanged += HandleChildJobStateChanged;
job.JobUnblocked += HandleJobUnblocked;
// Add the child job to list of child jobs
ChildJobs.Add(job);
}
CommonInit(throttleLimit, runspaceHelpers);
}
/// <summary>
/// Creates a job object and child jobs for each disconnected pipeline/runspace
/// provided in the list of ExecutionCmdletHelperRunspace items. The runspace
/// object must have a remote running command that can be connected to.
/// Use Connect() method to transition to the connected state.
/// </summary>
/// <param name="helpers">List of DisconnectedJobOperation objects with disconnected pipelines.</param>
/// <param name="throttleLimit">Throttle limit value.</param>
/// <param name="name">Job name.</param>
/// <param name="aggregateResults">Aggregate results.</param>
internal PSRemotingJob(List<IThrottleOperation> helpers,
int throttleLimit, string name, bool aggregateResults)
: base(string.Empty, name)
{
// All pipeline objects must be in "disconnected" state and associated to running
// remote commands. Once the jobs are connected they can be stopped using the
// ExecutionCmdletHelperRunspace object and ThrottleManager.
foreach (ExecutionCmdletHelper helper in helpers)
{
PSRemotingChildJob job = new PSRemotingChildJob(helper, _throttleManager, aggregateResults);
job.StateChanged += HandleChildJobStateChanged;
job.JobUnblocked += HandleJobUnblocked;
ChildJobs.Add(job);
}
// Since no results are produced by any streams. We should
// close all the streams.
base.CloseAllStreams();
// Set status to "disconnected".
SetJobState(JobState.Disconnected);
// Submit the disconnected operation helpers to the throttle manager
_throttleManager.ThrottleLimit = throttleLimit;
_throttleManager.SubmitOperations(helpers);
_throttleManager.EndSubmitOperations();
}
/// <summary>
/// Default constructor.
/// </summary>
protected PSRemotingJob() { }
/// <summary>
/// Initialization common to both constructors.
/// </summary>
private void CommonInit(int throttleLimit, List<IThrottleOperation> helpers)
{
// Since no results are produced by any streams. We should
// close all the streams
base.CloseAllStreams();
// set status to "in progress"
SetJobState(JobState.Running);
// submit operations to the throttle manager
_throttleManager.ThrottleLimit = throttleLimit;
_throttleManager.SubmitOperations(helpers);
_throttleManager.EndSubmitOperations();
}
#endregion Internal Constructors
#region internal methods
/// <summary>
/// Get entity result for the specified computer.
/// </summary>
/// <param name="computerName">computername for which entity
/// result is required</param>
/// <returns>Entity result.</returns>
internal List<Job> GetJobsForComputer(string computerName)
{
List<Job> returnJobList = new List<Job>();
foreach (Job j in ChildJobs)
{
if (!(j is PSRemotingChildJob child)) continue;
if (string.Equals(child.Runspace.ConnectionInfo.ComputerName, computerName,
StringComparison.OrdinalIgnoreCase))
{
returnJobList.Add(child);
}
}
return returnJobList;
}
/// <summary>
/// Get entity result for the specified runspace.
/// </summary>
/// <param name="runspace">runspace for which entity
/// result is required</param>
/// <returns>Entity result.</returns>
internal List<Job> GetJobsForRunspace(PSSession runspace)
{
List<Job> returnJobList = new List<Job>();
foreach (Job j in ChildJobs)
{
if (!(j is PSRemotingChildJob child)) continue;
if (child.Runspace.InstanceId.Equals(runspace.InstanceId))
{
returnJobList.Add(child);
}
}
return returnJobList;
}
/// <summary>
/// Get entity result for the specified helper object.
/// </summary>
/// <param name="operation">helper for which entity
/// result is required</param>
/// <returns>Entity result.</returns>
internal List<Job> GetJobsForOperation(IThrottleOperation operation)
{
List<Job> returnJobList = new List<Job>();
ExecutionCmdletHelper helper = operation as ExecutionCmdletHelper;
foreach (Job j in ChildJobs)
{
if (!(j is PSRemotingChildJob child)) continue;
if (child.Helper.Equals(helper))
{
returnJobList.Add(child);
}
}
return returnJobList;
}
#endregion internal methods
#region Connection Support
/// <summary>
/// Connect all child jobs if they are in a disconnected state.
/// </summary>
internal void ConnectJobs()
{
// Create connect operation objects for each child job object to connect.
List<IThrottleOperation> connectJobOperations = new List<IThrottleOperation>();
foreach (PSRemotingChildJob childJob in ChildJobs)
{
if (childJob.JobStateInfo.State == JobState.Disconnected)
{
connectJobOperations.Add(new ConnectJobOperation(childJob));
}
}
if (connectJobOperations.Count == 0)
{
return;
}
// Submit the connect job operation.
// Return only after the connect operation completes.
SubmitAndWaitForConnect(connectJobOperations);
}
/// <summary>
/// Connect a single child job associated with the provided runspace.
/// </summary>
/// <param name="runspaceInstanceId">Runspace instance id for child job.</param>
internal void ConnectJob(Guid runspaceInstanceId)
{
List<IThrottleOperation> connectJobOperations = new List<IThrottleOperation>();
PSRemotingChildJob childJob = FindDisconnectedChildJob(runspaceInstanceId);
if (childJob != null)
{
connectJobOperations.Add(new ConnectJobOperation(childJob));
}
if (connectJobOperations.Count == 0)
{
return;
}
// Submit the connect job operation.
// Return only after the connect operation completes.
SubmitAndWaitForConnect(connectJobOperations);
}
private static void SubmitAndWaitForConnect(List<IThrottleOperation> connectJobOperations)
{
using (ThrottleManager connectThrottleManager = new ThrottleManager())
{
using (ManualResetEvent connectResult = new ManualResetEvent(false))
{
EventHandler<EventArgs> throttleCompleteEventHandler =
(object sender, EventArgs eventArgs) => connectResult.Set();
connectThrottleManager.ThrottleComplete += throttleCompleteEventHandler;
try
{
connectThrottleManager.ThrottleLimit = 0;
connectThrottleManager.SubmitOperations(connectJobOperations);
connectThrottleManager.EndSubmitOperations();
connectResult.WaitOne();
}
finally
{
connectThrottleManager.ThrottleComplete -= throttleCompleteEventHandler;
}
}
}
}
/// <summary>
/// Simple throttle operation class for connecting jobs.
/// </summary>
private sealed class ConnectJobOperation : IThrottleOperation
{
private readonly PSRemotingChildJob _psRemoteChildJob;
internal ConnectJobOperation(PSRemotingChildJob job)
{
_psRemoteChildJob = job;
_psRemoteChildJob.StateChanged += ChildJobStateChangedHandler;
}
internal override void StartOperation()
{
bool startedSuccessfully = true;
try
{
_psRemoteChildJob.ConnectAsync();
}
catch (InvalidJobStateException e)
{
startedSuccessfully = false;
string msg = StringUtil.Format(RemotingErrorIdStrings.JobConnectFailed, _psRemoteChildJob.Name);
Exception reason = new RuntimeException(msg, e);
ErrorRecord errorRecord = new ErrorRecord(reason, "PSJobConnectFailed", ErrorCategory.InvalidOperation, _psRemoteChildJob);
_psRemoteChildJob.WriteError(errorRecord);
}
if (!startedSuccessfully)
{
RemoveEventCallback();
SendStartComplete();
}
}
internal override void StopOperation()
{
RemoveEventCallback();
// Cannot stop a connect attempt.
OperationStateEventArgs operationStateEventArgs = new OperationStateEventArgs();
operationStateEventArgs.OperationState = OperationState.StopComplete;
OperationComplete.SafeInvoke(this, operationStateEventArgs);
}
internal override event EventHandler<OperationStateEventArgs> OperationComplete;
private void ChildJobStateChangedHandler(object sender, JobStateEventArgs eArgs)
{
if (eArgs.JobStateInfo.State == JobState.Disconnected)
{
return;
}
RemoveEventCallback();
SendStartComplete();
}
private void SendStartComplete()
{
OperationStateEventArgs operationStateEventArgs = new OperationStateEventArgs();
operationStateEventArgs.OperationState = OperationState.StartComplete;
OperationComplete.SafeInvoke(this, operationStateEventArgs);
}
private void RemoveEventCallback()
{
_psRemoteChildJob.StateChanged -= ChildJobStateChangedHandler;
}
}
/// <summary>
/// Finds the disconnected child job associated with this runspace and returns
/// the PowerShell object that is remotely executing the command.
/// </summary>
/// <param name="runspaceInstanceId">Runspace instance Id.</param>
/// <returns>Associated PowerShell object.</returns>
internal PowerShell GetAssociatedPowerShellObject(Guid runspaceInstanceId)
{
PowerShell ps = null;
PSRemotingChildJob childJob = FindDisconnectedChildJob(runspaceInstanceId);
if (childJob != null)
{
ps = childJob.GetPowerShell();
}
return ps;
}
/// <summary>
/// Helper method to find a disconnected child job associated with
/// a given runspace.
/// </summary>
/// <param name="runspaceInstanceId">Runspace Id.</param>
/// <returns>PSRemotingChildJob object.</returns>
private PSRemotingChildJob FindDisconnectedChildJob(Guid runspaceInstanceId)
{
PSRemotingChildJob rtnJob = null;
foreach (PSRemotingChildJob childJob in this.ChildJobs)
{
if ((childJob.Runspace.InstanceId.Equals(runspaceInstanceId)) &&
(childJob.JobStateInfo.State == JobState.Disconnected))
{
rtnJob = childJob;
break;
}
}
return rtnJob;
}
/// <summary>
/// Internal method to stop a job without first connecting it if it is in
/// a disconnected state. This supports Receive-PSSession where it abandons
/// a currently running/disconnected job when user selects -OutTarget Host.
/// </summary>
internal void InternalStopJob()
{
if (_isDisposed || _stopIsCalled || IsFinishedState(JobStateInfo.State))
{
return;
}
lock (_syncObject)
{
if (_isDisposed || _stopIsCalled || IsFinishedState(JobStateInfo.State))
{
return;
}
_stopIsCalled = true;
}
_throttleManager.StopAllOperations();
Finished.WaitOne();
}
#endregion
private bool _moreData = true;
/// <summary>
/// Indicates if more data is available.
/// </summary>
/// <remarks>
/// This has more data if any of the child jobs have more data.
/// </remarks>
public override bool HasMoreData
{
get
{
// moreData is initially set to true, and it
// will remain so until the async result
// object has completed execution.
if (_moreData && IsFinishedState(JobStateInfo.State))
{
bool atleastOneChildHasMoreData = false;
for (int i = 0; i < ChildJobs.Count; i++)
{
if (ChildJobs[i].HasMoreData)
{
atleastOneChildHasMoreData = true;
break;
}
}
_moreData = atleastOneChildHasMoreData;
}
return _moreData;
}
}
private bool _stopIsCalled = false;
/// <summary>
/// Stop Job.
/// </summary>
public override void StopJob()
{
// If the job is in a disconnected state then try to connect it
// so that it can be stopped on the server.
if (JobStateInfo.State == JobState.Disconnected)
{
bool ConnectSuccessful;
try
{
ConnectJobs();
ConnectSuccessful = true;
}
catch (InvalidRunspaceStateException)
{
ConnectSuccessful = false;
}
catch (PSRemotingTransportException)
{
ConnectSuccessful = false;
}
catch (PSInvalidOperationException)
{
ConnectSuccessful = false;
}
if (!ConnectSuccessful && this.Error.IsOpen)
{
string msg = StringUtil.Format(RemotingErrorIdStrings.StopJobNotConnected, this.Name);
Exception reason = new RuntimeException(msg);
ErrorRecord errorRecord = new ErrorRecord(reason, "StopJobCannotConnectToServer",
ErrorCategory.InvalidOperation, this);
WriteError(errorRecord);
return;
}
}
InternalStopJob();
}
private string _statusMessage;
/// <summary>
/// Message indicating status of the job.
/// </summary>
public override string StatusMessage
{
get
{
return _statusMessage;
}
}
/// <summary>
/// Used by Invoke-Command cmdlet to show/hide computername property value.
/// Format and Output has capability to understand RemoteObjects and this property lets
/// Format and Output decide whether to show/hide computername.
/// Default is true.
/// </summary>
internal bool HideComputerName
{
get
{
return _hideComputerName;
}
set
{
_hideComputerName = value;
foreach (Job job in this.ChildJobs)
{
PSRemotingChildJob rJob = job as PSRemotingChildJob;
if (rJob != null)
{
rJob.HideComputerName = value;
}
}
}
}
private bool _hideComputerName = true;
// ISSUE: Implement StatusMessage
/// <summary>
/// Checks the status of remote command execution.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
private void SetStatusMessage()
{
_statusMessage = "test";
// bool localErrors = false; // if local errors are present
// bool setFinished = false; // if finished needs to be set
// statusMessage = "OK";
// lock (syncObject)
// {
// if (finishedCount == ChildJobs.Count)
// {
// // ISSUE: Change this code to look in to child jobs for exception
// if (errors.Count > 0)
// {
// statusMessage = "LocalErrors";
// localErrors = true;
// }
// // check for status of remote command
// for (int i = 0; i < ChildJobs.Count; i++)
// {
// PSRemotingChildJob childJob = ChildJobs[i] as PSRemotingChildJob;
// if (childJob == null) continue;
// if (childJob.ContainsErrors)
// {
// if (localErrors)
// {
// statusMessage = "LocalAndRemoteErrors";
// }
// else
// {
// statusMessage = "RemoteErrors";
// }
// break;
// }
// }
// setFinished = true;
// }
// }
}
#region finish logic
// This variable is set to true if atleast one child job failed.
private bool _atleastOneChildJobFailed = false;
// count of number of child jobs which have finished
private int _finishedChildJobsCount = 0;
// count of number of child jobs which are blocked
private int _blockedChildJobsCount = 0;
// count of child jobs that are in disconnected state.
private int _disconnectedChildJobsCount = 0;
// count of child jobs that are in Debug halted state.
private int _debugChildJobsCount = 0;
/// <summary>
/// Handles the StateChanged event from each of the child job objects.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleChildJobStateChanged(object sender, JobStateEventArgs e)
{
// Update object state to reflect disconnect state related changes in child jobs.
CheckDisconnectedAndUpdateState(e.JobStateInfo.State, e.PreviousJobStateInfo.State);
if (e.JobStateInfo.State == JobState.Blocked)
{
// increment count of blocked child jobs
lock (_syncObject)
{
_blockedChildJobsCount++;
}
// if any of the child job is blocked, we set state to blocked
SetJobState(JobState.Blocked, null);
return;
}
// Handle transition of child job to Debug halt state.
if (e.JobStateInfo.State == JobState.AtBreakpoint)
{
lock (_syncObject) { _debugChildJobsCount++; }
// If any child jobs are Debug halted, we set state to Debug.
SetJobState(JobState.AtBreakpoint);
return;
}
// Handle transition of child job back to running state.
if ((e.JobStateInfo.State == JobState.Running) &&
(e.PreviousJobStateInfo.State == JobState.AtBreakpoint))
{
int totalDebugCount;
lock (_syncObject) { totalDebugCount = --_debugChildJobsCount; }
if (totalDebugCount == 0)
{
SetJobState(JobState.Running);
return;
}
}
// Ignore state changes which are not resulting in state change to finished.
if (!IsFinishedState(e.JobStateInfo.State))
{
return;
}
if (e.JobStateInfo.State == JobState.Failed)
{
// If any of the child job failed, we set status to failed
_atleastOneChildJobFailed = true;
}
bool allChildJobsFinished = false;
lock (_syncObject)
{
_finishedChildJobsCount++;
// We are done
if (_finishedChildJobsCount + _disconnectedChildJobsCount
== ChildJobs.Count)
{
allChildJobsFinished = true;
}
}
if (allChildJobsFinished)
{
// if any child job failed, set status to failed
// If stop was called set, status to stopped
// else completed
if (_disconnectedChildJobsCount > 0)
{
SetJobState(JobState.Disconnected);
}
else if (_atleastOneChildJobFailed)
{
SetJobState(JobState.Failed);
}
else if (_stopIsCalled)
{
SetJobState(JobState.Stopped);
}
else
{
SetJobState(JobState.Completed);
}
}
}
/// <summary>
/// Updates the parent job state based on state of all child jobs.
/// </summary>
/// <param name="newState">New child job state.</param>
/// <param name="prevState">Previous child job state.</param>
private void CheckDisconnectedAndUpdateState(JobState newState, JobState prevState)
{
if (IsFinishedState(JobStateInfo.State))
{
return;
}
// Do all logic inside a lock to ensure it is atomic against
// multiple job thread state changes.
lock (_syncObject)
{
if (newState == JobState.Disconnected)
{
++(_disconnectedChildJobsCount);
// If previous state was Blocked then we need to decrement the count
// since it is now Disconnected.
if (prevState == JobState.Blocked)
{
--_blockedChildJobsCount;
}
// If all unfinished and unblocked child jobs are disconnected then this
// parent job becomes disconnected.
if ((_disconnectedChildJobsCount +
_finishedChildJobsCount +
_blockedChildJobsCount) == ChildJobs.Count)
{
SetJobState(JobState.Disconnected, null);
}
}
else
{
if (prevState == JobState.Disconnected)
{
--(_disconnectedChildJobsCount);
}
if ((newState == JobState.Running) && (JobStateInfo.State == JobState.Disconnected))
{
// Note that SetJobState() takes a lock so it is unnecessary to do
// this under a lock here.
SetJobState(JobState.Running, null);
}
}
}
}
#endregion finish logic
/// <summary>
/// Release all the resources.
/// </summary>
/// <param name="disposing">
/// if true, release all the managed objects.
/// </param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_isDisposed)
{
return;
}
lock (_syncObject)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
}
try
{
if (!IsFinishedState(JobStateInfo.State))
{
StopJob();
}
foreach (Job job in ChildJobs)
{
job.Dispose();
}
_throttleManager.Dispose();
}
finally
{
base.Dispose(disposing);
}
}
}
private bool _isDisposed = false;
private string ConstructLocation()
{
StringBuilder location = new StringBuilder();
if (ChildJobs.Count > 0)
{
foreach (PSRemotingChildJob job in ChildJobs)
{
location.Append(job.Location);
location.Append(',');
}
location.Remove(location.Length - 1, 1);
}
return location.ToString();
}
/// <summary>
/// Computers on which this job is running.
/// </summary>
public override string Location
{
get
{
return ConstructLocation();
}
}
/// <summary>
/// Returns boolean indicating whether the underlying
/// transport for the job (or child jobs) supports
/// connect/disconnect semantics.
/// </summary>
internal override bool CanDisconnect
{
get
{
// If one child job can disconnect then all of them can since
// all child jobs use the same remote runspace transport.
return (ChildJobs.Count > 0) && ChildJobs[0].CanDisconnect;
}
}
/// <summary>
/// Returns runspaces associated with the Job, including
/// child jobs.
/// </summary>
/// <returns>IEnumerable of RemoteRunspaces.</returns>
internal override IEnumerable<RemoteRunspace> GetRunspaces()
{
List<RemoteRunspace> runspaces = new List<RemoteRunspace>();
foreach (PSRemotingChildJob job in ChildJobs)
{
runspaces.Add(job.Runspace as RemoteRunspace);
}
return runspaces;
}
/// <summary>
/// Handles JobUnblocked event from a child job and decrements
/// count of blocked child jobs. When count reaches 0, sets the
/// state of the parent job to running.
/// </summary>
/// <param name="sender">Sender of this event, unused.</param>
/// <param name="eventArgs">event arguments, should be empty in this
/// case</param>
private void HandleJobUnblocked(object sender, EventArgs eventArgs)
{
bool unblockjob = false;
lock (_syncObject)
{
_blockedChildJobsCount--;
if (_blockedChildJobsCount == 0)
{
unblockjob = true;
}
}
if (unblockjob)
{
SetJobState(JobState.Running, null);
}
}
#region Private Members
private readonly ThrottleManager _throttleManager = new ThrottleManager();
private readonly object _syncObject = new object(); // sync object
#endregion Private Members
}
#region DisconnectedJobOperation class
/// <summary>
/// Simple throttle operation class for PSRemoting jobs created in the
/// disconnected state.
/// </summary>
internal class DisconnectedJobOperation : ExecutionCmdletHelper
{
internal DisconnectedJobOperation(Pipeline pipeline)
{
this.pipeline = pipeline;
this.pipeline.StateChanged += HandlePipelineStateChanged;
}
internal override void StartOperation()
{
// This is a no-op since disconnected jobs (pipelines) have
// already been started.
}
internal override void StopOperation()
{
if (pipeline.PipelineStateInfo.State == PipelineState.Running ||
pipeline.PipelineStateInfo.State == PipelineState.Disconnected ||
pipeline.PipelineStateInfo.State == PipelineState.NotStarted)
{
// If the pipeline state has reached Complete/Failed/Stopped
// by the time control reaches here, then this operation
// becomes a no-op. However, an OperationComplete would have
// already been raised from the handler.
pipeline.StopAsync();
}
else
{
// Will have to raise OperationComplete from here,
// else ThrottleManager will have
SendStopComplete();
}
}
internal override event EventHandler<OperationStateEventArgs> OperationComplete;
private void HandlePipelineStateChanged(object sender, PipelineStateEventArgs stateEventArgs)
{
PipelineStateInfo stateInfo = stateEventArgs.PipelineStateInfo;
switch (stateInfo.State)
{
case PipelineState.Running:
case PipelineState.NotStarted:
case PipelineState.Stopping:
case PipelineState.Disconnected:
return;
}
SendStopComplete(stateEventArgs);
}
private void SendStopComplete(EventArgs eventArgs = null)
{
OperationStateEventArgs operationStateEventArgs = new OperationStateEventArgs();
operationStateEventArgs.BaseEvent = eventArgs;
operationStateEventArgs.OperationState = OperationState.StopComplete;
OperationComplete.SafeInvoke(this, operationStateEventArgs);
}
}
#endregion
/// <summary>
/// Class for RemotingChildJob object. This job object invokes command
/// on one remote machine.
/// </summary>
/// <remarks>
/// TODO: I am not sure whether to change this internal to just RemotingChildJob.
/// </remarks>
/// <remarks>
/// Not removing the prefix "PS" as this signifies powershell specific remoting job
/// </remarks>
internal class PSRemotingChildJob : Job, IJobDebugger
{
#region Internal Constructor
/// <summary>
/// Creates an instance of PSRemotingChildJob.
/// </summary>
/// <param name="remoteCommand">Command invoked by this job object.</param>
/// <param name="helper"></param>
/// <param name="throttleManager"></param>
internal PSRemotingChildJob(string remoteCommand, ExecutionCmdletHelper helper, ThrottleManager throttleManager)
: base(remoteCommand)
{
UsesResultsCollection = true;
Dbg.Assert(helper.Pipeline is RemotePipeline, "Pipeline passed should be a remote pipeline");
Helper = helper;
Runspace = helper.Pipeline.Runspace;
_remotePipeline = helper.Pipeline as RemotePipeline;
_throttleManager = throttleManager;
RemoteRunspace remoteRS = Runspace as RemoteRunspace;
if ((remoteRS != null) && (remoteRS.RunspaceStateInfo.State == RunspaceState.BeforeOpen))
{
remoteRS.URIRedirectionReported += HandleURIDirectionReported;
}
AggregateResultsFromHelper(helper);
Runspace.AvailabilityChanged += HandleRunspaceAvailabilityChanged;
RegisterThrottleComplete(throttleManager);
}
/// <summary>
/// Constructs a disconnected child job that is able to connect to a remote
/// runspace/command on a server. The ExecutionCmdletHelperRunspace must
/// contain a remote pipeline object in a disconnected state. In addition
/// the pipeline runspace must be associated with a valid running remote
/// command that can be connected to.
/// </summary>
/// <param name="helper">ExecutionCmdletHelper object containing runspace and pipeline objects.</param>
/// <param name="throttleManager">ThrottleManger object.</param>
/// <param name="aggregateResults">Aggregate results.</param>
internal PSRemotingChildJob(ExecutionCmdletHelper helper, ThrottleManager throttleManager, bool aggregateResults = false)
{
UsesResultsCollection = true;
Dbg.Assert((helper.Pipeline is RemotePipeline), "Helper pipeline object should be a remote pipeline");
Dbg.Assert((helper.Pipeline.PipelineStateInfo.State == PipelineState.Disconnected), "Remote pipeline object must be in Disconnected state.");
Helper = helper;
_remotePipeline = helper.Pipeline as RemotePipeline;
Runspace = helper.Pipeline.Runspace;
_throttleManager = throttleManager;
if (aggregateResults)
{
AggregateResultsFromHelper(helper);
}
else
{
_remotePipeline.StateChanged += HandlePipelineStateChanged;
_remotePipeline.Output.DataReady += HandleOutputReady;
_remotePipeline.Error.DataReady += HandleErrorReady;
}
Runspace.AvailabilityChanged += HandleRunspaceAvailabilityChanged;
IThrottleOperation operation = helper as IThrottleOperation;
operation.OperationComplete += HandleOperationComplete;
SetJobState(JobState.Disconnected, null);
}
/// <summary>
/// Default constructor.
/// </summary>
protected PSRemotingChildJob()
{
}
#endregion Internal Constructor
#region Internal Methods
/// <summary>
/// Connects the remote pipeline that this job represents.
/// </summary>
internal void ConnectAsync()
{
if (JobStateInfo.State != JobState.Disconnected)
{
throw new InvalidJobStateException(JobStateInfo.State);
}
_remotePipeline.ConnectAsync();
}
#endregion
#region stop
// bool isStopCalled = false;
/// <summary>
/// Stops the job.
/// </summary>
public override void StopJob()
{
if (_isDisposed || _stopIsCalled || IsFinishedState(JobStateInfo.State))
{
return;
}
lock (SyncObject)
{
if (_isDisposed || _stopIsCalled || IsFinishedState(JobStateInfo.State))
{
return;
}
_stopIsCalled = true;
}
_throttleManager.StopOperation(Helper);
// if IgnoreStop is set, then StopOperation will
// return immediately, but StopJob should only
// return when job is complete. Waiting on the
// wait handle will ensure that its blocked
// until the job reaches a terminal state
Finished.WaitOne();
}
#endregion stop
#region Properties
/// <summary>
/// Status Message associated with the Job.
/// </summary>
public override string StatusMessage
{
get
{
// ISSUE implement this.
return string.Empty;
}
}
/// <summary>
/// Indicates if there is more data available in
/// this Job.
/// </summary>
public override bool HasMoreData
{
get
{
return (Results.IsOpen || Results.Count > 0);
}
}
/// <summary>
/// Returns the computer on which this command is
/// running.
/// </summary>
public override string Location
{
get
{
return (Runspace != null) ? Runspace.ConnectionInfo.ComputerName : string.Empty;
}
}
/// <summary>
/// </summary>
public Runspace Runspace { get; }
/// <summary>
/// Helper associated with this entity.
/// </summary>
internal ExecutionCmdletHelper Helper { get; } = null;
/// <summary>
/// Used by Invoke-Command cmdlet to show/hide computername property value.
/// Format and Output has capability to understand RemoteObjects and this property lets
/// Format and Output decide whether to show/hide computername.
/// Default is true.
/// </summary>
internal bool HideComputerName
{
get
{
return _hideComputerName;
}
set
{
_hideComputerName = value;
foreach (Job job in this.ChildJobs)
{
PSRemotingChildJob rJob = job as PSRemotingChildJob;
if (rJob != null)
{
rJob.HideComputerName = value;
}
}
}
}
private bool _hideComputerName = true;
/// <summary>
/// Property that indicates this disconnected child job was
/// previously in the Blocked state.
/// </summary>
internal bool DisconnectedAndBlocked { get; private set; } = false;
/// <summary>
/// Returns boolean indicating whether the underlying
/// transport for the job (or child jobs) supports
/// connect/disconnect semantics.
/// </summary>
internal override bool CanDisconnect
{
get
{
RemoteRunspace remoteRS = Runspace as RemoteRunspace;
return remoteRS != null && remoteRS.CanDisconnect;
}
}
#endregion Properties
#region IJobDebugger
/// <summary>
/// Job Debugger.
/// </summary>
public Debugger Debugger
{
get
{
if (_jobDebugger == null)
{
lock (this.SyncObject)
{
if ((_jobDebugger == null) &&
(Runspace.Debugger != null))
{
_jobDebugger = new RemotingJobDebugger(Runspace.Debugger, Runspace, this.Name);
}
}
}
return _jobDebugger;
}
}
/// <summary>
/// True if job is synchronous and can be debugged.
/// </summary>
public bool IsAsync
{
get { return _isAsync; }
set { _isAsync = true; }
}
#endregion
#region Private Methods
/// <summary>
/// Handler which will handle output ready events of the
/// pipeline. The output objects are queued on to the
/// internal stream.
/// </summary>
/// <param name="sender">the pipeline reader which raised
/// this event</param>
/// <param name="eventArgs">Information describing the ready event.</param>
private void HandleOutputReady(object sender, EventArgs eventArgs)
{
PSDataCollectionPipelineReader<PSObject, PSObject> reader =
sender as PSDataCollectionPipelineReader<PSObject, PSObject>;
Collection<PSObject> output = reader.NonBlockingRead();
foreach (PSObject dataObject in output)
{
// attach origin information only if it doesn't exist
// in case of a second-hop scenario, the origin information
// will already be added at the second hop machine
if (dataObject != null)
{
// if the server has already added some properties, which we do not
// want to trust, we simply replace them with the server's
// identity we know of
if (dataObject.Properties[RemotingConstants.ComputerNameNoteProperty] != null)
{
dataObject.Properties.Remove(RemotingConstants.ComputerNameNoteProperty);
}
if (dataObject.Properties[RemotingConstants.RunspaceIdNoteProperty] != null)
{
dataObject.Properties.Remove(RemotingConstants.RunspaceIdNoteProperty);
}
dataObject.Properties.Add(new PSNoteProperty(RemotingConstants.ComputerNameNoteProperty, reader.ComputerName));
dataObject.Properties.Add(new PSNoteProperty(RemotingConstants.RunspaceIdNoteProperty, reader.RunspaceId));
// PSShowComputerName is present for all the objects (from remoting)..this is to allow PSComputerName to be selected.
// Ex: Invoke-Command localhost,blah { gps } | select PSComputerName should work.
if (dataObject.Properties[RemotingConstants.ShowComputerNameNoteProperty] == null)
{
PSNoteProperty showComputerNameNP = new PSNoteProperty(RemotingConstants.ShowComputerNameNoteProperty, !_hideComputerName);
dataObject.Properties.Add(showComputerNameNP);
}
}
this.WriteObject(dataObject);
}
}
/// <summary>
/// Handler which will handle error ready events of the
/// pipeline. The error records are queued on to the
/// internal stream.
/// </summary>
/// <param name="sender">the pipeline reader which raised
/// this event</param>
/// <param name="eventArgs">Information describing the ready event.</param>
private void HandleErrorReady(object sender, EventArgs eventArgs)
{
PSDataCollectionPipelineReader<ErrorRecord, object> reader =
sender as PSDataCollectionPipelineReader<ErrorRecord, object>;
Collection<object> error = reader.NonBlockingRead();
foreach (object errorData in error)
{
ErrorRecord er = errorData as ErrorRecord;
if (er != null)
{
OriginInfo originInfo = new OriginInfo(reader.ComputerName, reader.RunspaceId);
RemotingErrorRecord errorRecord =
new RemotingErrorRecord(er, originInfo);
errorRecord.PreserveInvocationInfoOnce = true;
// ISSUE: Add an Assert for ErrorRecord.
// Add to the PSRemotingChild jobs streams
this.WriteError(errorRecord);
}
}
}
/// <summary>
/// When the client remote session reports a URI redirection, this method will report the
/// message to the user as a Warning using Host method calls.
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
protected void HandleURIDirectionReported(object sender, RemoteDataEventArgs<Uri> eventArgs)
{
string message = StringUtil.Format(RemotingErrorIdStrings.URIRedirectWarningToHost, eventArgs.Data.OriginalString);
this.WriteWarning(message);
}
/// <summary>
/// Handle method executor stream events.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="eventArgs">The event args.</param>
private void HandleHostCalls(object sender, EventArgs eventArgs)
{
ObjectStream hostCallsStream = sender as ObjectStream;
if (hostCallsStream != null)
{
Collection<object> hostCallMethodExecutors =
hostCallsStream.NonBlockingRead(hostCallsStream.Count);
lock (SyncObject)
{
foreach (ClientMethodExecutor hostCallMethodExecutor in hostCallMethodExecutors)
{
Results.Add(new PSStreamObject(PSStreamObjectType.MethodExecutor, hostCallMethodExecutor));
// if the call id of the underlying remote host call is not ServerDispatchTable.VoidCallId
// then the call is waiting on user input. Change state to Blocked
if (hostCallMethodExecutor.RemoteHostCall.CallId != ServerDispatchTable.VoidCallId)
{
SetJobState(JobState.Blocked, null);
}
}
}
}
}
/// <summary>
/// Handle changes in pipeline states.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected virtual void HandlePipelineStateChanged(object sender, PipelineStateEventArgs e)
{
if ((Runspace != null) && (e.PipelineStateInfo.State != PipelineState.Running))
{
// since we got state changed event..we dont need to listen on
// URI redirections anymore
((RemoteRunspace)Runspace).URIRedirectionReported -= HandleURIDirectionReported;
}
PipelineState state = e.PipelineStateInfo.State;
switch (state)
{
case PipelineState.Running:
if (DisconnectedAndBlocked)
{
DisconnectedAndBlocked = false;
SetJobState(JobState.Blocked);
}
else
{
SetJobState(JobState.Running);
}
break;
case PipelineState.Disconnected:
DisconnectedAndBlocked = (JobStateInfo.State == JobState.Blocked);
SetJobState(JobState.Disconnected);
break;
}
// Question: Why is the DoFinish() call on terminal pipeline state deleted
// Answer: Because in the runspace case, when pipeline reaches a terminal state
// OperationComplete will be raised and DoFinish() is called on OperationComplete
// In the computer name case, once pipeline reaches a terminal state, runspace is
// closed which will result in an OperationComplete event
}
/// <summary>
/// Handle a throttle complete event.
/// </summary>
/// <param name="sender">Sender of this event.</param>
/// <param name="eventArgs">Not used in this method.</param>
private void HandleThrottleComplete(object sender, EventArgs eventArgs)
{
// Question: Why do we register for HandleThrottleComplete when we have already
// registered for PipelineStateChangedEvent?
// Answer: Because ThrottleManager at a given time can have some pipelines which are
// still not started. If TM.Stop() is called, then it simply discards those pipelines and
// PipelineStateChangedEvent is not called for them. For such jobs, we depend on
// HandleThrottleComplete to mark the finish of job.
// Question: So it is possible in some cases DoFinish can be called twice.
// Answer: Yes: One from PipelineStateChangedEvent and Another here. But
// DoFinish has logic to check if it has been already called and second call
// becomes noOp.
DoFinish();
}
/// <summary>
/// Handle the operation complete event.
/// </summary>
/// <param name="sender">Sender of this event.</param>
/// <param name="stateEventArgs">Operation complete event args.</param>
protected virtual void HandleOperationComplete(object sender, OperationStateEventArgs stateEventArgs)
{
// Question:Why are we registering for OperationComplete if we already
// registering for StateChangedEvent and ThrottleComplete event
// Answer:Because in case of computer, if Runspace.Open it self fails,
// no pipeline is created and no pipeline state changed event is raised.
// We can wait for throttle complete, but it is raised only when all the
// operations are completed and this means that status of job is not updated
// untill Operation Complete.
ExecutionCmdletHelper helper = sender as ExecutionCmdletHelper;
Dbg.Assert(helper != null, "Sender of OperationComplete has to be ExecutionCmdletHelper");
DeterminedAndSetJobState(helper);
}
private bool _doFinishCalled = false;
/// <summary>
/// This method marks the completion state for Job. Also if job failed, it processes the
/// reason of failure.
/// </summary>
protected virtual void DoFinish()
{
if (_doFinishCalled)
return;
lock (SyncObject)
{
if (_doFinishCalled)
return;
_doFinishCalled = true;
}
DeterminedAndSetJobState(Helper);
DoCleanupOnFinished();
}
/// <summary>
/// This is the pretty formated error record associated with the reason of failure.
/// </summary>
private ErrorRecord _failureErrorRecord;
/// <summary>
/// This is the pretty formated error record associated with the reason of failure.
/// This is set if Job state is Failed and Reason has a exception.
/// </summary>
internal ErrorRecord FailureErrorRecord
{
get
{
return _failureErrorRecord;
}
}
/// <summary>
/// Process the exceptions to decide reason for job failure.
/// </summary>
/// <param name="helper"></param>
/// <param name="failureException"></param>
/// <param name="failureErrorRecord"></param>
protected void ProcessJobFailure(ExecutionCmdletHelper helper, out Exception failureException,
out ErrorRecord failureErrorRecord)
{
// There are three errors possible
// 1. The remote runspace is in (or went into) a
// broken state. This information is available
// in the runspace state information
// 2. The remote pipeline failed because of an
// exception. This information is available
// in the pipeline state information
// 3. Runspace.OpenAsync or Pipeline.InvokeAsync threw exception
// They are in Helper.InternalException
Dbg.Assert(helper != null, "helper is null");
RemotePipeline pipeline = helper.Pipeline as RemotePipeline;
Dbg.Assert(pipeline != null, "pipeline is null");
RemoteRunspace runspace = pipeline.GetRunspace() as RemoteRunspace;
Dbg.Assert(runspace != null, "runspace is null");
failureException = null;
failureErrorRecord = null;
if (helper.InternalException != null)
{
string errorId = "RemotePipelineExecutionFailed";
failureException = helper.InternalException;
if ((failureException is InvalidRunspaceStateException) || (failureException is InvalidRunspacePoolStateException))
{
errorId = "InvalidSessionState";
if (!string.IsNullOrEmpty(failureException.Source))
{
errorId = string.Format(System.Globalization.CultureInfo.InvariantCulture,
"{0},{1}", errorId, failureException.Source);
}
}
failureErrorRecord = new ErrorRecord(helper.InternalException,
errorId, ErrorCategory.OperationStopped,
helper);
}
// there is a failure reason available in the runspace
else if ((runspace.RunspaceStateInfo.State == RunspaceState.Broken) ||
(runspace.RunspaceStateInfo.Reason != null))
{
failureException = runspace.RunspaceStateInfo.Reason;
object targetObject = runspace.ConnectionInfo.ComputerName;
string errorDetails = null;
// set the transport message in the error detail so that
// the user can directly get to see the message without
// having to mine through the error record details
PSRemotingTransportException transException =
failureException as PSRemotingTransportException;
string fullyQualifiedErrorId =
System.Management.Automation.Remoting.Client.WSManTransportManagerUtils.GetFQEIDFromTransportError(
(transException != null) ? transException.ErrorCode : 0,
"PSSessionStateBroken");
if (transException != null)
{
errorDetails = "[" + runspace.ConnectionInfo.ComputerName + "] ";
if (transException.ErrorCode ==
Remoting.Client.WSManNativeApi.ERROR_WSMAN_REDIRECT_REQUESTED)
{
// Handling a special case for redirection..we should talk about
// AllowRedirection parameter and WSManMaxRedirectionCount preference
// variables
string message = PSRemotingErrorInvariants.FormatResourceString(
RemotingErrorIdStrings.URIRedirectionReported,
transException.Message,
"MaximumConnectionRedirectionCount",
Microsoft.PowerShell.Commands.PSRemotingBaseCmdlet.DEFAULT_SESSION_OPTION,
"AllowRedirection");
errorDetails += message;
}
else if (!string.IsNullOrEmpty(transException.Message))
{
errorDetails += transException.Message;
}
else if (!string.IsNullOrEmpty(transException.TransportMessage))
{
errorDetails += transException.TransportMessage;
}
}
if (failureException == null)
{
failureException = new RuntimeException(
PSRemotingErrorInvariants.FormatResourceString(
RemotingErrorIdStrings.RemoteRunspaceOpenUnknownState,
runspace.RunspaceStateInfo.State));
}
failureErrorRecord = new ErrorRecord(failureException, targetObject,
fullyQualifiedErrorId, ErrorCategory.OpenError,
null, null, null, null, null, errorDetails, null);
}
else if (pipeline.PipelineStateInfo.State == PipelineState.Failed
|| (pipeline.PipelineStateInfo.State == PipelineState.Stopped
&& pipeline.PipelineStateInfo.Reason != null
&& pipeline.PipelineStateInfo.Reason is not PipelineStoppedException))
{
// Pipeline stopped state is also an error condition if the associated exception is not 'PipelineStoppedException'.
object targetObject = runspace.ConnectionInfo.ComputerName;
failureException = pipeline.PipelineStateInfo.Reason;
if (failureException != null)
{
RemoteException rException = failureException as RemoteException;
ErrorRecord errorRecord = null;
if (rException != null)
{
errorRecord = rException.ErrorRecord;
// A RemoteException will hide a PipelineStoppedException, which should be ignored.
if (errorRecord != null &&
errorRecord.FullyQualifiedErrorId.Equals("PipelineStopped", StringComparison.OrdinalIgnoreCase))
{
// PipelineStoppedException should not be reported as error.
failureException = null;
return;
}
}
else
{
// at this point, there may be no failure reason available in
// the runspace because the remoting protocol
// layer may not have yet assigned it to the runspace
// in such a case, the remoting protocol layer would have
// assigned an exception in the client end to the pipeline
// create an error record from it and write it out
errorRecord = new ErrorRecord(pipeline.PipelineStateInfo.Reason,
"JobFailure", ErrorCategory.OperationStopped,
targetObject);
}
string computerName = ((RemoteRunspace)pipeline.GetRunspace()).ConnectionInfo.ComputerName;
Guid runspaceId = pipeline.GetRunspace().InstanceId;
OriginInfo originInfo = new OriginInfo(computerName, runspaceId);
failureErrorRecord = new RemotingErrorRecord(errorRecord, originInfo);
}
}
}
/// <summary>
/// Release all the resources.
/// </summary>
/// <param name="disposing">
/// if true, release all the managed objects.
/// </param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_isDisposed)
{
return;
}
lock (SyncObject)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
}
try
{
DoCleanupOnFinished();
}
finally
{
base.Dispose(disposing);
}
}
}
private bool _isDisposed = false;
private bool _cleanupDone = false;
/// <summary>
/// Cleanup after state changes to finished.
/// </summary>
protected virtual void DoCleanupOnFinished()
{
bool doCleanup = false;
if (!_cleanupDone)
{
lock (SyncObject)
{
if (!_cleanupDone)
{
_cleanupDone = true;
doCleanup = true;
}
}
}
if (!doCleanup) return;
StopAggregateResultsFromHelper(Helper);
Runspace.AvailabilityChanged -= HandleRunspaceAvailabilityChanged;
IThrottleOperation operation = Helper as IThrottleOperation;
operation.OperationComplete -= HandleOperationComplete;
UnregisterThrottleComplete(_throttleManager);
_throttleManager = null;
}
/// <summary>
/// Aggregates results from the pipeline associated
/// with the specified helper.
/// </summary>
/// <param name="helper">helper whose pipeline results
/// need to be aggregated</param>
protected void AggregateResultsFromHelper(ExecutionCmdletHelper helper)
{
// Get the pipeline associated with this helper and register for appropriate events
Pipeline pipeline = helper.Pipeline;
pipeline.Output.DataReady += HandleOutputReady;
pipeline.Error.DataReady += HandleErrorReady;
pipeline.StateChanged += HandlePipelineStateChanged;
// Register handler for method executor object stream.
Dbg.Assert(pipeline is RemotePipeline, "pipeline is RemotePipeline");
RemotePipeline remotePipeline = pipeline as RemotePipeline;
remotePipeline.MethodExecutorStream.DataReady += HandleHostCalls;
remotePipeline.PowerShell.Streams.Progress.DataAdded += HandleProgressAdded;
remotePipeline.PowerShell.Streams.Warning.DataAdded += HandleWarningAdded;
remotePipeline.PowerShell.Streams.Verbose.DataAdded += HandleVerboseAdded;
remotePipeline.PowerShell.Streams.Debug.DataAdded += HandleDebugAdded;
remotePipeline.PowerShell.Streams.Information.DataAdded += HandleInformationAdded;
// Enable method executor stream so that host methods are queued up
// on it instead of being executed asynchronously when they arrive.
remotePipeline.IsMethodExecutorStreamEnabled = true;
IThrottleOperation operation = helper as IThrottleOperation;
operation.OperationComplete += HandleOperationComplete;
}
/// <summary>
/// If the pipeline is not null, returns the pipeline's PowerShell
/// If it is null, then returns the PowerShell with the specified
/// instance Id.
/// </summary>
/// <param name="pipeline">Remote pipeline.</param>
/// <param name="instanceId">Instance as described in event args.</param>
/// <returns>PowerShell instance.</returns>
private PowerShell GetPipelinePowerShell(RemotePipeline pipeline, Guid instanceId)
{
if (pipeline != null)
{
return pipeline.PowerShell;
}
return GetPowerShell(instanceId);
}
/// <summary>
/// When a debug message is raised in the underlying PowerShell
/// add it to the jobs debug stream.
/// </summary>
/// <param name="sender">Unused.</param>
/// <param name="eventArgs">Arguments describing this event.</param>
private void HandleDebugAdded(object sender, DataAddedEventArgs eventArgs)
{
int index = eventArgs.Index;
PowerShell powershell = GetPipelinePowerShell(_remotePipeline, eventArgs.PowerShellInstanceId);
if (powershell != null)
{
this.Debug.Add(powershell.Streams.Debug[index]);
}
}
/// <summary>
/// When a verbose message is raised in the underlying PowerShell
/// add it to the jobs verbose stream.
/// </summary>
/// <param name="sender">Unused.</param>
/// <param name="eventArgs">Arguments describing this event.</param>
private void HandleVerboseAdded(object sender, DataAddedEventArgs eventArgs)
{
int index = eventArgs.Index;
PowerShell powershell = GetPipelinePowerShell(_remotePipeline, eventArgs.PowerShellInstanceId);
if (powershell != null)
{
this.Verbose.Add(powershell.Streams.Verbose[index]);
}
}
/// <summary>
/// When a warning message is raised in the underlying PowerShell
/// add it to the jobs warning stream.
/// </summary>
/// <param name="sender">Unused.</param>
/// <param name="eventArgs">Arguments describing this event.</param>
private void HandleWarningAdded(object sender, DataAddedEventArgs eventArgs)
{
int index = eventArgs.Index;
PowerShell powershell = GetPipelinePowerShell(_remotePipeline, eventArgs.PowerShellInstanceId);
if (powershell != null)
{
WarningRecord warningRecord = powershell.Streams.Warning[index];
this.Warning.Add(warningRecord);
this.Results.Add(new PSStreamObject(PSStreamObjectType.WarningRecord, warningRecord));
}
}
/// <summary>
/// When a progress message is raised in the underlying PowerShell
/// add it to the jobs progress tream.
/// </summary>
/// <param name="sender">Unused.</param>
/// <param name="eventArgs">Arguments describing this event.</param>
private void HandleProgressAdded(object sender, DataAddedEventArgs eventArgs)
{
int index = eventArgs.Index;
PowerShell powershell = GetPipelinePowerShell(_remotePipeline, eventArgs.PowerShellInstanceId);
if (powershell != null)
{
this.Progress.Add(powershell.Streams.Progress[index]);
}
}
/// <summary>
/// When a Information message is raised in the underlying PowerShell
/// add it to the jobs Information stream.
/// </summary>
/// <param name="sender">Unused.</param>
/// <param name="eventArgs">Arguments describing this event.</param>
private void HandleInformationAdded(object sender, DataAddedEventArgs eventArgs)
{
int index = eventArgs.Index;
PowerShell powershell = GetPipelinePowerShell(_remotePipeline, eventArgs.PowerShellInstanceId);
if (powershell != null)
{
InformationRecord informationRecord = powershell.Streams.Information[index];
this.Information.Add(informationRecord);
// Host output is handled by the hosting APIs directly, so we need to add a tag that it was
// forwarded so that it is not written twice.
// For all other Information records, forward them.
if (informationRecord.Tags.Contains("PSHOST"))
{
informationRecord.Tags.Add("FORWARDED");
}
this.Results.Add(new PSStreamObject(PSStreamObjectType.Information, informationRecord));
}
}
/// <summary>
/// Stops collecting results from the pipeline associated with
/// the specified helper.
/// </summary>
/// <param name="helper">helper class whose pipeline results
/// aggregation has to be stopped</param>
protected void StopAggregateResultsFromHelper(ExecutionCmdletHelper helper)
{
// Get the pipeline associated with this helper and register for appropriate events
RemoveAggreateCallbacksFromHelper(helper);
Pipeline pipeline = helper.Pipeline;
pipeline.Dispose();
pipeline = null;
}
/// <summary>
/// Removes aggregate callbacks from pipeline so that a new job object can
/// be created and can add its own callbacks.
/// This is to support Invoke-Command auto-disconnect where a new PSRemoting
/// job must be created to pass back to user for connection.
/// </summary>
/// <param name="helper">Helper class.</param>
protected void RemoveAggreateCallbacksFromHelper(ExecutionCmdletHelper helper)
{
// Remove old data output callbacks from pipeline so new callbacks can be added.
Pipeline pipeline = helper.Pipeline;
pipeline.Output.DataReady -= HandleOutputReady;
pipeline.Error.DataReady -= HandleErrorReady;
pipeline.StateChanged -= HandlePipelineStateChanged;
// Remove old data aggregation and host calls.
Dbg.Assert(pipeline is RemotePipeline, "pipeline is RemotePipeline");
RemotePipeline remotePipeline = pipeline as RemotePipeline;
remotePipeline.MethodExecutorStream.DataReady -= HandleHostCalls;
if (remotePipeline.PowerShell != null)
{
remotePipeline.PowerShell.Streams.Progress.DataAdded -= HandleProgressAdded;
remotePipeline.PowerShell.Streams.Warning.DataAdded -= HandleWarningAdded;
remotePipeline.PowerShell.Streams.Verbose.DataAdded -= HandleVerboseAdded;
remotePipeline.PowerShell.Streams.Debug.DataAdded -= HandleDebugAdded;
remotePipeline.PowerShell.Streams.Information.DataAdded -= HandleInformationAdded;
remotePipeline.IsMethodExecutorStreamEnabled = false;
}
}
/// <summary>
/// Register for throttle complete from the specified
/// throttlemanager.
/// </summary>
/// <param name="throttleManager"></param>
protected void RegisterThrottleComplete(ThrottleManager throttleManager)
{
throttleManager.ThrottleComplete += HandleThrottleComplete;
}
/// <summary>
/// Unregister for throttle complete from the specified
/// throttle manager.
/// </summary>
/// <param name="throttleManager"></param>
protected void UnregisterThrottleComplete(ThrottleManager throttleManager)
{
throttleManager.ThrottleComplete -= HandleThrottleComplete;
}
/// <summary>
/// Determine the current state of the job based on the underlying
/// pipeline state and set the state accordingly.
/// </summary>
/// <param name="helper"></param>
protected void DeterminedAndSetJobState(ExecutionCmdletHelper helper)
{
Exception failureException;
// Process the reason in case of failure.
ProcessJobFailure(helper, out failureException, out _failureErrorRecord);
if (failureException != null)
{
SetJobState(JobState.Failed, failureException);
}
else
{
// Get the state of the pipeline
PipelineState state = helper.Pipeline.PipelineStateInfo.State;
if (state == PipelineState.NotStarted)
{
// This is a case in which pipeline was not started and TM.Stop was
// called. See comment in HandleThrottleComplete
SetJobState(JobState.Stopped);
}
else if (state == PipelineState.Completed)
{
SetJobState(JobState.Completed);
}
else
{
SetJobState(JobState.Stopped);
}
}
}
/// <summary>
/// Set the state of the current job from blocked to
/// running and raise an event indicating to this
/// parent job that this job is unblocked.
/// </summary>
internal void UnblockJob()
{
Dbg.Assert(JobStateInfo.State == JobState.Blocked,
"Current state of job must be blocked before it can be unblocked");
SetJobState(JobState.Running, null);
Dbg.Assert(JobUnblocked != null, "Parent job must register for JobUnblocked event from all child jobs");
JobUnblocked.SafeInvoke(this, EventArgs.Empty);
}
/// <summary>
/// Returns the PowerShell for the specified instance id.
/// </summary>
/// <param name="instanceId">Instance id of powershell.</param>
/// <returns>Powershell instance.</returns>
internal virtual PowerShell GetPowerShell(Guid instanceId)
{
// this should be called only in the derived implementation
throw PSTraceSource.NewInvalidOperationException();
}
/// <summary>
/// Returns the PowerShell object associated with this remote child job.
/// </summary>
/// <returns>PowerShell object.</returns>
internal PowerShell GetPowerShell()
{
PowerShell ps = null;
if (_remotePipeline != null)
{
ps = _remotePipeline.PowerShell;
}
return ps;
}
/// <summary>
/// Monitor runspace availability and if it goes to RemoteDebug then set
/// job state to Debug. Set back to Running when availability goes back to
/// Busy (indicating the script/command is running again).
/// </summary>
/// <param name="sender">Runspace.</param>
/// <param name="e">RunspaceAvailabilityEventArgs.</param>
private void HandleRunspaceAvailabilityChanged(object sender, RunspaceAvailabilityEventArgs e)
{
RunspaceAvailability prevAvailability = _prevRunspaceAvailability;
_prevRunspaceAvailability = e.RunspaceAvailability;
if (e.RunspaceAvailability == RunspaceAvailability.RemoteDebug)
{
SetJobState(JobState.AtBreakpoint);
}
else if ((prevAvailability == RunspaceAvailability.RemoteDebug) &&
(e.RunspaceAvailability == RunspaceAvailability.Busy))
{
SetJobState(JobState.Running);
}
}
/// <summary>
/// Event raised by this job to indicate to its parent that
/// its now unblocked by the user.
/// </summary>
internal event EventHandler JobUnblocked;
#endregion Private Methods
#region Private Members
// helper associated with this job object
private readonly RemotePipeline _remotePipeline = null;
// object used for synchronization
protected object SyncObject = new object();
private ThrottleManager _throttleManager;
private bool _stopIsCalled = false;
private volatile Debugger _jobDebugger;
private bool _isAsync = true;
private RunspaceAvailability _prevRunspaceAvailability = RunspaceAvailability.None;
#endregion Private Members
}
/// <summary>
/// This is a debugger wrapper class used to allow debugging of
/// remoting jobs that implement the IJobDebugger interface.
/// </summary>
internal sealed class RemotingJobDebugger : Debugger
{
#region Members
private readonly Debugger _wrappedDebugger;
private readonly Runspace _runspace;
private readonly string _jobName;
#endregion
#region Constructor
private RemotingJobDebugger() { }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="debugger">Debugger to wrap.</param>
/// <param name="runspace">Remote runspace.</param>
/// <param name="jobName">Name of associated job.</param>
public RemotingJobDebugger(
Debugger debugger,
Runspace runspace,
string jobName)
{
if (debugger == null)
{
throw new PSArgumentNullException(nameof(debugger));
}
if (runspace == null)
{
throw new PSArgumentNullException(nameof(runspace));
}
_wrappedDebugger = debugger;
_runspace = runspace;
_jobName = jobName ?? string.Empty;
// Create handlers for wrapped debugger events.
_wrappedDebugger.BreakpointUpdated += HandleBreakpointUpdated;
_wrappedDebugger.DebuggerStop += HandleDebuggerStop;
}
#endregion
#region Debugger overrides
/// <summary>
/// Evaluates provided command either as a debugger specific command
/// or a PowerShell command.
/// </summary>
/// <param name="command">PowerShell command.</param>
/// <param name="output">Output.</param>
/// <returns>DebuggerCommandResults.</returns>
public override DebuggerCommandResults ProcessCommand(PSCommand command, PSDataCollection<PSObject> output)
{
// Special handling for the prompt command.
if (command.Commands[0].CommandText.Trim().Equals("prompt", StringComparison.OrdinalIgnoreCase))
{
return HandlePromptCommand(output);
}
return _wrappedDebugger.ProcessCommand(command, output);
}
/// <summary>
/// Adds the provided set of breakpoints to the debugger.
/// </summary>
/// <param name="breakpoints">Breakpoints to set.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
public override void SetBreakpoints(IEnumerable<Breakpoint> breakpoints, int? runspaceId) =>
_wrappedDebugger.SetBreakpoints(breakpoints, runspaceId);
/// <summary>
/// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets.
/// </summary>
/// <param name="id">Id of the breakpoint you want.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>A a breakpoint with the specified id.</returns>
public override Breakpoint GetBreakpoint(int id, int? runspaceId) =>
_wrappedDebugger.GetBreakpoint(id, runspaceId);
/// <summary>
/// Returns breakpoints on a runspace.
/// </summary>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>A list of breakpoints in a runspace.</returns>
public override List<Breakpoint> GetBreakpoints(int? runspaceId) =>
_wrappedDebugger.GetBreakpoints(runspaceId);
/// <summary>
/// Sets a command breakpoint in the debugger.
/// </summary>
/// <param name="command">The name of the command that will trigger the breakpoint. This value may not be null.</param>
/// <param name="action">The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit.</param>
/// <param name="path">The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>The command breakpoint that was set.</returns>
public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action, string path, int? runspaceId) =>
_wrappedDebugger.SetCommandBreakpoint(command, action, path, runspaceId);
/// <summary>
/// Sets a line breakpoint in the debugger.
/// </summary>
/// <param name="path">The path to the script file where the breakpoint may be hit. This value may not be null.</param>
/// <param name="line">The line in the script file where the breakpoint may be hit. This value must be greater than or equal to 1.</param>
/// <param name="column">The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line.</param>
/// <param name="action">The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>The line breakpoint that was set.</returns>
public override LineBreakpoint SetLineBreakpoint(string path, int line, int column, ScriptBlock action, int? runspaceId) =>
_wrappedDebugger.SetLineBreakpoint(path, line, column, action, runspaceId);
/// <summary>
/// Sets a variable breakpoint in the debugger.
/// </summary>
/// <param name="variableName">The name of the variable that will trigger the breakpoint. This value may not be null.</param>
/// <param name="accessMode">The variable access mode that will trigger the breakpoint.</param>
/// <param name="action">The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit.</param>
/// <param name="path">The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>The variable breakpoint that was set.</returns>
public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode, ScriptBlock action, string path, int? runspaceId) =>
_wrappedDebugger.SetVariableBreakpoint(variableName, accessMode, action, path, runspaceId);
/// <summary>
/// Removes a breakpoint from the debugger.
/// </summary>
/// <param name="breakpoint">The breakpoint to remove from the debugger. This value may not be null.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>True if the breakpoint was removed from the debugger; false otherwise.</returns>
public override bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId) =>
_wrappedDebugger.RemoveBreakpoint(breakpoint, runspaceId);
/// <summary>
/// Enables a breakpoint in the debugger.
/// </summary>
/// <param name="breakpoint">The breakpoint to enable in the debugger. This value may not be null.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>The updated breakpoint if it was found; null if the breakpoint was not found in the debugger.</returns>
public override Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId) =>
_wrappedDebugger.EnableBreakpoint(breakpoint, runspaceId);
/// <summary>
/// Disables a breakpoint in the debugger.
/// </summary>
/// <param name="breakpoint">The breakpoint to enable in the debugger. This value may not be null.</param>
/// <param name="runspaceId">The runspace id of the runspace you want to interact with. A null value will use the current runspace.</param>
/// <returns>The updated breakpoint if it was found; null if the breakpoint was not found in the debugger.</returns>
public override Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId) =>
_wrappedDebugger.DisableBreakpoint(breakpoint, runspaceId);
/// <summary>
/// Sets the debugger resume action.
/// </summary>
/// <param name="resumeAction">DebuggerResumeAction.</param>
public override void SetDebuggerAction(DebuggerResumeAction resumeAction)
{
_wrappedDebugger.SetDebuggerAction(resumeAction);
}
/// <summary>
/// Stops a running command.
/// </summary>
public override void StopProcessCommand()
{
_wrappedDebugger.StopProcessCommand();
}
/// <summary>
/// Returns current debugger stop event arguments if debugger is in
/// debug stop state. Otherwise returns null.
/// </summary>
/// <returns>DebuggerStopEventArgs.</returns>
public override DebuggerStopEventArgs GetDebuggerStopArgs()
{
return _wrappedDebugger.GetDebuggerStopArgs();
}
/// <summary>
/// Sets the parent debugger, breakpoints, and other debugging context information.
/// </summary>
/// <param name="parent">Parent debugger.</param>
/// <param name="breakPoints">List of breakpoints.</param>
/// <param name="startAction">Debugger mode.</param>
/// <param name="host">PowerShell host.</param>
/// <param name="path">Current path.</param>
public override void SetParent(
Debugger parent,
IEnumerable<Breakpoint> breakPoints,
DebuggerResumeAction? startAction,
PSHost host,
PathInfo path)
{
// For now always enable step mode debugging.
SetDebuggerStepMode(true);
}
/// <summary>
/// Sets the debugger mode.
/// </summary>
public override void SetDebugMode(DebugModes mode)
{
_wrappedDebugger.SetDebugMode(mode);
base.SetDebugMode(mode);
}
/// <summary>
/// Returns IEnumerable of CallStackFrame objects.
/// </summary>
/// <returns></returns>
public override IEnumerable<CallStackFrame> GetCallStack()
{
return _wrappedDebugger.GetCallStack();
}
/// <summary>
/// Sets debugger stepping mode.
/// </summary>
/// <param name="enabled">True if stepping is to be enabled.</param>
public override void SetDebuggerStepMode(bool enabled)
{
_wrappedDebugger.SetDebuggerStepMode(enabled);
}
/// <summary>
/// CheckStateAndRaiseStopEvent.
/// </summary>
internal void CheckStateAndRaiseStopEvent()
{
RemoteDebugger remoteDebugger = _wrappedDebugger as RemoteDebugger;
if (remoteDebugger != null)
{
remoteDebugger.CheckStateAndRaiseStopEvent();
}
}
/// <summary>
/// True when debugger is stopped at a breakpoint.
/// </summary>
public override bool InBreakpoint
{
get { return _wrappedDebugger.InBreakpoint; }
}
#endregion
#region Private methods
private void HandleDebuggerStop(object sender, DebuggerStopEventArgs e)
{
Pipeline remoteRunningCmd = null;
try
{
// For remote debugging drain/block output channel.
remoteRunningCmd = DrainAndBlockRemoteOutput();
this.RaiseDebuggerStopEvent(e);
}
finally
{
RestoreRemoteOutput(remoteRunningCmd);
}
}
private Pipeline DrainAndBlockRemoteOutput()
{
// We only do this for remote runspaces.
if (_runspace is not RemoteRunspace) { return null; }
Pipeline runningCmd = _runspace.GetCurrentlyRunningPipeline();
if (runningCmd != null)
{
runningCmd.DrainIncomingData();
runningCmd.SuspendIncomingData();
return runningCmd;
}
return null;
}
private static void RestoreRemoteOutput(Pipeline runningCmd)
{
if (runningCmd != null)
{
runningCmd.ResumeIncomingData();
}
}
private void HandleBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e)
{
this.RaiseBreakpointUpdatedEvent(e);
}
private DebuggerCommandResults HandlePromptCommand(PSDataCollection<PSObject> output)
{
// Nested debugged runspace prompt should look like:
// [DBG]: [JobName]: PS C:\>>
string promptScript = "'[DBG]: '" + " + " + "'[" + CodeGeneration.EscapeSingleQuotedStringContent(_jobName) + "]: '" + " + " + @"""PS $($executionContext.SessionState.Path.CurrentLocation)>> """;
PSCommand promptCommand = new PSCommand();
promptCommand.AddScript(promptScript);
_wrappedDebugger.ProcessCommand(promptCommand, output);
return new DebuggerCommandResults(null, true);
}
#endregion
}
/// <summary>
/// This job is used for running as a job the results from multiple
/// pipelines. This is used in synchronous Invoke-Expression execution.
/// </summary>
/// <remarks>
/// TODO: I am not sure whether to change this internal to just InvokeExpressionSyncJob.
/// </remarks>
/// <remarks>
/// Not removing the prefix "PS" as this signifies powershell specific remoting job
/// </remarks>
internal class PSInvokeExpressionSyncJob : PSRemotingChildJob
{
#region Private Members
private readonly List<ExecutionCmdletHelper> _helpers = new List<ExecutionCmdletHelper>();
private readonly ThrottleManager _throttleManager;
private readonly Dictionary<Guid, PowerShell> _powershells = new Dictionary<Guid, PowerShell>();
private int _pipelineFinishedCount;
private int _pipelineDisconnectedCount;
#endregion Private Members
#region Constructors
/// <summary>
/// Construct an invoke-expression sync job.
/// </summary>
/// <param name="operations">List of operations to use.</param>
/// <param name="throttleManager">throttle manager to use for
/// this job</param>
internal PSInvokeExpressionSyncJob(List<IThrottleOperation> operations, ThrottleManager throttleManager)
{
UsesResultsCollection = true;
Results.AddRef();
_throttleManager = throttleManager;
RegisterThrottleComplete(_throttleManager);
foreach (IThrottleOperation operation in operations)
{
ExecutionCmdletHelper helper = operation as ExecutionCmdletHelper;
RemoteRunspace remoteRS = helper.Pipeline.Runspace as RemoteRunspace;
if (remoteRS != null)
{
remoteRS.StateChanged += HandleRunspaceStateChanged;
if (remoteRS.RunspaceStateInfo.State == RunspaceState.BeforeOpen)
{
remoteRS.URIRedirectionReported += HandleURIDirectionReported;
}
}
_helpers.Add(helper);
AggregateResultsFromHelper(helper);
Dbg.Assert(helper.Pipeline is RemotePipeline, "Only remote pipeline can be used in InvokeExpressionSyncJob");
RemotePipeline pipeline = helper.Pipeline as RemotePipeline;
_powershells.Add(pipeline.PowerShell.InstanceId, pipeline.PowerShell);
}
}
#endregion Constructors
#region Protected Methods
private bool _cleanupDone = false;
/// <summary>
/// Clean up once job is finished.
/// </summary>
protected override void DoCleanupOnFinished()
{
bool doCleanup = false;
if (!_cleanupDone)
{
lock (SyncObject)
{
if (!_cleanupDone)
{
_cleanupDone = true;
doCleanup = true;
}
}
}
if (!doCleanup) return;
foreach (ExecutionCmdletHelper helper in _helpers)
{
// cleanup remote runspace related handlers
RemoteRunspace remoteRS = helper.PipelineRunspace as RemoteRunspace;
if (remoteRS != null)
{
remoteRS.StateChanged -= HandleRunspaceStateChanged;
remoteRS.URIRedirectionReported -= HandleURIDirectionReported;
}
StopAggregateResultsFromHelper(helper);
}
UnregisterThrottleComplete(_throttleManager);
// throttleManager = null;
Results.DecrementRef();
}
/// <summary>
/// Release all resources.
/// </summary>
/// <param name="disposing">True if called by Dispose().</param>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
/// <summary>
/// Handles operation complete from the operations. Adds an error record
/// to results whenever an error is encountered.
/// </summary>
/// <param name="sender">Sender of this event.</param>
/// <param name="stateEventArgs">Arguments describing this event, unused.</param>
protected override void HandleOperationComplete(object sender, OperationStateEventArgs stateEventArgs)
{
ExecutionCmdletHelper helper = sender as ExecutionCmdletHelper;
Dbg.Assert(helper != null, "Sender of OperationComplete has to be ExecutionCmdletHelper");
Exception failureException;
// Process the reason in case of failure.
ErrorRecord failureErrorRecord;
ProcessJobFailure(helper, out failureException, out failureErrorRecord);
if (failureErrorRecord != null)
{
this.WriteError(failureErrorRecord);
}
}
/// <summary>
/// Handle changes in pipeline states.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void HandlePipelineStateChanged(object sender, PipelineStateEventArgs e)
{
PipelineState state = e.PipelineStateInfo.State;
switch (state)
{
case PipelineState.Running:
SetJobState(JobState.Running);
break;
case PipelineState.Completed:
case PipelineState.Failed:
case PipelineState.Stopped:
case PipelineState.Disconnected:
CheckForAndSetDisconnectedState(state);
break;
}
}
/// <summary>
/// Checks for a condition where all pipelines are either finished
/// or disconnected and at least one pipeline is disconnected.
/// In this case the Job state is set to Disconnected.
/// </summary>
private void CheckForAndSetDisconnectedState(PipelineState pipelineState)
{
bool setJobStateToDisconnected;
lock (SyncObject)
{
if (IsTerminalState())
{
return;
}
switch (pipelineState)
{
case PipelineState.Completed:
case PipelineState.Failed:
case PipelineState.Stopped:
_pipelineFinishedCount += 1;
break;
case PipelineState.Disconnected:
_pipelineDisconnectedCount += 1;
break;
}
setJobStateToDisconnected = ((_pipelineFinishedCount + _pipelineDisconnectedCount) == _helpers.Count &&
_pipelineDisconnectedCount > 0);
}
if (setJobStateToDisconnected)
{
// Job cannot finish with pipelines in disconnected state.
// Set Job state to Disconnected.
SetJobState(JobState.Disconnected);
}
}
/// <summary>
/// Used to stop all operations.
/// </summary>
public override void StopJob()
{
_throttleManager.StopAllOperations();
}
private bool _doFinishCalled = false;
/// <summary>
/// This method marks the completion state for Job. Also if job failed, it processes the
/// reason of failure.
/// </summary>
protected override void DoFinish()
{
if (_doFinishCalled)
return;
lock (SyncObject)
{
if (_doFinishCalled)
return;
_doFinishCalled = true;
}
foreach (ExecutionCmdletHelper helper in _helpers)
{
DeterminedAndSetJobState(helper);
}
if (_helpers.Count == 0 && this.JobStateInfo.State == JobState.NotStarted)
{
SetJobState(JobState.Completed);
}
DoCleanupOnFinished();
}
/// <summary>
/// Returns the PowerShell instance for the specified id.
/// </summary>
/// <param name="instanceId">Instance id of PowerShell.</param>
/// <returns>PowerShell instance.</returns>
internal override PowerShell GetPowerShell(Guid instanceId)
{
PowerShell powershell = null;
_powershells.TryGetValue(instanceId, out powershell);
return powershell;
}
#endregion Protected Methods
#region Event Handlers
/// <summary>
/// Used to unregister URI Redirection handler.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs e)
{
RemoteRunspace remoteRS = sender as RemoteRunspace;
// remote runspace must be connected (or connection failed)
// we dont need URI redirection any more..so clear it
if (remoteRS != null)
{
if (e.RunspaceStateInfo.State != RunspaceState.Opening)
{
remoteRS.URIRedirectionReported -= HandleURIDirectionReported;
if (e.RunspaceStateInfo.State != RunspaceState.Opened)
{
remoteRS.StateChanged -= HandleRunspaceStateChanged;
}
}
}
}
#endregion
#region Internal Methods
/// <summary>
/// Submits the operations created in the constructor for invocation.
/// </summary>
internal void StartOperations(List<IThrottleOperation> operations)
{
// submit operations to the throttle manager
_throttleManager.SubmitOperations(operations);
_throttleManager.EndSubmitOperations();
}
/// <summary>
/// Determines if the job is in a terminal state.
/// </summary>
/// <returns>True, if job in terminal state
/// false otherwise.</returns>
internal bool IsTerminalState()
{
return (IsFinishedState(this.JobStateInfo.State) ||
this.JobStateInfo.State == JobState.Disconnected);
}
/// <summary>
/// Returns a collection of all powershells for this job.
/// </summary>
/// <returns>Collection of PowerShell objects.</returns>
internal Collection<PowerShell> GetPowerShells()
{
Collection<PowerShell> powershellsToReturn = new Collection<PowerShell>();
foreach (PowerShell ps in _powershells.Values)
{
powershellsToReturn.Add(ps);
}
return powershellsToReturn;
}
/// <summary>
/// Returns a disconnected remoting job object that contains all
/// remote pipelines/runspaces that are in the Disconnected state.
/// </summary>
/// <returns></returns>
internal PSRemotingJob CreateDisconnectedRemotingJob()
{
List<IThrottleOperation> disconnectedJobHelpers = new List<IThrottleOperation>();
foreach (var helper in _helpers)
{
if (helper.Pipeline.PipelineStateInfo.State == PipelineState.Disconnected)
{
// Remove data callbacks from the old helper.
RemoveAggreateCallbacksFromHelper(helper);
// Create new helper used to create the new Disconnected PSRemoting job.
disconnectedJobHelpers.Add(new DisconnectedJobOperation(helper.Pipeline));
}
}
if (disconnectedJobHelpers.Count == 0)
{
return null;
}
return new PSRemotingJob(disconnectedJobHelpers, 0, Name, true);
}
#endregion Internal Methods
}
#region OutputProcessingState class
internal class OutputProcessingStateEventArgs : EventArgs
{
internal bool ProcessingOutput { get; }
internal OutputProcessingStateEventArgs(bool processingOutput)
{
ProcessingOutput = processingOutput;
}
}
#nullable enable
internal interface IOutputProcessingState
{
event EventHandler<OutputProcessingStateEventArgs>? OutputProcessingStateChanged;
}
#endregion
}