/******************************************************************************* * Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"). You may not use * this file except in compliance with the License. A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * ***************************************************************************** * * AWS Tools for Windows (TM) PowerShell (TM) * */ using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Reflection; using System.Text; using Amazon.Runtime; using System.Collections; using Amazon.Util.Internal; namespace Amazon.PowerShell.Common { /// <summary> /// Ultimate base class for the AWS cmdlet hierarchy; adds helper methods for error /// and progress reporting. Cmdlets that need region or credential handling, or /// communicate with AWS services in any way, should derive from ServiceCmdlet. /// </summary> public abstract class BaseCmdlet : PSCmdlet { // path beneath user's appdata folder where we can store information protected const string AWSPowerShellAppDataSubPath = @"AWSPowerShell"; // update user agent string for current process internal static bool AWSPowerShellUserAgentSet; // the max number of items to use in a confirmation prompt, to avoid // a wall of text const int ArrayTruncationThreshold = 10; static readonly object[] EmptyObjectArray = new object[0]; // True if request contain any sensitive data protected virtual bool IsSensitiveRequest { get; set; } // True if response contain any sensitive data protected virtual bool IsSensitiveResponse { get; set; } #region Error calls /// <summary> /// Summary to throw error based on inspection of the exception type. /// </summary> /// <param name="e"></param> protected void ThrowError(Exception e) { if (e is ArgumentException) ThrowArgumentError(e.Message, this, e); else ThrowExecutionError(e.Message, this, e); } /// <summary> /// Helper to throw a terminating exception on detection of invalid argument(s) /// </summary> /// <param name="message">The message to emit to the error record</param> /// <param name="errorSource">The source (parameter or cmdlet) reporting the error</param> protected void ThrowArgumentError(string message, object errorSource) { ThrowArgumentError(message, errorSource, null); } /// <summary> /// Displays the specified warning message in the shell. /// </summary> /// <param name="message"></param> protected void DisplayWarning(string message) { this.WriteWarning(message); } /// <summary> /// Helper to throw a terminating exception on detection of invalid argument(s) /// </summary> /// <param name="message">The message to emit to the error record</param> /// <param name="errorSource">The source (parameter or cmdlet) reporting the error</param> /// <param name="innerException">The exception that occurred processing the parameter, if any</param> protected void ThrowArgumentError(string message, object errorSource, Exception innerException) { this.ThrowTerminatingError(new ErrorRecord(new ArgumentException(message, innerException), "ArgumentException", ErrorCategory.InvalidArgument, errorSource)); } protected void ThrowExecutionError(string message, object errorSource) { ThrowExecutionError(message, errorSource, null); } /// <summary> /// Helper to throw an error occuring during service execution /// </summary> /// <param name="message">The message to emit to the error record</param> /// <param name="errorSource">The source (parameter or cmdlet) reporting the error</param> /// <param name="innerException">The exception that was caught, if any</param> protected void ThrowExecutionError(string message, object errorSource, Exception innerException) { this.ThrowTerminatingError(new ErrorRecord(new InvalidOperationException(message, innerException), innerException == null ? "InvalidOperationException" : innerException.GetType().ToString(), ErrorCategory.InvalidOperation, errorSource)); } #endregion /// <summary> /// Helper to call ShouldProcess, mixing in usage of a -Force flag (more commonly used with /// ShouldContinue, but we don't use that) to override prompting unless -WhatIf is specified. /// The switch settings -WhatIf and -Confirm are retrieved from the invocation, since they /// are added dynamically at runtime by the shell. /// </summary> /// <param name="force">True if the -Force switch has been set</param> /// <param name="resourceIdentifiersText">Formatted string containing the identifiers of the resources to be operated on.</param> /// <param name="operationName">The name of the operation to be run (usually cmdlet name plus service api name)</param> /// <returns>True if the operation should proceed</returns> protected bool ConfirmShouldProceed(bool force, string resourceIdentifiersText, string operationName) { //var confirm = false; var whatif = false; if (MyInvocation.BoundParameters.ContainsKey("WhatIf")) whatif = ((SwitchParameter)MyInvocation.BoundParameters["WhatIf"]).ToBool(); // -WhatIf trumps -Force, trumps -Confirm; the interplay of these in the shell is a bit // complicated :-(. ShouldProcess always yields false if -WhatIf is set. if (whatif) return ShouldProcess(resourceIdentifiersText, operationName); // if -Confirm is not set, ShouldProcess here will not prompt unless the shell's // $ConfirmPreference level is equal or below the cmdlets declared impact level return force || ShouldProcess(resourceIdentifiersText, operationName); } /// <summary> /// Returns formatted string containing the target of the operation for use in /// confirmation messages. Collections are truncated to avoid message bloat. /// </summary> public string FormatParameterValuesForConfirmationMsg(string targetParameterName, IDictionary<string, object> boundParameters) { if (string.IsNullOrEmpty(targetParameterName) || boundParameters.Keys.Count == 0) return string.Empty; object paramValue; if (!boundParameters.TryGetValue(targetParameterName, out paramValue) || paramValue == null) return string.Empty; // probe to determine the data type and format accordingly - very few types will actually // be used as resource-identifier parameters (string and string[] are the most likely) var asString = paramValue as string; if (asString != null) return asString; var asEnumerable = paramValue as IEnumerable; if (asEnumerable != null) { // try and keep the set of items to a reasonable value, to avoid the // command line 'exploding' with a wall of text. Take() would work here // except we want to add a 'plus n more items' suffix, so we need the // overall count var sb = new StringBuilder(); var itemCount = 0; foreach (var item in asEnumerable) { if (itemCount < ArrayTruncationThreshold) { if (sb.Length != 0) sb.Append(", "); sb.Append(item); } itemCount++; } if (itemCount > ArrayTruncationThreshold) sb.AppendFormat(" (plus {0} more)", itemCount - ArrayTruncationThreshold); return sb.ToString(); } if (TypeFactory.GetTypeInfo(paramValue.GetType()).IsValueType) // unlikely but just in case... return paramValue.ToString(); // otherwise give up and report the parameter name for x-checking purposes return string.Format("values bound to the parameter {0}", targetParameterName); } /// <summary> /// Inspects the bound parameters to return the first from the set that has a value. /// Used when we are overriding the output for otherwise-void output cmdlets for -PassThru /// and the user had a choice of parameters to specify to mean the same underlying object /// (eg Beanstalk's EnvironmentId or EnvironmentName). If no bound parameter is found, the /// routine yields null. /// </summary> /// <param name="parameterNames"></param> /// <returns></returns> protected object GetFirstAssignedParameterValue(params string[] parameterNames) { var boundParameters = MyInvocation.BoundParameters; if (boundParameters == null) // don't think this is ever the case but just in case... return null; return (from name in parameterNames where boundParameters.ContainsKey(name) select boundParameters[name]).FirstOrDefault(); } /// <summary> /// Returns true if the supplied value type parameter, which corresponds to a nullable /// value type in the execution context, was bound in our current invocation and therefore /// is safe to take the value from. /// </summary> /// <param name="parameterName"></param> /// <returns></returns> protected bool ParameterWasBound(string parameterName) { return MyInvocation.BoundParameters.ContainsKey(parameterName); } /// <summary> /// Allows additional parameters to be added (manually) /// to generated cmdlets and to transform the parameter value into /// the equivalent generated parameter prior to populating the /// execution context. /// </summary> /// <param name="context"> /// Newly constructed context. On entry to this routine, the Region /// and Credentials members may have been set but no further parameter /// load has occurred. /// </param> protected virtual void PreExecutionContextLoad(ExecutorContext context) { } /// <summary> /// Allows further transformation or manipulation of parameter values /// loaded into the context before we commence processing. /// </summary> /// <param name="context"> /// The context with all parameters processed and ready for use in /// service calls (or whatever processing the cmdlet performs). /// </param> protected virtual void PostExecutionContextLoad(ExecutorContext context) { } #region Progress calls /// <summary> /// Writes progress record to the shell. /// </summary> /// <param name="activity"></param> /// <param name="message"></param> /// <param name="percentComplete"></param> public void WriteProgressRecord(string activity, string message, int? percentComplete = null) { WriteProgressRecord(this.GetHashCode(), activity, message, percentComplete); } /// <summary> /// Writes progress completed record to the shell. /// </summary> /// <param name="activity"></param> /// <param name="message"></param> public void WriteProgressCompleteRecord(string activity, string message) { WriteProgressRecord(this.GetHashCode(), activity, message, null, true); } private void WriteProgressRecord(int activityId, string activity, string message, int? percentComplete = null, bool isComplete = false) { var pr = new ProgressRecord(activityId, activity, message) { RecordType = isComplete ? ProgressRecordType.Completed : ProgressRecordType.Processing }; if (percentComplete != null) pr.PercentComplete = percentComplete.Value; WriteProgress(pr); } #endregion #region Processing helpers public IEnumerable SafeEnumerable(object value) { var s = value as string; if (s != null) return new string[] { s }; var enumerable = value as IEnumerable; if (enumerable != null) return enumerable; else return new object[] { value }; } protected AWSCmdletHistory ServiceCalls { get; private set; } protected override void BeginProcessing() { base.BeginProcessing(); if (!AWSPowerShellUserAgentSet) { Utils.Common.SetAWSPowerShellUserAgent(Host.Version); AWSPowerShellUserAgentSet = true; } // wanted to emit just the stack, or copy of it (to prevent modification) but if we do that, // we see only the Count of entries, not the actual content - need to figure out if (this.SessionState.PSVariable.Get(SessionKeys.AWSCallHistoryName) == null) this.SessionState.PSVariable.Set(SessionKeys.AWSCallHistoryName, new PSObject(AWSCmdletHistoryBuffer.Instance)); ServiceCalls = AWSCmdletHistoryBuffer.Instance.StartCmdletHistory(this.MyInvocation.MyCommand.Name); } protected override void EndProcessing() { // if we get here, there were no terminating errors during ProcessRecord AWSCmdletHistoryBuffer.Instance.PushCmdletHistory(ServiceCalls); base.EndProcessing(); } protected virtual void ProcessOutput(CmdletOutput cmdletOutput) { if (cmdletOutput == null) return; if (cmdletOutput.ErrorResponse != null) { ServiceCalls.PushServiceResponse(cmdletOutput.ErrorResponse, null); // need to manually end the history data here, as once we throw the error we won't // get called to run EndProcessing... AWSCmdletHistoryBuffer.Instance.PushCmdletHistory(ServiceCalls); ThrowExecutionError(cmdletOutput.ErrorResponse.Message, this, cmdletOutput.ErrorResponse); } else { // pipe the output manually, so we can track the number of emitted objects to add // as a convenience note on the LastServiceResponse int emittedObjectCount = 0; if (cmdletOutput.PipelineOutput != null) { IEnumerator enumerator = LanguagePrimitives.GetEnumerator(cmdletOutput.PipelineOutput); if (enumerator == null) { WriteObject(cmdletOutput.PipelineOutput); emittedObjectCount++; } else { while (enumerator.MoveNext()) { WriteObject(enumerator.Current); emittedObjectCount++; } } } ServiceCalls.EmittedObjectsCount += emittedObjectCount; } } protected void ResponseEventHandler(object sender, ResponseEventArgs args) { var wsrea = args as WebServiceResponseEventArgs; if (wsrea != null) { var response = wsrea.Response; if (response != null) ServiceCalls.PushServiceResponse(response, IsSensitiveResponse); } } protected void RequestEventHandler(object sender, RequestEventArgs args) { var wsrea = args as WebServiceRequestEventArgs; if (wsrea != null) { var request = wsrea.Request; if (request != null) ServiceCalls.PushServiceRequest(request, this.MyInvocation, IsSensitiveRequest); } } #endregion /// <summary> /// Safely emit a diagnostic message indicating where the credentials we are about to use /// originated from. /// </summary> /// <param name="awsPSCredentials"></param> protected void WriteCredentialSourceDiagnostic(AWSPSCredentials awsPSCredentials) { try { WriteCredentialSourceDiagnostic(FormatCredentialSourceForDisplay(awsPSCredentials)); } catch { } } /// <summary> /// Emit a diagnostic message indicating where the credentials we are about to use /// originated from. /// </summary> /// <param name="credentialSource"></param> protected void WriteCredentialSourceDiagnostic(string credentialSource) { WriteDebug(string.Format("Credentials obtained from {0}", credentialSource)); } /// <summary> /// Emit a diagnostic message indicating where the credentials we are about to use /// originated from. /// </summary> /// <param name="regionSource"></param> /// <param name="regionValue"></param> protected void WriteRegionSourceDiagnostic(RegionSource regionSource, string regionValue = null) { WriteRegionSourceDiagnostic(FormatRegionSourceForDisplay(regionSource), regionValue); } /// <summary> /// Emit a diagnostic message indicating where the credentials we are about to use /// originated from. /// </summary> /// <param name="regionSource"></param> /// <param name="regionValue"></param> protected void WriteRegionSourceDiagnostic(string regionSource, string regionValue = null) { var sb = new StringBuilder(); sb.AppendFormat("Region obtained from {0}", regionSource); if (!String.IsNullOrEmpty(regionValue)) sb.AppendFormat(" with value '{0}'", regionValue); WriteDebug(sb.ToString()); } #if DESKTOP protected IEnumerable<PSObject> ExecuteCmdlet(string cmdletName, Dictionary<string, object> parameters, string moduleName = "AWSPowerShell") { using (var pipeline = System.Management.Automation.Runspaces.Runspace.DefaultRunspace.CreateNestedPipeline()) { System.Management.Automation.Runspaces.Command command = new System.Management.Automation.Runspaces.Command($"{moduleName}\\{cmdletName}"); if (parameters != null) { foreach (var parameter in parameters) { command.Parameters.Add(parameter.Key, parameter.Value); } } pipeline.Commands.Add(command); return pipeline.Invoke(); } } #else protected IEnumerable<PSObject> ExecuteCmdlet(CmdletInfo cmdlet, Dictionary<string, object> parameters) { using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) { powerShell.AddCommand(cmdlet); if (parameters != null) { powerShell.AddParameters(parameters); } return powerShell.Invoke(); } } #endif /// <summary> /// Translates enum into a friendlier 'from xxx' display string /// </summary> /// <param name="creds"></param> /// <returns></returns> internal static string FormatCredentialSourceForDisplay(AWSPSCredentials creds) { switch (creds.Source) { case CredentialsSource.CredentialsObject: return "supplied credentials object"; case CredentialsSource.Container: return "container environment"; case CredentialsSource.InstanceProfile: return "instance profile"; case CredentialsSource.Profile: return String.Format("stored profile named '{0}'", creds.Name); case CredentialsSource.Session: return "shell variable $" + SessionKeys.AWSCredentialsVariableName; case CredentialsSource.Strings: return "the supplied key parameters"; } // fallback return Enum.GetName(typeof(CredentialsSource), creds.Source); } /// <summary> /// Translates enum into a friendlier 'from xxx' display string /// </summary> /// <param name="source"></param> /// <returns></returns> internal static string FormatRegionSourceForDisplay(RegionSource source) { switch (source) { case RegionSource.RegionObject: return "supplied region object"; case RegionSource.Saved: return "stored region"; case RegionSource.Session: return "shell variable $" + SessionKeys.AWSRegionVariableName; case RegionSource.String: return "region parameter"; } // fallback return Enum.GetName(typeof(RegionSource), source); } protected static Func<TResponse, TCmdlet, object> CreateSelectDelegate<TResponse, TCmdlet>(string selectString) where TCmdlet : BaseCmdlet { switch(selectString) { case null: case "": return null; case "*": return (response, cmdlet) => response; case var s when s.StartsWith("^"): { var type = typeof(TCmdlet); var parameterName = selectString.Substring(1); PropertyInfo selectedProperty = null; foreach (var property in type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)) { if (property.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase)) { selectedProperty = property; break; } else { foreach (var attributeAlias in property .GetCustomAttributes(typeof(AliasAttribute), false) .Cast<AliasAttribute>() .SelectMany(attribute => attribute.AliasNames)) { if (attributeAlias.Equals(parameterName, StringComparison.OrdinalIgnoreCase)) { selectedProperty = property; break; } } if (selectedProperty != null) { break; } } } var getter = selectedProperty?.GetGetMethod(); if (getter == null) { return null; } return (response, cmdlet) => getter.Invoke(cmdlet, EmptyObjectArray); } default: { var type = typeof(TResponse); var selectors = new List<Func<IEnumerable<object>, IEnumerable<object>>>(); foreach (var propertyName in selectString.Split('.')) { var properties = type .GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) .Where(property => property.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) .ToArray(); if (properties.Length != 1) { return null; } var getter = properties[0].GetGetMethod(); if (getter == null) { return null; } type = properties[0].PropertyType; var iEnumerableInterface = type.GetInterface("System.Collections.Generic.IEnumerable`1"); if (iEnumerableInterface != null && type != typeof(string)) { selectors.Add(enumerable => enumerable .Select(item => ((IEnumerable)getter.Invoke(item, EmptyObjectArray)).Cast<object>()) .Where(collection => collection != null) .SelectMany(collection => collection) .Where(item => item != null)); type = iEnumerableInterface.GetGenericArguments()[0]; } else { selectors.Add(enumerable => enumerable .Select(item => getter.Invoke(item, EmptyObjectArray)) .Where(item => item != null)); } } return (response, cmdlet) => { if (response == null) { return null; } IEnumerable<object> current = new object[] { response }; foreach (var selector in selectors) { current = selector(current); } return current.ToArray(); }; } } } } /// <summary> /// Base class for all AWS cmdlets that interact with an AWS service in some way and /// thus need region and credential support. /// </summary> public abstract class ServiceCmdlet : AWSCommonArgumentsCmdlet { protected AWSCredentials _CurrentCredentials { get; private set; } public RegionEndpoint _RegionEndpoint { get; private set; } protected bool _ExecuteWithAnonymousCredentials { get; set; } protected ClientConfig _ClientConfig { get; set; } protected string _AWSSignerType { get; set; } /// <summary> /// <para> /// The endpoint to make the call against. /// </para> /// <para> /// <b>Note:</b> This parameter is primarily for internal AWS use and is not required/should not be specified for /// normal usage. The cmdlets normally determine which endpoint to call based on the region specified to the -Region /// parameter or set as default in the shell (via Set-DefaultAWSRegion). Only specify this parameter if you must /// direct the call to a specific custom endpoint. /// </para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] public System.String EndpointUrl { get; set; } protected virtual string _DefaultRegion { get { return null; } } protected virtual void CustomizeClientConfig(ClientConfig config) { // if user passes $null as value, we see a bound parameter if (ParameterWasBound("EndpointUrl") && !string.IsNullOrEmpty(this.EndpointUrl)) { if(_AWSSignerType != "bearer" || (_AWSSignerType == "bearer" && config?.AWSTokenProvider == null)) { // To allow use of urls that do not contain region info, swap any region // we've already detected for the command into AuthRegion for the config; // setting ServiceUrl will clear RegionEndpoint on the config. config.AuthenticationRegion = config.RegionEndpoint.SystemName; } config.ServiceURL = this.EndpointUrl.ToString(); } } protected override void ProcessRecord() { base.ProcessRecord(); if (this._ExecuteWithAnonymousCredentials) { _CurrentCredentials = new AnonymousAWSCredentials(); WriteCredentialSourceDiagnostic("anonymous credentials"); } else { AWSPSCredentials awsPSCredentials; if (!this.TryGetCredentials(Host, out awsPSCredentials, SessionState)) ThrowExecutionError("No credentials specified or obtained from persisted/shell defaults.", this); _CurrentCredentials = awsPSCredentials.Credentials; WriteCredentialSourceDiagnostic(awsPSCredentials); } this.TryGetRegion(useInstanceMetadata: true, out var region, out var regionSource, SessionState); _RegionEndpoint = region; // At this point. if user explicitly passes Region parameter, the source is set as String. Next preference in order is explicitly passed region in ClientConfig. if (_RegionEndpoint == null || regionSource != RegionSource.String) { if (_ClientConfig?.RegionEndpoint != null) { RegionEndpoint regionFactoryValue = FallbackRegionFactory.GetRegionEndpoint(); // Set region from explicitly passed value in ClientConfig only if it is different from region resolved by .NET SDK. if (regionFactoryValue == null || _ClientConfig.RegionEndpoint.SystemName != regionFactoryValue.SystemName) { _RegionEndpoint = _ClientConfig.RegionEndpoint; regionSource = RegionSource.String; } } } if(_AWSSignerType == "bearer") { //Bearer tokens using AWSTokenProvider/Profiles do not require a region as it will be resolved by the .NET SDK. //AWSTokenProvider is set as: // 1. $config.AWSTokenProvider = New-Object -TypeName Amazon.Runtime.ProfileTokenProvider("SOME_PROFILE") // 2. $config.AWSTokenProvider = New-Object -TypeName Amazon.Runtime.StaticTokenProvider("SOME_TOKEN") _RegionEndpoint = null; regionSource = RegionSource.String; WriteRegionSourceDiagnostic("bearer-token", null); } else if (_RegionEndpoint == null) { if (String.IsNullOrEmpty(_DefaultRegion)) ThrowExecutionError("No region specified or obtained from persisted/shell defaults.", this); else { _RegionEndpoint = RegionEndpoint.GetBySystemName(_DefaultRegion); WriteRegionSourceDiagnostic("built-in-default", _DefaultRegion); } } else { WriteRegionSourceDiagnostic(regionSource, region.SystemName); } } } /// <summary> /// Base class for all AWS cmdlets that interact with an AWS service in some way but can call /// with anonymous user credentials. /// </summary> public abstract class AnonymousServiceCmdlet : AWSRegionArgumentsCmdlet { protected RegionEndpoint _RegionEndpoint { get; private set; } protected ClientConfig _ClientConfig { get; set; } protected string _AWSSignerType { get; set; } #region Parameter EndpointURL /// <summary> /// <para> /// The endpoint to make the call against. /// </para> /// <para> /// <b>Note:</b> This parameter is primarily for internal AWS use and is not required/should not be specified for /// normal usage. The cmdlets normally determine which endpoint to call based on the region specified to the -Region /// parameter or set as default in the shell (via Set-DefaultAWSRegion). Only specify this parameter if you must /// direct the call to a specific custom endpoint. /// </para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] public System.String EndpointUrl { get; set; } #endregion protected virtual string _DefaultRegion { get { return null; } } protected virtual void CustomizeClientConfig(ClientConfig config) { // if user passes $null as value, we see a bound parameter if (ParameterWasBound("EndpointUrl") && !string.IsNullOrEmpty(this.EndpointUrl)) { config.ServiceURL = this.EndpointUrl.ToString(); } } protected override void ProcessRecord() { base.ProcessRecord(); this.TryGetRegion(useInstanceMetadata: true, out var region, out var regionSource, SessionState); _RegionEndpoint = region; // At this point. if user explicitly passes Region parameter, the source is set as String. Next preference in order is explicitly passed region in ClientConfig. if (_RegionEndpoint == null || regionSource != RegionSource.String) { if (_ClientConfig?.RegionEndpoint != null) { RegionEndpoint regionFactoryValue = FallbackRegionFactory.GetRegionEndpoint(); // Set region from explicitly passed value in ClientConfig only if it is different from region resolved by .NET SDK. if (regionFactoryValue == null || _ClientConfig.RegionEndpoint.SystemName != regionFactoryValue.SystemName) { _RegionEndpoint = _ClientConfig.RegionEndpoint; regionSource = RegionSource.String; } } } if (_RegionEndpoint == null) { if (String.IsNullOrEmpty(_DefaultRegion)) ThrowExecutionError("No region specified or obtained from persisted/shell defaults.", this); else { _RegionEndpoint = RegionEndpoint.GetBySystemName(_DefaultRegion); WriteRegionSourceDiagnostic("built-in-default", _DefaultRegion); } } else { WriteRegionSourceDiagnostic(regionSource, region.SystemName); } } } public class CmdletOutput { public object PipelineOutput { get; set; } public object ServiceResponse { get; set; } public Exception ErrorResponse { get; set; } /// <summary> /// True if the output data is an enumerable collection that we should /// emit object-by-object to the pipe. Note that strings are enumerable /// so we must test for that specific case. /// </summary> public bool IsEnumerableOutput { get { if (this.PipelineOutput == null) return false; return !(this.PipelineOutput is string) && (this.PipelineOutput is IEnumerable); } } } internal class CmdletContext { } public class ExecutorContext { } public interface IExecutor { object Execute(ExecutorContext context); ExecutorContext CreateContext(); } }