/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Management.Automation; namespace Microsoft.PowerShell.Cmdletization { /// /// Provides common code for processing session-based object models. The common code /// Session, ThrottleLimit, AsJob parameters and delegates creation of jobs to derived classes. /// /// Type that represents instances of objects from the wrapped object model /// Type representing remote sessions [SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes")] public abstract class SessionBasedCmdletAdapter : CmdletAdapter, IDisposable where TObjectInstance : class where TSession : class { internal SessionBasedCmdletAdapter() { } #region Constants private const string CIMJobType = "CimJob"; #endregion #region IDisposable Members private bool disposed; /// /// Releases resources associated with this object /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases resources associated with this object /// protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { if (this.parentJob != null) { this.parentJob.Dispose(); this.parentJob = null; } } disposed = true; } } #endregion #region Common parameters (AsJob, ThrottleLimit, Session) /// /// Session to operate on /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] protected TSession[] Session { get { if (this.session == null) { this.session = new TSession[] {this.DefaultSession}; } return this.session; } set { if (value == null) { throw new ArgumentNullException("value"); } this.session = value; this.sessionWasSpecified = true; } } private TSession[] session; private bool sessionWasSpecified; /// /// Whether to wrap and emit the whole operation as a background job /// [Parameter] public SwitchParameter AsJob { get { return this.asJob; } set { this.asJob = value; } } private bool asJob; /// /// Maximum number of remote connections that can remain active at any given time. /// [Parameter] public virtual int ThrottleLimit { get; set; } #endregion Common CIM-related parameters #region Abstract methods to be overriden in derived classes /// /// Creates a object that performs a query against the wrapped object model. /// /// Remote session to query /// Query parameters /// /// /// This method shouldn't do any processing or interact with the remote session. /// Doing so will interfere with ThrottleLimit functionality. /// /// /// (and other methods returning job results) will block to support throttling and flow-control. /// Implementations of Job instance returned from this method should make sure that implementation-specific flow-control mechanism pauses further procesing, /// until calls from (and other methods returning job results) return. /// /// internal abstract StartableJob CreateQueryJob(TSession session, QueryBuilder query); private StartableJob DoCreateQueryJob(TSession sessionForJob, QueryBuilder query, Action actionAgainstResults) { StartableJob queryJob = this.CreateQueryJob(sessionForJob, query); if (queryJob != null) { if (actionAgainstResults != null) { queryJob.SuppressOutputForwarding = true; } bool discardNonPipelineResults = (actionAgainstResults != null) || !this.AsJob.IsPresent; HandleJobOutput( queryJob, sessionForJob, discardNonPipelineResults, actionAgainstResults == null ? (Action) null : delegate(PSObject pso) { var objectInstance = (TObjectInstance) LanguagePrimitives.ConvertTo(pso, typeof (TObjectInstance), CultureInfo.InvariantCulture); actionAgainstResults(sessionForJob, objectInstance); }); } return queryJob; } /// /// Creates a object that invokes an instance method in the wrapped object model. /// /// Remote session to invoke the method in /// The object on which to invoke the method /// Method invocation details /// true if successful method invocations should emit downstream the being operated on /// /// /// This method shouldn't do any processing or interact with the remote session. /// Doing so will interfere with ThrottleLimit functionality. /// /// /// (and other methods returning job results) will block to support throttling and flow-control. /// Implementations of Job instance returned from this method should make sure that implementation-specific flow-control mechanism pauses further procesing, /// until calls from (and other methods returning job results) return. /// /// internal abstract StartableJob CreateInstanceMethodInvocationJob(TSession session, TObjectInstance objectInstance, MethodInvocationInfo methodInvocationInfo, bool passThru); private StartableJob DoCreateInstanceMethodInvocationJob(TSession sessionForJob, TObjectInstance objectInstance, MethodInvocationInfo methodInvocationInfo, bool passThru, bool asJob) { StartableJob methodInvocationJob = this.CreateInstanceMethodInvocationJob(sessionForJob, objectInstance, methodInvocationInfo, passThru); if (methodInvocationJob != null) { bool discardNonPipelineResults = !asJob; HandleJobOutput( methodInvocationJob, sessionForJob, discardNonPipelineResults, outputAction: null); } return methodInvocationJob; } /// /// Creates a object that invokes a static method in the wrapped object model. /// /// Remote session to invoke the method in /// Method invocation details /// /// /// This method shouldn't do any processing or interact with the remote session. /// Doing so will interfere with ThrottleLimit functionality. /// /// /// (and other methods returning job results) will block to support throttling and flow-control. /// Implementations of Job instance returned from this method should make sure that implementation-specific flow-control mechanism pauses further procesing, /// until calls from (and other methods returning job results) return. /// /// internal abstract StartableJob CreateStaticMethodInvocationJob(TSession session, MethodInvocationInfo methodInvocationInfo); private StartableJob DoCreateStaticMethodInvocationJob(TSession sessionForJob, MethodInvocationInfo methodInvocationInfo) { StartableJob methodInvocationJob = this.CreateStaticMethodInvocationJob(sessionForJob, methodInvocationInfo); if (methodInvocationJob != null) { bool discardNonPipelineResults = !this.AsJob.IsPresent; HandleJobOutput( methodInvocationJob, sessionForJob, discardNonPipelineResults, outputAction: null); } return methodInvocationJob; } private void HandleJobOutput(Job job, TSession sessionForJob, bool discardNonPipelineResults, Action outputAction) { Action processOutput = delegate(PSObject pso) { if (pso == null) { return; } if (outputAction != null) { outputAction(pso); } }; job.Output.DataAdded += delegate(object sender, DataAddedEventArgs eventArgs) { var dataCollection = (PSDataCollection) sender; if (discardNonPipelineResults) { foreach (PSObject pso in dataCollection.ReadAll()) { // TODO/FIXME - need to make sure that the dataCollection will be throttled // (i.e. it won't accept more items - it will block in Add method) // until this processOutput call completes processOutput(pso); } } else { PSObject pso = dataCollection[eventArgs.Index]; processOutput(pso); } }; if (discardNonPipelineResults) { DiscardJobOutputs(job, JobOutputs.NonPipelineResults & (~JobOutputs.Output)); } } internal virtual TSession GetSessionOfOriginFromInstance(TObjectInstance instance) { return null; } /// /// Returns default sessions to use when the user doesn't specify the -Session cmdlet parameter. /// /// Default sessions to use when the user doesn't specify the -Session cmdlet parameter protected abstract TSession DefaultSession { get; } /// /// A new job name to use for the parent job that handles throttling of the child jobs that actually perform querying and method invocation. /// protected abstract string GenerateParentJobName(); #endregion #region Helper methods private static void DiscardJobOutputs(PSDataCollection psDataCollection) { psDataCollection.DataAdded += delegate(object sender, DataAddedEventArgs e) { var localDataCollection = (PSDataCollection)sender; localDataCollection.Clear(); }; } [Flags] private enum JobOutputs { Output = 0x1, Error = 0x2, Warning = 0x4, Verbose = 0x8, Debug = 0x10, Progress = 0x20, Results = 0x40, NonPipelineResults = Output | Error | Warning | Verbose | Debug | Progress, PipelineResults = Results, } private static void DiscardJobOutputs(Job job, JobOutputs jobOutputsToDiscard) { if (JobOutputs.Output == (jobOutputsToDiscard & JobOutputs.Output)) { DiscardJobOutputs(job.Output); } if (JobOutputs.Error == (jobOutputsToDiscard & JobOutputs.Error)) { DiscardJobOutputs(job.Error); } if (JobOutputs.Warning == (jobOutputsToDiscard & JobOutputs.Warning)) { DiscardJobOutputs(job.Warning); } if (JobOutputs.Verbose == (jobOutputsToDiscard & JobOutputs.Verbose)) { DiscardJobOutputs(job.Verbose); } if (JobOutputs.Debug == (jobOutputsToDiscard & JobOutputs.Debug)) { DiscardJobOutputs(job.Debug); } if (JobOutputs.Progress == (jobOutputsToDiscard & JobOutputs.Progress)) { DiscardJobOutputs(job.Progress); } if (JobOutputs.Results == (jobOutputsToDiscard & JobOutputs.Results)) { DiscardJobOutputs(job.Results); } } #endregion Helper methods #region Implementation of ObjectModelWrapper functionality private ThrottlingJob parentJob; /// /// Queries for object instances in the object model. /// /// Query parameters /// A lazy evaluated collection of object instances public override void ProcessRecord(QueryBuilder query) { this.parentJob.DisableFlowControlForPendingCmdletActionsQueue(); foreach (TSession sessionForJob in this.GetSessionsToActAgainst(query)) { StartableJob childJob = this.DoCreateQueryJob(sessionForJob, query, actionAgainstResults: null); if (childJob != null) { if (!this.AsJob.IsPresent) { this.parentJob.AddChildJobAndPotentiallyBlock(this.Cmdlet, childJob, ThrottlingJob.ChildJobFlags.None); } else { this.parentJob.AddChildJobWithoutBlocking(childJob, ThrottlingJob.ChildJobFlags.None); } } } } /// /// Queries for instance and invokes an instance method /// /// Query parameters /// Method invocation details /// true if successful method invocations should emit downstream the object instance being operated on public override void ProcessRecord(QueryBuilder query, MethodInvocationInfo methodInvocationInfo, bool passThru) { this.parentJob.DisableFlowControlForPendingJobsQueue(); ThrottlingJob closureOverParentJob = this.parentJob; SwitchParameter closureOverAsJob = this.AsJob; foreach (TSession sessionForJob in this.GetSessionsToActAgainst(query)) { StartableJob queryJob = this.DoCreateQueryJob( sessionForJob, query, delegate(TSession sessionForMethodInvocationJob, TObjectInstance objectInstance) { StartableJob methodInvocationJob = this.DoCreateInstanceMethodInvocationJob( sessionForMethodInvocationJob, objectInstance, methodInvocationInfo, passThru, closureOverAsJob.IsPresent); if (methodInvocationJob != null) { closureOverParentJob.AddChildJobAndPotentiallyBlock(methodInvocationJob, ThrottlingJob.ChildJobFlags.None); } }); if (queryJob != null) { if (!this.AsJob.IsPresent) { this.parentJob.AddChildJobAndPotentiallyBlock(this.Cmdlet, queryJob, ThrottlingJob.ChildJobFlags.CreatesChildJobs); } else { this.parentJob.AddChildJobWithoutBlocking(queryJob, ThrottlingJob.ChildJobFlags.CreatesChildJobs); } } } } private IEnumerable GetSessionsToActAgainst(TObjectInstance objectInstance) { if (this.sessionWasSpecified) { return this.Session; } TSession associatedSession = this.GetSessionOfOriginFromInstance(objectInstance); if (associatedSession != null) { return new[] {associatedSession}; } return new[] { this.GetImpliedSession() }; } private TSession GetSessionAssociatedWithPipelineObject() { object inputVariableValue = this.Cmdlet.Context.GetVariableValue(SpecialVariables.InputVarPath, null); if (inputVariableValue == null) { return null; } IEnumerable inputEnumerable = LanguagePrimitives.GetEnumerable(inputVariableValue); if (inputEnumerable == null) { return null; } List inputCollection = inputEnumerable.Cast().ToList(); if (inputCollection.Count != 1) { return null; } TObjectInstance inputInstance; if (!LanguagePrimitives.TryConvertTo(inputCollection[0], CultureInfo.InvariantCulture, out inputInstance)) { return null; } TSession associatedSession = this.GetSessionOfOriginFromInstance(inputInstance); return associatedSession; } private IEnumerable GetSessionsToActAgainst(QueryBuilder queryBuilder) { if (this.sessionWasSpecified) { return this.Session; } var sessionBoundQueryBuilder = queryBuilder as ISessionBoundQueryBuilder; if (sessionBoundQueryBuilder != null) { TSession sessionOfTheQueryBuilder = sessionBoundQueryBuilder.GetTargetSession(); if (sessionOfTheQueryBuilder != null) { return new[] { sessionOfTheQueryBuilder }; } } TSession sessionAssociatedWithPipelineObject = this.GetSessionAssociatedWithPipelineObject(); if (sessionAssociatedWithPipelineObject != null) { return new[] { sessionAssociatedWithPipelineObject }; } return new[] { this.GetImpliedSession() }; } private IEnumerable GetSessionsToActAgainst(MethodInvocationInfo methodInvocationInfo) { if (this.sessionWasSpecified) { return this.Session; } var associatedSessions = new HashSet(); foreach (TObjectInstance objectInstance in methodInvocationInfo.GetArgumentsOfType()) { TSession associatedSession = this.GetSessionOfOriginFromInstance(objectInstance); if (associatedSession != null) { associatedSessions.Add(associatedSession); } } if (associatedSessions.Count == 1) { return associatedSessions; } TSession sessionAssociatedWithPipelineObject = this.GetSessionAssociatedWithPipelineObject(); if (sessionAssociatedWithPipelineObject != null) { return new[] { sessionAssociatedWithPipelineObject }; } return new[] { this.GetImpliedSession() }; } internal PSModuleInfo PSModuleInfo { get { var scriptCommandInfo = this.Cmdlet.CommandInfo as IScriptCommandInfo; return scriptCommandInfo.ScriptBlock.Module; } } private TSession GetImpliedSession() { TSession sessionFromImportModule; // When being called from a CIM actiivty, this will be invoked as // a function so there will be no module info if (this.PSModuleInfo != null) { if (PSPrimitiveDictionary.TryPathGet( this.PSModuleInfo.PrivateData as IDictionary, out sessionFromImportModule, ScriptWriter.PrivateDataKey_CmdletsOverObjects, ScriptWriter.PrivateDataKey_DefaultSession)) { return sessionFromImportModule; } } return this.DefaultSession; } /// /// Invokes an instance method in the object model. /// /// The object on which to invoke the method /// Method invocation details /// true if successful method invocations should emit downstream the being operated on public override void ProcessRecord(TObjectInstance objectInstance, MethodInvocationInfo methodInvocationInfo, bool passThru) { if (objectInstance == null) throw new ArgumentNullException("objectInstance"); if (methodInvocationInfo == null) throw new ArgumentNullException("methodInvocationInfo"); foreach (TSession sessionForJob in this.GetSessionsToActAgainst(objectInstance)) { StartableJob childJob = this.DoCreateInstanceMethodInvocationJob(sessionForJob, objectInstance, methodInvocationInfo, passThru, this.AsJob.IsPresent); if (childJob != null) { if (!this.AsJob.IsPresent) { this.parentJob.AddChildJobAndPotentiallyBlock(this.Cmdlet, childJob, ThrottlingJob.ChildJobFlags.None); } else { this.parentJob.AddChildJobWithoutBlocking(childJob, ThrottlingJob.ChildJobFlags.None); } } } } /// /// Invokes a static method in the object model. /// /// Method invocation details public override void ProcessRecord(MethodInvocationInfo methodInvocationInfo) { if (methodInvocationInfo == null) throw new ArgumentNullException("methodInvocationInfo"); foreach (TSession sessionForJob in this.GetSessionsToActAgainst(methodInvocationInfo)) { StartableJob childJob = this.DoCreateStaticMethodInvocationJob(sessionForJob, methodInvocationInfo); if (childJob != null) { if (!this.AsJob.IsPresent) { this.parentJob.AddChildJobAndPotentiallyBlock(this.Cmdlet, childJob, ThrottlingJob.ChildJobFlags.None); } else { this.parentJob.AddChildJobWithoutBlocking(childJob, ThrottlingJob.ChildJobFlags.None); } } } } /// /// Performs initialization of cmdlet execution. /// public override void BeginProcessing() { if (this.AsJob.IsPresent) { MshCommandRuntime commandRuntime = (MshCommandRuntime)this.Cmdlet.CommandRuntime; // PSCmdlet.CommandRuntime is always MshCommandRuntime string conflictingParameter = null; if (commandRuntime.WhatIf.IsPresent) { conflictingParameter = "WhatIf"; } else if (commandRuntime.Confirm.IsPresent) { conflictingParameter = "Confirm"; } if (conflictingParameter != null) { string errorMessage = string.Format( CultureInfo.InvariantCulture, CmdletizationResources.SessionBasedWrapper_ShouldProcessVsJobConflict, conflictingParameter); throw new InvalidOperationException(errorMessage); } } this.parentJob = new ThrottlingJob( command: Job.GetCommandTextFromInvocationInfo(this.Cmdlet.MyInvocation), jobName: this.GenerateParentJobName(), jobTypeName: CIMJobType, maximumConcurrentChildJobs: this.ThrottleLimit, cmdletMode: !this.AsJob.IsPresent); } /// /// Performs cleanup after cmdlet execution. /// public override void EndProcessing() { this.parentJob.EndOfChildJobs(); if (this.AsJob.IsPresent) { this.Cmdlet.WriteObject(this.parentJob); this.Cmdlet.JobRepository.Add(this.parentJob); this.parentJob = null; // this class doesn't own parentJob after it has been emitted to the outside world } else { this.parentJob.ForwardAllResultsToCmdlet(this.Cmdlet); this.parentJob.Finished.WaitOne(); } } /// /// Stops the parent job when called. /// public override void StopProcessing() { Job jobToStop = this.parentJob; if (jobToStop != null) { jobToStop.StopJob(); } base.StopProcessing(); } #endregion } internal interface ISessionBoundQueryBuilder { TSession GetTargetSession(); } }