/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Management.Automation.Language; using System.Management.Automation.Remoting.Internal; using System.Management.Automation.Tracing; using System.Management.Automation.Remoting; using System.Management.Automation.Runspaces; using System.Management.Automation.Internal; using System.Management.Automation.Host; using System.Runtime.Serialization; using System.Threading; using System.Text; using Microsoft.PowerShell.Commands; using Dbg = System.Management.Automation.Diagnostics; #if CORECLR // Use stubs for SerializableAttribute, SystemException, SerializationInfo and ISerializable related types. using Microsoft.PowerShell.CoreClr.Stubs; #endif // Stops compiler from warning about unknown warnings #pragma warning disable 1634, 1691 namespace System.Management.Automation { /// /// Enumeration for job status values. Indicates the status /// of the result object /// public enum JobState { /// /// Execution of command in job not started /// NotStarted = 0, /// /// execution of command in progress /// Running = 1, /// /// execution of command completed in all /// computernames/runspaces /// Completed = 2, /// /// An error was encountered when trying to executed /// command in one or more computernames/runspaces /// Failed = 3, /// /// Command execution is cancelled (stopped) in one or more /// computernames/runspaces. /// Stopped = 4, /// /// Command execution is blocked (on user input host calls etc) /// Blocked = 5, /// /// The job has been suspended /// Suspended = 6, /// /// The job is a remote job and has been disconnected from the server. /// Disconnected = 7, /// /// Suspend is in progress /// Suspending = 8, /// /// Stop is in progress /// Stopping = 9, /// /// Script execution is halted in a debugger stop. /// AtBreakpoint = 10 } /// /// Defines exception which is thrown when state of the PSJob is different /// from the expected state. /// [Serializable] public class InvalidJobStateException : SystemException { /// /// Creates a new instance of InvalidPSJobStateException class. /// public InvalidJobStateException() : base ( PSRemotingErrorInvariants.FormatResourceString ( RemotingErrorIdStrings.InvalidJobStateGeneral ) ) { } /// /// Creates a new instance of InvalidPSJobStateException class. /// /// /// The error message that explains the reason for the exception. /// public InvalidJobStateException(string message) : base(message) { } /// /// Creates a new instance of InvalidPSJobStateException class. /// /// /// The error message that explains the reason for the exception. /// /// /// The exception that is the cause of the current exception. /// public InvalidJobStateException(string message, Exception innerException) : base(message, innerException) { } /// /// Creates a new instance of InvalidJobStateException class. /// /// /// The Job State at the time of the error. /// /// /// An additional message that gives more information about the error. Used /// for context after a generalized error message. /// public InvalidJobStateException(JobState currentState, string actionMessage) : base ( PSRemotingErrorInvariants.FormatResourceString ( RemotingErrorIdStrings.InvalidJobStateSpecific, currentState, actionMessage ) ) { _currState = currentState; } /// /// Initializes a new instance of the InvalidPSJobStateException and defines value of /// CurrentState. /// /// Current state of powershell 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] /// /// Initializes a new instance of the InvalidPSJobStateException /// class with serialized data. /// /// /// The that holds the serialized object /// data about the exception being thrown. /// /// /// The that contains contextual information /// about the source or destination. /// protected InvalidJobStateException(SerializationInfo info, StreamingContext context) : base(info, context) { } #endregion /// /// Gets CurrentState of the Job /// public JobState CurrentState { get { return _currState; } } /// /// State of job when exception was thrown. /// [NonSerialized] private JobState _currState = 0; } /// /// Type which has information about JobState and Exception /// ,if any, associated with JobState /// public sealed class JobStateInfo { #region constructors /// /// Constructor for state changes not resulting from an error. /// /// Execution state public JobStateInfo(JobState state) : this(state, null) { } /// /// Constructor for state changes with an optional error. /// /// The new state. /// A non-null exception if the state change was /// caused by an error,otherwise; null. /// public JobStateInfo(JobState state, Exception reason) { State = state; Reason = reason; } /// /// Copy constructor to support cloning /// /// source information /// /// ArgumentNullException when is null. /// internal JobStateInfo(JobStateInfo jobStateInfo) { State = jobStateInfo.State; Reason = jobStateInfo.Reason; } #endregion constructors #region public_properties /// /// The state of the job. /// /// /// This value indicates the state of the job . /// public JobState State { get; } /// /// The reason for the state change, if caused by an error. /// /// /// The value of this property is non-null if the state /// changed due to an error. Otherwise, the value of this /// property is null. /// public Exception Reason { get; } #endregion public_properties /// /// override for ToString() /// /// public override string ToString() { return State.ToString(); } /// /// Clones this object /// /// Cloned object internal JobStateInfo Clone() { return new JobStateInfo(this); } #region private_fields #endregion private_fields } /// /// Event arguments passed to JobStateEvent handlers /// event. /// public sealed class JobStateEventArgs : EventArgs { #region constructors /// /// Constructor of JobStateEventArgs /// /// The current state of the job. public JobStateEventArgs(JobStateInfo jobStateInfo) : this(jobStateInfo, null) { } /// /// Constructor of JobStateEventArgs /// /// The current state of the job. /// The previous state of the job. public JobStateEventArgs(JobStateInfo jobStateInfo, JobStateInfo previousJobStateInfo) { if (jobStateInfo == null) { throw PSTraceSource.NewArgumentNullException("jobStateInfo"); } JobStateInfo = jobStateInfo; PreviousJobStateInfo = previousJobStateInfo; } #endregion constructors #region public_properties /// /// Info about the current state of the job. /// public JobStateInfo JobStateInfo { get; } /// /// Info about the previous state of the job. /// public JobStateInfo PreviousJobStateInfo { get; } #endregion public_properties } /// /// 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. /// public sealed class JobIdentifier { internal JobIdentifier(int id, Guid instanceId) { if (id <= 0) PSTraceSource.NewArgumentException("id", RemotingErrorIdStrings.JobSessionIdLessThanOne, id); Id = id; InstanceId = instanceId; } internal int Id { get; private set; } internal Guid InstanceId { get; private set; } } /// /// Interface to expose a job debugger. /// public interface IJobDebugger { /// /// Job Debugger /// Debugger Debugger { get; } /// /// True if job is running asynchronously. /// bool IsAsync { get; set; } } /// /// Represents a command running in background. A job object can internally /// contain many child job objects. /// public abstract class Job : IDisposable { #region Constructor /// /// Default constructor /// protected Job() { Id = System.Threading.Interlocked.Increment(ref s_jobIdSeed); } /// /// Creates an instance of this class /// /// Command invoked by this job object protected Job(string command) : this() { Command = command; _name = AutoGenerateJobName(); } /// /// Creates an instance of this class /// /// Command invoked by this job object /// Friendly name for the job object protected Job(string command, string name) : this(command) { if (!String.IsNullOrEmpty(name)) { _name = name; } } /// /// Creates an instance of this class /// /// Command invoked by this job object /// Friendly name for the job object /// Child jobs of this job object protected Job(string command, string name, IList childJobs) : this(command, name) { _childJobs = childJobs; } /// /// Creates an instance of this class /// /// Command invoked by this job object /// Friendly name for the job object /// Id and InstanceId pair to be used for this job object /// 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. protected Job(string command, string name, JobIdentifier token) { if (token == null) throw PSTraceSource.NewArgumentNullException("token", RemotingErrorIdStrings.JobIdentifierNull); if (token.Id > s_jobIdSeed) { throw PSTraceSource.NewArgumentException("token", RemotingErrorIdStrings.JobIdNotYetAssigned, token.Id); } Command = command; Id = token.Id; InstanceId = token.InstanceId; if (!String.IsNullOrEmpty(name)) { _name = name; } else { _name = AutoGenerateJobName(); } } /// /// Creates an instance of this class /// /// Command invoked by this job object /// Friendly name for the job object /// InstanceId to be used for this job object /// The InstanceId may need to be set to maintain job identity across /// instances of the process. 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.Substring(scriptExtent.StartScriptPosition.ColumnNumber - 1).Trim(); } return invocationInfo.InvocationName; } #endregion Constructor #region Private Members private ManualResetEvent _finished = new ManualResetEvent(false); private string _name; private IList _childJobs; internal readonly object syncObject = new object(); // object used for synchronization //ISSUE: Should Result be public property private PSDataCollection _results = new PSDataCollection(); private bool _resultsOwner = true; private PSDataCollection _error = new PSDataCollection(); private bool _errorOwner = true; private PSDataCollection _progress = new PSDataCollection(); private bool _progressOwner = true; private PSDataCollection _verbose = new PSDataCollection(); private bool _verboseOwner = true; private PSDataCollection _warning = new PSDataCollection(); private bool _warningOwner = true; private PSDataCollection _debug = new PSDataCollection(); private bool _debugOwner = true; private PSDataCollection _information = new PSDataCollection(); private bool _informationOwner = true; private PSDataCollection _output = new PSDataCollection(); private bool _outputOwner = true; /// /// Static variable which is incremented to generate id /// private static int s_jobIdSeed = 0; private string _jobTypeName = String.Empty; #endregion Private Members #region Job Properties /// /// Command Invoked by this Job /// public string Command { get; } /// /// Status of the command execution. /// public JobStateInfo JobStateInfo { get; private set; } = new JobStateInfo(JobState.NotStarted); /// /// Wait Handle which is signaled when job is finished. /// This is set when state of the job is set to Completed, /// Stopped or Failied. /// 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); } } } } /// /// unique identifier for this job /// public Guid InstanceId { get; } = Guid.NewGuid(); /// /// Short identifier for this result which will be /// recycled and used within a process /// public Int32 Id { get; } /// /// Name for identifying this job object /// public String Name { get { return _name; } set { AssertNotDisposed(); _name = value; } } /// /// List of child jobs contained within this job /// public IList ChildJobs { get { if (_childJobs == null) { lock (syncObject) { if (_childJobs == null) { _childJobs = new List(); } } } return _childJobs; } } /// /// Success status of the command execution. /// public abstract String StatusMessage { get; } /// /// Indicates that more data is available in this /// result object for reading. /// public abstract bool HasMoreData { get; } /// /// Time job was started. /// public DateTime? PSBeginTime { get; protected set; } = null; /// /// Time job stopped. /// public DateTime? PSEndTime { get; protected set; } = null; /// /// Job type name. /// public string PSJobTypeName { get { return _jobTypeName; } protected internal set { _jobTypeName = value ?? this.GetType().ToString(); } } #region results /// /// 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 /// internal PSDataCollection Results { get { return _results; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Results"); } lock (syncObject) { AssertChangesAreAccepted(); _resultsOwner = false; _results = value; } } } /// /// Indicates if a particular Job type uses the /// internal results collection /// internal bool UsesResultsCollection { get; set; } /// /// Suppresses forwarding of job output into a cmdlet (like Receive-Job). /// This flag modifies functionality of method, so that it doesnt add output-processing to collection. /// 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)); } } /// /// Allows propagating of terminating exceptions from remote "throw" statement /// (normally / by default all remote errors are transformed into non-terminating errors /// 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) { RuntimeException runtimeException = errorRecord.Exception as RuntimeException; if (runtimeException == null) return null; RemoteException remoteException = runtimeException as RemoteException; if (remoteException == null) 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( delegate (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 _parentActivityId; internal void SetParentActivityIdGetter(Func parentActivityIdGetter) { Dbg.Assert(parentActivityIdGetter != null, "Caller should verify parentActivityIdGetter != null"); _parentActivityId = new Lazy(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( delegate (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 invokeCmdletMethod) { object resultsLock = new object(); CmdletMethodInvoker methodInvoker = new CmdletMethodInvoker { Action = delegate (Cmdlet cmdlet) { invokeCmdletMethod(cmdlet); return null; }, Finished = null, SyncObject = resultsLock }; Results.Add(new PSStreamObject(PSStreamObjectType.BlockingError, methodInvoker)); } private T InvokeCmdletMethodAndWaitForResults(Func 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 stateChangedEventHandler = delegate (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 methodInvoker = new CmdletMethodInvoker { 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); } } /// /// This method is introduce for delaying the loading of streams /// for a particular job. /// protected virtual void DoLoadJobStreams() { } /// /// Unloads job streams information. Enables jobs to /// clear stream information from memory /// protected virtual void DoUnloadJobStreams() { } /// /// Load the required job streams /// 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; /// /// Unload the required job streams /// 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); } } } /// /// Gets or sets the output buffer. Output of job is written /// into this buffer. /// /// /// Cannot set to a null value. /// /// /// Object is disposed. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public PSDataCollection Output { get { LoadJobStreams(); // for delayed loading return _output; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Output"); } lock (syncObject) { AssertChangesAreAccepted(); _outputOwner = false; _output = value; _jobStreamsLoaded = true; } } } /// /// Gets or sets the error buffer. Errors of job are written /// into this buffer. /// /// /// Cannot set to a null value. /// /// /// Object is disposed. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public PSDataCollection Error { get { LoadJobStreams(); // for delayed loading return _error; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Error"); } lock (syncObject) { AssertChangesAreAccepted(); _errorOwner = false; _error = value; _jobStreamsLoaded = true; } } } /// /// Gets or sets the progress buffer. Progress of job is written /// into this buffer. /// /// /// Cannot set to a null value. /// /// /// Object is disposed. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public PSDataCollection Progress { get { LoadJobStreams(); // for delayed loading return _progress; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Progress"); } lock (syncObject) { AssertChangesAreAccepted(); _progressOwner = false; _progress = value; _jobStreamsLoaded = true; } } } /// /// Gets or sets the verbose buffer. Verbose output of job is written to /// this stream. /// /// /// Object is disposed. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public PSDataCollection Verbose { get { LoadJobStreams(); // for delayed loading return _verbose; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Verbose"); } lock (syncObject) { AssertChangesAreAccepted(); _verboseOwner = false; _verbose = value; _jobStreamsLoaded = true; } } } /// /// Gets or sets the debug buffer. Debug output of Job is written /// to this buffer. /// /// Cannot set to a null value. /// /// /// /// Object is disposed. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public PSDataCollection Debug { get { LoadJobStreams(); // for delayed loading return _debug; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Debug"); } lock (syncObject) { AssertChangesAreAccepted(); _debugOwner = false; _debug = value; } } } /// /// Gets or sets the warning buffer. Warnings of job are written to /// this buffer /// /// /// Cannot set to a null value. /// /// /// Object is disposed. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public PSDataCollection Warning { get { LoadJobStreams(); // for delayed loading return _warning; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Warning"); } lock (syncObject) { AssertChangesAreAccepted(); _warningOwner = false; _warning = value; _jobStreamsLoaded = true; } } } /// /// Gets or sets the information buffer. Information records of job are written to /// this buffer /// /// /// Cannot set to a null value. /// /// /// Object is disposed. /// [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public PSDataCollection Information { get { LoadJobStreams(); // for delayed loading return _information; } set { if (null == value) { throw PSTraceSource.NewArgumentNullException("Information"); } lock (syncObject) { AssertChangesAreAccepted(); _informationOwner = false; _information = value; _jobStreamsLoaded = true; } } } /// /// Indicates a location where this job is running /// public abstract String Location { get; } #endregion results #region Connect/Disconnect /// /// Returns boolean indicating whether the underlying /// transport for the job (or child jobs) supports /// connect/disconnect semantics. /// internal virtual bool CanDisconnect { get { return false; } } /// /// Returns runspaces associated with the Job, including /// child jobs. /// /// IEnumerable of RemoteRunspaces internal virtual IEnumerable GetRunspaces() { return null; } #endregion #endregion Job Properties #region Job State and State Change Event /// /// Event raised when state of the job changes /// [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public event EventHandler StateChanged; /// /// Sets Job State. /// /// /// New State of Job /// protected void SetJobState(JobState state) { AssertNotDisposed(); SetJobState(state, null); } /// /// Sets Job State. /// /// /// New State of Job /// /// /// Reason associated with the state. /// 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 e) { CommandProcessorBase.CheckForSevereException(e); } } } #pragma warning disable 56500 //Exception rasied 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); CommandProcessorBase.CheckForSevereException(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 /// /// Stop this job object. If job contains child job, this should /// stop child job objects also. /// public abstract void StopJob(); #endregion Job Public Methods #region Private/Internal Methods /// /// Returns the items in results collection /// after clearing up all the internal /// structures /// /// collection of stream objects internal Collection ReadAll() { Output.Clear(); Error.Clear(); Debug.Clear(); Warning.Clear(); Verbose.Clear(); Progress.Clear(); return Results.ReadAll(); } /// /// Helper function to check if job is finished /// /// /// 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); } } /// /// 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. /// /// /// Powershell instance cannot be changed in its /// current state. /// /// /// Object is disposed. /// private void AssertChangesAreAccepted() { AssertNotDisposed(); lock (syncObject) { if (JobStateInfo.State == JobState.Running) { throw new InvalidJobStateException(JobState.Running); } } } /// /// Automatically generate a job name if the user /// does not supply one /// /// auto generated job name /// Since the user can script/program against the /// job name, the auto generated name will not be /// localizable protected String AutoGenerateJobName() { return "Job" + Id.ToString(System.Globalization.NumberFormatInfo.InvariantInfo); } /// /// Checks if the current powershell instance is disposed. /// If disposed, throws ObjectDisposedException. /// /// /// Object is disposed. /// internal void AssertNotDisposed() { if (_isDisposed) { throw PSTraceSource.NewObjectDisposedException("PSJob"); } } /// /// A helper method to close all the streams. /// internal void CloseAllStreams() { if (_resultsOwner) _results.Complete(); if (_outputOwner) _output.Complete(); if (_errorOwner) _error.Complete(); if (_progressOwner) _progress.Complete(); if (_verboseOwner) _verbose.Complete(); if (_warningOwner) _warning.Complete(); if (_debugOwner) _debug.Complete(); if (_informationOwner) _information.Complete(); } /// /// Gets the job for the specified location /// /// location to filter on /// collection of jobs internal List GetJobsForLocation(String location) { List returnJobList = new List(); foreach (Job job in ChildJobs) { if (String.Equals(job.Location, location, StringComparison.OrdinalIgnoreCase)) { returnJobList.Add(job); } } return returnJobList; } // GetJobsForLocation #endregion Private/Internal Methods #region IDisposable Members /// /// Dispose all managed resources. This will suppress finalizer on the object from getting called by /// calling System.GC.SuppressFinalize(this). /// 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); } /// /// Release all the resources. /// /// /// if true, release all the managed objects. /// protected virtual void Dispose(bool disposing) { if (disposing) { if (!_isDisposed) { CloseAllStreams(); // release the WaitHandle lock (syncObject) { if (null != _finished) { _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 OutputProcessingStateChanged; private bool _processingOutput; /// /// MonitorOutputProcessing /// 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(this, e); } #endregion } //Job /// /// Top level job object for remoting. This contains multiple child job /// objects. Each child job object invokes command on one remote machine. /// /// /// Not removing the prefix "PS" as this signifies powershell specific remoting job /// internal class PSRemotingJob : Job { #region Internal Constructors /// /// Internal constructor for initializing PSRemotingJob using /// computer names /// /// names of computers for /// which the job object is being created /// list of helper objects /// corresponding to the computer names /// /// remote command corresponding to this /// job object /// a friendly name for the job object /// [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal PSRemotingJob(String[] computerNames, List computerNameHelpers, String remoteCommand, string name) : this(computerNames, computerNameHelpers, remoteCommand, 0, name) { } /// /// Internal constructor for initializing job using /// PSSession objects /// /// array of runspace info /// objects on which the remote command is executed /// List of helper objects for the /// runspaces /// remote command corresponding to this /// job object /// a friendly name for the job object /// internal PSRemotingJob(PSSession[] remoteRunspaceInfos, List runspaceHelpers, String remoteCommand, string name) : this(remoteRunspaceInfos, runspaceHelpers, remoteCommand, 0, name) { } /// /// Internal constructor for initializing PSRemotingJob using /// computer names /// /// names of computers for /// which the result object is being created /// list of helper objects /// corresponding to the computer names /// /// remote command corresponding to this /// result object /// Throttle limit to use /// a friendly name for the job object internal PSRemotingJob(String[] computerNames, List 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 += new EventHandler(HandleChildJobStateChanged); childJob.JobUnblocked += new EventHandler(HandleJobUnblocked); //Add this job to list of childjobs ChildJobs.Add(childJob); } CommonInit(throttleLimit, computerNameHelpers); } /// /// Internal constructor for initializing job using /// PSSession objects /// /// array of runspace info /// objects on which the remote command is executed /// List of helper objects for the /// runspaces /// remote command corresponding to this /// result object /// throttle limit to use /// internal PSRemotingJob(PSSession[] remoteRunspaceInfos, List runspaceHelpers, String remoteCommand, int throttleLimit, string name) : base(remoteCommand, name) { // Create chiild 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 += new EventHandler(HandleChildJobStateChanged); job.JobUnblocked += new EventHandler(HandleJobUnblocked); //Add the child job to list of child jobs ChildJobs.Add(job); } CommonInit(throttleLimit, runspaceHelpers); } /// /// 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. /// /// List of DisconnectedJobOperation objects with disconnected pipelines. /// Throttle limit value. /// Job name. /// Aggregate results. internal PSRemotingJob(List 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 += new EventHandler(HandleChildJobStateChanged); job.JobUnblocked += new EventHandler(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(); } /// /// Default constructor /// protected PSRemotingJob() { } /// /// Initialization common to both constructors /// private void CommonInit(int throttleLimit, List 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 /// /// Get entity result for the specified computer /// /// computername for which entity /// result is required /// entity result internal List GetJobsForComputer(String computerName) { List returnJobList = new List(); foreach (Job j in ChildJobs) { PSRemotingChildJob child = j as PSRemotingChildJob; if (j == null) continue; if (String.Equals(child.Runspace.ConnectionInfo.ComputerName, computerName, StringComparison.OrdinalIgnoreCase)) { returnJobList.Add(child); } } return returnJobList; } // GetResultForComputer /// /// Get entity result for the specified runspace /// /// runspace for which entity /// result is required /// entity result internal List GetJobsForRunspace(PSSession runspace) { List returnJobList = new List(); foreach (Job j in ChildJobs) { PSRemotingChildJob child = j as PSRemotingChildJob; if (j == null) continue; if (child.Runspace.InstanceId.Equals(runspace.InstanceId)) { returnJobList.Add(child); } } return returnJobList; } // GetResultForRunspace /// /// Get entity result for the specified helper object /// /// helper for which entity /// result is required /// entity result internal List GetJobsForOperation(IThrottleOperation operation) { List returnJobList = new List(); ExecutionCmdletHelper helper = operation as ExecutionCmdletHelper; foreach (Job j in ChildJobs) { PSRemotingChildJob child = j as PSRemotingChildJob; if (j == null) continue; if (child.Helper.Equals(helper)) { returnJobList.Add(child); } } return returnJobList; } // GetResultForHelper #endregion internal methods #region Connection Support /// /// Connect all child jobs if they are in a disconnected state. /// internal void ConnectJobs() { // Create connect operation objects for each child job object to connect. List connectJobOperations = new List(); 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); } /// /// Connect a single child job associated with the provided runspace. /// /// Runspace instance id for child job. internal void ConnectJob(Guid runspaceInstanceId) { List connectJobOperations = new List(); 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 void SubmitAndWaitForConnect(List connectJobOperations) { using (ThrottleManager connectThrottleManager = new ThrottleManager()) { using (ManualResetEvent connectResult = new ManualResetEvent(false)) { EventHandler throttleCompleteEventHandler = delegate (object sender, EventArgs eventArgs) { connectResult.Set(); }; connectThrottleManager.ThrottleComplete += throttleCompleteEventHandler; try { connectThrottleManager.ThrottleLimit = 0; connectThrottleManager.SubmitOperations(connectJobOperations); connectThrottleManager.EndSubmitOperations(); connectResult.WaitOne(); } finally { connectThrottleManager.ThrottleComplete -= throttleCompleteEventHandler; } } } } /// /// Simple throttle operation class for connecting jobs. /// private class ConnectJobOperation : IThrottleOperation { private 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 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; } } /// /// Finds the disconnected child job associated with this runspace and returns /// the PowerShell object that is remotely executing the command. /// /// Runspace instance Id. /// Associated PowerShell object. internal PowerShell GetAssociatedPowerShellObject(Guid runspaceInstanceId) { PowerShell ps = null; PSRemotingChildJob childJob = FindDisconnectedChildJob(runspaceInstanceId); if (childJob != null) { ps = childJob.GetPowerShell(); } return ps; } /// /// Helper method to find a disconnected child job associated with /// a given runspace. /// /// Runspace Id. /// PSRemotingChildJob object. 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; } /// /// 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. /// 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; /// /// indicates if more data is available /// /// /// This has more data if any of the child jobs have more data. /// 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; /// /// Stop Job /// 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; /// /// Message indicating status of the job /// public override string StatusMessage { get { return _statusMessage; } } /// /// 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. /// internal bool HideComputerName { get { return _hideComputerName; } set { _hideComputerName = value; foreach (Job job in this.ChildJobs) { PSRemotingChildJob rJob = job as PSRemotingChildJob; if (null != rJob) { rJob.HideComputerName = value; } } } } private bool _hideComputerName = true; //ISSUE: Implement StatusMessage /// /// Checks the status of remote command execution /// [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; // } // } // for (int ... // setFinished = true; // } // if (finishedCount ... // } // lock... } // SetStatusMessage #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; /// /// Handles the StateChanged event from each of the child job objects /// /// /// 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 == true) { SetJobState(JobState.Stopped); } else { SetJobState(JobState.Completed); } } } /// /// Updates the parent job state based on state of all child jobs. /// /// New child job state. /// Previous child job state. 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 /// /// Release all the resources. /// /// /// if true, release all the managed objects. /// 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(); } /// /// Computers on which this job is running /// public override string Location { get { return ConstructLocation(); } } /// /// Returns boolean indicating whether the underlying /// transport for the job (or child jobs) supports /// connect/disconnect semantics. /// 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; } } /// /// Returns runspaces associated with the Job, including /// child jobs. /// /// IEnumerable of RemoteRunspaces internal override IEnumerable GetRunspaces() { List runspaces = new List(); foreach (PSRemotingChildJob job in ChildJobs) { runspaces.Add(job.Runspace as RemoteRunspace); } return runspaces; } /// /// 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 /// /// sender of this event, unused /// event arguments, should be empty in this /// case 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 ThrottleManager _throttleManager = new ThrottleManager(); private readonly object _syncObject = new object(); // sync object #endregion Private Members } #region DisconnectedJobOperation class /// /// Simple throttle operation class for PSRemoting jobs created in the /// disconnected state. /// internal class DisconnectedJobOperation : ExecutionCmdletHelper { internal DisconnectedJobOperation(Pipeline pipeline) { this.pipeline = pipeline; this.pipeline.StateChanged += new EventHandler(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 OpertionComplete from here, // else ThrottleManager will have SendStopComplete(); } } internal override event EventHandler 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 /// /// Class for RemotingChildJob object. This job object invokes command /// on one remote machine. /// /// /// TODO: I am not sure whether to change this internal to just RemotingChildJob. /// /// /// Not removing the prefix "PS" as this signifies powershell specific remoting job /// internal class PSRemotingChildJob : Job, IJobDebugger { #region Internal Constructor /// /// Creates an instance of PSRemotingChildJob /// /// command invoked by this job object /// /// /// 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 ((null != remoteRS) && (remoteRS.RunspaceStateInfo.State == RunspaceState.BeforeOpen)) { remoteRS.URIRedirectionReported += HandleURIDirectionReported; } AggregateResultsFromHelper(helper); Runspace.AvailabilityChanged += HandleRunspaceAvailabilityChanged; RegisterThrottleComplete(throttleManager); } /// /// 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. /// /// ExecutionCmdletHelper object containing runspace and pipeline objects. /// ThrottleManger object. /// Aggregate results. 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 += new EventHandler(HandlePipelineStateChanged); _remotePipeline.Output.DataReady += new EventHandler(HandleOutputReady); _remotePipeline.Error.DataReady += new EventHandler(HandleErrorReady); } Runspace.AvailabilityChanged += HandleRunspaceAvailabilityChanged; IThrottleOperation operation = helper as IThrottleOperation; operation.OperationComplete += new EventHandler(HandleOperationComplete); SetJobState(JobState.Disconnected, null); } /// /// Default constructor /// protected PSRemotingChildJob() { } #endregion Internal Constructor #region Internal Methods /// /// Connects the remote pipeline that this job represents. /// internal void ConnectAsync() { if (JobStateInfo.State != JobState.Disconnected) { throw new InvalidJobStateException(JobStateInfo.State); } _remotePipeline.ConnectAsync(); } /// /// Removes job data aggregation callbacks. Used for jobs /// stopped in debugger so that debugger can access data. /// internal void RemoveJobAggregation() { RemoveAggreateCallbacksFromHelper(Helper); } #endregion #region stop //bool isStopCalled = false; /// /// Stops the job /// 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 /// /// Status Message associated with the Job /// public override string StatusMessage { get { //ISSUE implement this. return ""; } } /// /// Indicates if there is more data available in /// this Job /// public override bool HasMoreData { get { return (Results.IsOpen || Results.Count > 0); } } /// /// Returns the computer on which this command is /// running /// public override string Location { get { return (Runspace != null) ? Runspace.ConnectionInfo.ComputerName : string.Empty; } } /// /// /// public Runspace Runspace { get; } /// /// helper associated with this entity /// internal ExecutionCmdletHelper Helper { get; } = null; /// /// 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. /// internal bool HideComputerName { get { return _hideComputerName; } set { _hideComputerName = value; foreach (Job job in this.ChildJobs) { PSRemotingChildJob rJob = job as PSRemotingChildJob; if (null != rJob) { rJob.HideComputerName = value; } } } } private bool _hideComputerName = true; /// /// Property that indicates this disconnected child job was /// previously in the Blocked state. /// internal bool DisconnectedAndBlocked { get; private set; } = false; /// /// Returns boolean indicating whether the underlying /// transport for the job (or child jobs) supports /// connect/disconnect semantics. /// internal override bool CanDisconnect { get { RemoteRunspace remoteRS = Runspace as RemoteRunspace; return (remoteRS != null) ? remoteRS.CanDisconnect : false; } } #endregion Properties #region IJobDebugger /// /// Job Debugger /// 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; } } /// /// True if job is synchronous and can be debugged. /// public bool IsAsync { get { return _isAsync; } set { _isAsync = true; } } #endregion #region Private Methods /// /// Handler which will handle output ready events of the /// pipeline. The output objects are queued on to the /// internal stream /// /// the pipeline reader which raised /// this event /// information describing the ready event private void HandleOutputReady(object sender, EventArgs eventArgs) { PSDataCollectionPipelineReader reader = sender as PSDataCollectionPipelineReader; Collection 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); } } //HandleOutputReady /// /// Handler which will handle error ready events of the /// pipeline. The error records are queued on to the /// internal stream /// /// the pipeline reader which raised /// this event /// information describing the ready event private void HandleErrorReady(object sender, EventArgs eventArgs) { PSDataCollectionPipelineReader reader = sender as PSDataCollectionPipelineReader; Collection 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); } } } //HandleErrorReady /// /// 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. /// /// /// protected void HandleURIDirectionReported(object sender, RemoteDataEventArgs eventArgs) { string message = StringUtil.Format(RemotingErrorIdStrings.URIRedirectWarningToHost, eventArgs.Data.OriginalString); this.WriteWarning(message); } /// /// Used to detect an Invoke-Command running command breakpoint hit. /// In this case disconnect the runspace so that a debugger can be /// attached later by the user. /// /// /// protected void HandleRunspaceAvailabilityChangedForInvoke(object sender, RunspaceAvailabilityEventArgs e) { RemoteRunspace remoteRunspace = sender as RemoteRunspace; if (remoteRunspace != null && e.RunspaceAvailability == RunspaceAvailability.RemoteDebug) { remoteRunspace.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; try { remoteRunspace.DisconnectAsync(); } catch (PSNotImplementedException) { } catch (InvalidRunspacePoolStateException) { } catch (InvalidRunspaceStateException) { } catch (PSInvalidOperationException) { } } } /// /// Handle method executor stream events. /// /// The sender. /// The event args. private void HandleHostCalls(object sender, EventArgs eventArgs) { ObjectStream hostCallsStream = sender as ObjectStream; if (hostCallsStream != null) { Collection 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); } } } }// if (hostCallsStream... } // HandleHostCalls /// /// Handle changes in pipeline states /// /// /// protected virtual void HandlePipelineStateChanged(object sender, PipelineStateEventArgs e) { if ((null != Runspace) && (e.PipelineStateInfo.State != PipelineState.Running)) { // since we got state changed event..we dont need to listen on // URI redirections anymore ((RemoteRunspace)Runspace).URIRedirectionReported -= HandleURIDirectionReported; // We monitor runspace RemoteDebug availability only while // this pipeline is running. ((RemoteRunspace)Runspace).AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; } 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 } /// /// Handle a throttle complete event /// /// sender of this event /// not used in this method 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(); } // HandleThrottleComplete /// /// Handle the operation complete event /// /// sender of this event /// operation complete event args 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; /// /// This method marks the completion state for Job. Also if job failed, it processes the /// reason of failure. /// protected virtual void DoFinish() { if (_doFinishCalled == true) return; lock (SyncObject) { if (_doFinishCalled == true) return; _doFinishCalled = true; } DeterminedAndSetJobState(Helper); DoCleanupOnFinished(); } /// /// This is the pretty formated error record associated with the reason of failure. /// private ErrorRecord _failureErrorRecord; /// /// 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. /// internal ErrorRecord FailureErrorRecord { get { return _failureErrorRecord; } } /// /// Process the exceptions to decide reason for job failure /// /// /// /// 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) { 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); } // if (runspace... else if (pipeline.PipelineStateInfo.State == PipelineState.Failed) { 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; } 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); } // if (exception != null... } // if (pipeline... } /// /// Release all the resources. /// /// /// if true, release all the managed objects. /// 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; /// /// Cleanup after state changes to finished /// protected virtual void DoCleanupOnFinished() { bool doCleanup = false; if (_cleanupDone == false) { lock (SyncObject) { if (_cleanupDone == false) { _cleanupDone = true; doCleanup = true; } } } if (!doCleanup) return; StopAggregateResultsFromHelper(Helper); Runspace.AvailabilityChanged -= HandleRunspaceAvailabilityChanged; IThrottleOperation operation = Helper as IThrottleOperation; operation.OperationComplete -= new EventHandler(HandleOperationComplete); UnregisterThrottleComplete(_throttleManager); _throttleManager = null; } /// /// Aggregates results from the pipeline associated /// with the specified helper /// /// helper whose pipeline results /// need to be aggregated protected void AggregateResultsFromHelper(ExecutionCmdletHelper helper) { //Get the pipeline associated with this helper and register for appropriate events Pipeline pipeline = helper.Pipeline; pipeline.Output.DataReady += new EventHandler(HandleOutputReady); pipeline.Error.DataReady += new EventHandler(HandleErrorReady); pipeline.StateChanged += new EventHandler(HandlePipelineStateChanged); // Register handler for method executor object stream. Dbg.Assert(pipeline is RemotePipeline, "pipeline is RemotePipeline"); RemotePipeline remotePipeline = pipeline as RemotePipeline; remotePipeline.MethodExecutorStream.DataReady += new EventHandler(HandleHostCalls); remotePipeline.PowerShell.Streams.Progress.DataAdded += new EventHandler(HandleProgressAdded); remotePipeline.PowerShell.Streams.Warning.DataAdded += new EventHandler(HandleWarningAdded); remotePipeline.PowerShell.Streams.Verbose.DataAdded += new EventHandler(HandleVerboseAdded); remotePipeline.PowerShell.Streams.Debug.DataAdded += new EventHandler(HandleDebugAdded); remotePipeline.PowerShell.Streams.Information.DataAdded += new EventHandler(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 += new EventHandler(HandleOperationComplete); } /// /// If the pipeline is not null, returns the pipeline's PowerShell /// If it is null, then returns the PowerShell with the specified /// instance Id. /// /// remote pipeline /// instance as described in event args /// PowerShell instance private PowerShell GetPipelinePowerShell(RemotePipeline pipeline, Guid instanceId) { if (pipeline != null) { return pipeline.PowerShell; } return GetPowerShell(instanceId); } /// /// When a debug message is raised in the underlying PowerShell /// add it to the jobs debug stream /// /// unused /// arguments describing this event 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]); } } /// /// When a verbose message is raised in the underlying PowerShell /// add it to the jobs verbose stream /// /// unused /// arguments describing this event 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]); } } /// /// When a warning message is raised in the underlying PowerShell /// add it to the jobs warning stream /// /// unused /// arguments describing this event 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)); } } /// /// When a progress message is raised in the underlying PowerShell /// add it to the jobs progress tream /// /// unused /// arguments describing this event 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]); } } /// /// When a Information message is raised in the underlying PowerShell /// add it to the jobs Information stream /// /// unused /// arguments describing this event 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)); } } /// /// Stops collecting results from the pipeline associated with /// the specified helper /// /// helper class whose pipeline results /// aggregation has to be stopped protected void StopAggregateResultsFromHelper(ExecutionCmdletHelper helper) { // Ensure the Runspace availability handler is removed on command completion. if (helper.PipelineRunspace != null) { helper.PipelineRunspace.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; } // Get the pipeline associated with this helper and register for appropriate events RemoveAggreateCallbacksFromHelper(helper); Pipeline pipeline = helper.Pipeline; pipeline.Dispose(); pipeline = null; } /// /// 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. /// /// helper class 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 -= new EventHandler(HandleHostCalls); if (remotePipeline.PowerShell != null) { remotePipeline.PowerShell.Streams.Progress.DataAdded -= new EventHandler(HandleProgressAdded); remotePipeline.PowerShell.Streams.Warning.DataAdded -= new EventHandler(HandleWarningAdded); remotePipeline.PowerShell.Streams.Verbose.DataAdded -= new EventHandler(HandleVerboseAdded); remotePipeline.PowerShell.Streams.Debug.DataAdded -= new EventHandler(HandleDebugAdded); remotePipeline.PowerShell.Streams.Information.DataAdded -= new EventHandler(HandleInformationAdded); remotePipeline.IsMethodExecutorStreamEnabled = false; } } /// /// register for throttle complete from the specified /// throttlemanager /// /// protected void RegisterThrottleComplete(ThrottleManager throttleManager) { throttleManager.ThrottleComplete += new EventHandler(HandleThrottleComplete); } /// /// unregister for throttle complete from the specified /// throttle manager /// /// protected void UnregisterThrottleComplete(ThrottleManager throttleManager) { throttleManager.ThrottleComplete -= new EventHandler(HandleThrottleComplete); } /// /// Determine the current state of the job based on the underlying /// pipeline state and set the state accordingly /// /// 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); } } } /// /// 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 /// 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); } /// /// Returns the PowerShell for the specified instance id /// /// instance id of powershell /// powershell instance internal virtual PowerShell GetPowerShell(Guid instanceId) { // this should be called only in the derived implementation throw PSTraceSource.NewInvalidOperationException(); } /// /// Returns the PowerShell object associated with this remote child job. /// /// PowerShell object. internal PowerShell GetPowerShell() { PowerShell ps = null; if (_remotePipeline != null) { ps = _remotePipeline.PowerShell; } return ps; } /// /// 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). /// /// Runspace /// RunspaceAvailabilityEventArgs 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); } } /// /// Event raised by this job to indicate to its parent that /// its now unblocked by the user /// internal event EventHandler JobUnblocked; #endregion Private Methods #region Private Members //helper associated with this job object private 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 } /// /// This is a debugger wrapper class used to allow debugging of /// remoting jobs that implement the IJobDebugger interface. /// internal sealed class RemotingJobDebugger : Debugger { #region Members private Debugger _wrappedDebugger; private Runspace _runspace; private string _jobName; #endregion #region Constructor private RemotingJobDebugger() { } /// /// Constructor /// /// Debugger to wrap. /// Remote runspace. /// Name of associated job. public RemotingJobDebugger( Debugger debugger, Runspace runspace, string jobName) { if (debugger == null) { throw new PSArgumentNullException("debugger"); } if (runspace == null) { throw new PSArgumentNullException("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 /// /// Evaluates provided command either as a debugger specific command /// or a PowerShell command. /// /// PowerShell command /// Output /// DebuggerCommandResults public override DebuggerCommandResults ProcessCommand(PSCommand command, PSDataCollection 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); } /// /// Sets the debugger resume action. /// /// DebuggerResumeAction public override void SetDebuggerAction(DebuggerResumeAction resumeAction) { _wrappedDebugger.SetDebuggerAction(resumeAction); } /// /// Stops a running command. /// public override void StopProcessCommand() { _wrappedDebugger.StopProcessCommand(); } /// /// Returns current debugger stop event arguments if debugger is in /// debug stop state. Otherwise returns null. /// /// DebuggerStopEventArgs public override DebuggerStopEventArgs GetDebuggerStopArgs() { return _wrappedDebugger.GetDebuggerStopArgs(); } /// /// Sets the parent debugger, breakpoints, function source and other /// debugging context information. /// /// Parent debugger /// List of breakpoints /// Debugger mode /// PowerShell host /// Current path /// Function to source map public override void SetParent( Debugger parent, IEnumerable breakPoints, DebuggerResumeAction? startAction, PSHost host, PathInfo path, Dictionary functionSourceMap) { // For now always enable step mode debugging. SetDebuggerStepMode(true); } /// /// Sets the parent debugger and breakpoints. /// /// Parent debugger /// List of breakpoints /// Debugger mode /// host /// Current path public override void SetParent( Debugger parent, IEnumerable breakPoints, DebuggerResumeAction? startAction, PSHost host, PathInfo path) { // For now always enable step mode debugging. SetDebuggerStepMode(true); } /// /// Sets the debugger mode. /// public override void SetDebugMode(DebugModes mode) { _wrappedDebugger.SetDebugMode(mode); base.SetDebugMode(mode); } /// /// Returns IEnumerable of CallStackFrame objects. /// /// public override IEnumerable GetCallStack() { return _wrappedDebugger.GetCallStack(); } /// /// Sets debugger stepping mode. /// /// True if stepping is to be enabled public override void SetDebuggerStepMode(bool enabled) { _wrappedDebugger.SetDebuggerStepMode(enabled); } /// /// CheckStateAndRaiseStopEvent /// internal void CheckStateAndRaiseStopEvent() { RemoteDebugger remoteDebugger = _wrappedDebugger as RemoteDebugger; if (remoteDebugger != null) { remoteDebugger.CheckStateAndRaiseStopEvent(); } } /// /// True when debugger is stopped at a breakpoint. /// 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 RemoteRunspace)) { return null; } Pipeline runningCmd = _runspace.GetCurrentlyRunningPipeline(); if (runningCmd != null) { runningCmd.DrainIncomingData(); runningCmd.SuspendIncomingData(); return runningCmd; } return null; } private void RestoreRemoteOutput(Pipeline runningCmd) { if (runningCmd != null) { runningCmd.ResumeIncomingData(); } } private void HandleBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) { this.RaiseBreakpointUpdatedEvent(e); } private DebuggerCommandResults HandlePromptCommand(PSDataCollection 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 } /// /// This job is used for running as a job the results from multiple /// pipelines. This is used in synchronouse Invoke-Expression execution /// /// /// TODO: I am not sure whether to change this internal to just InvokeExpressionSyncJob. /// /// /// Not removing the prefix "PS" as this signifies powershell specific remoting job /// internal class PSInvokeExpressionSyncJob : PSRemotingChildJob { #region Private Members private List _helpers = new List(); private ThrottleManager _throttleManager; private Dictionary _powershells = new Dictionary(); private int _pipelineFinishedCount; private int _pipelineDisconnectedCount; #endregion Private Members #region Constructors /// /// Construct an invoke-expression sync job /// /// list of operations to use /// throttle manager to use for /// this job internal PSInvokeExpressionSyncJob(List 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 (null != remoteRS) { remoteRS.AvailabilityChanged += HandleRunspaceAvailabilityChangedForInvoke; 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; /// /// Clean up once job is finished /// protected override void DoCleanupOnFinished() { bool doCleanup = false; if (_cleanupDone == false) { lock (SyncObject) { if (_cleanupDone == false) { _cleanupDone = true; doCleanup = true; } } } if (!doCleanup) return; foreach (ExecutionCmdletHelper helper in _helpers) { // cleanup remote runspace related handlers RemoteRunspace remoteRS = helper.PipelineRunspace as RemoteRunspace; if (null != remoteRS) { remoteRS.StateChanged -= HandleRunspaceStateChanged; remoteRS.URIRedirectionReported -= HandleURIDirectionReported; } StopAggregateResultsFromHelper(helper); } UnregisterThrottleComplete(_throttleManager); //throttleManager = null; Results.DecrementRef(); } /// /// release all resources /// /// true if called by Dispose() protected override void Dispose(bool disposing) { base.Dispose(disposing); } /// /// Handles operation complete from the operations. Adds an error record /// to results whenever an error is encountered /// /// sender of this event /// arguments describing this event, unused 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); } } /// /// Handle changes in pipeline states /// /// /// 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; } } /// /// 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. /// 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); } } /// /// Used to stop all operations /// public override void StopJob() { _throttleManager.StopAllOperations(); } private bool _doFinishCalled = false; /// /// This method marks the completion state for Job. Also if job failed, it processes the /// reason of failure. /// protected override void DoFinish() { if (_doFinishCalled == true) return; lock (SyncObject) { if (_doFinishCalled == true) return; _doFinishCalled = true; } foreach (ExecutionCmdletHelper helper in _helpers) { DeterminedAndSetJobState(helper); } if (_helpers.Count == 0 && this.JobStateInfo.State == JobState.NotStarted) { SetJobState(JobState.Completed); } DoCleanupOnFinished(); } /// /// Returns the PowerShell instance for the specified id /// /// instance id of PowerShell /// PowerShell instance internal override PowerShell GetPowerShell(Guid instanceId) { PowerShell powershell = null; _powershells.TryGetValue(instanceId, out powershell); return powershell; } #endregion Protected Methods #region Event Handlers /// /// Used to unregister URI Redirection handler. /// /// /// 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 (null != remoteRS) { if (e.RunspaceStateInfo.State != RunspaceState.Opening) { remoteRS.URIRedirectionReported -= HandleURIDirectionReported; if (e.RunspaceStateInfo.State != RunspaceState.Opened) { remoteRS.StateChanged -= HandleRunspaceStateChanged; remoteRS.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; } } } } #endregion #region Internal Methods /// /// Submits the operations created in the constructor for invocation. /// internal void StartOperations(List operations) { // submit operations to the throttle manager _throttleManager.SubmitOperations(operations); _throttleManager.EndSubmitOperations(); } /// /// Determines if the job is in a terminal state /// /// true, if job in terminal state /// false otherwise internal bool IsTerminalState() { return (IsFinishedState(this.JobStateInfo.State) || this.JobStateInfo.State == JobState.Disconnected); } /// /// Returns a collection of all powershells for this job. /// /// Collection of PowerShell objects. internal Collection GetPowerShells() { Collection powershellsToReturn = new Collection(); foreach (PowerShell ps in _powershells.Values) { powershellsToReturn.Add(ps); } return powershellsToReturn; } /// /// Returns a disconnected remoting job object that contains all /// remote pipelines/runspaces that are in the Disconnected state. /// /// internal PSRemotingJob CreateDisconnectedRemotingJob() { List disconnectedJobHelpers = new List(); 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; private set; } internal OutputProcessingStateEventArgs(bool processingOutput) { ProcessingOutput = processingOutput; } } internal interface IOutputProcessingState { event EventHandler OutputProcessingStateChanged; } #endregion }