// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Management.Automation.Host;
using System.Management.Automation.Internal;
using System.Management.Automation.Remoting;
using System.Management.Automation.Remoting.Client;
using System.Management.Automation.Tracing;
using System.Threading;
using Microsoft.PowerShell.Telemetry;

using Dbg = System.Management.Automation.Diagnostics;
#if LEGACYTELEMETRY
using Microsoft.PowerShell.Telemetry.Internal;
#endif

namespace System.Management.Automation.Runspaces.Internal
{
    /// <summary>
    /// Class which supports pooling remote powerShell runspaces
    /// on the client.
    /// </summary>
    internal class RemoteRunspacePoolInternal : RunspacePoolInternal, IDisposable
    {
        #region Constructor

        /// <summary>
        /// Constructor which creates a RunspacePool using the
        /// supplied <paramref name="connectionInfo"/>, <paramref name="minRunspaces"/>
        /// and <paramref name="maxRunspaces"/>
        /// </summary>
        /// <param name="maxRunspaces">
        /// The maximum number of Runspaces that can exist in this pool.
        /// Should be greater than or equal to 1.
        /// </param>
        /// <param name="minRunspaces">
        /// The minimum number of Runspaces that can exist in this pool.
        /// Should be greater than or equal to 1.
        /// </param>
        /// <param name="typeTable">
        /// The TypeTable to use while deserializing/serializing remote objects.
        /// TypeTable has the following information used by serializer:
        ///   1. SerializationMethod
        ///   2. SerializationDepth
        ///   3. SpecificSerializationProperties
        /// TypeTable has the following information used by deserializer:
        ///   1. TargetTypeForDeserialization
        ///   2. TypeConverter
        /// </param>
        /// <param name="host">Host associated with this runspacepool.</param>
        /// <param name="applicationArguments">
        /// Application arguments the server can see in <see cref="System.Management.Automation.Remoting.PSSenderInfo.ApplicationArguments"/>
        /// </param>
        /// <param name="connectionInfo">The RunspaceConnectionInfo object
        /// which identifies this runspace pools connection to the server
        /// </param>
        /// <param name="name">Session name.</param>
        /// <exception cref="ArgumentException">
        /// Maximum runspaces is less than 1.
        /// Minimum runspaces is less than 1.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// ConnectionInfo specified is null
        /// </exception>
        internal RemoteRunspacePoolInternal(int minRunspaces,
            int maxRunspaces, TypeTable typeTable, PSHost host, PSPrimitiveDictionary applicationArguments, RunspaceConnectionInfo connectionInfo, string name = null)
            : base(minRunspaces, maxRunspaces)
        {
            if (connectionInfo == null)
            {
                throw PSTraceSource.NewArgumentNullException("WSManConnectionInfo");
            }

            PSEtwLog.LogOperationalVerbose(PSEventId.RunspacePoolConstructor,
                    PSOpcode.Constructor, PSTask.CreateRunspace,
                    PSKeyword.UseAlwaysOperational,
                    instanceId.ToString(),
                    minPoolSz.ToString(CultureInfo.InvariantCulture),
                    maxPoolSz.ToString(CultureInfo.InvariantCulture));

            _connectionInfo = connectionInfo.Clone();

            this.host = host;
            ApplicationArguments = applicationArguments;
            AvailableForConnection = false;
            DispatchTable = new DispatchTable<object>();
            _runningPowerShells = new System.Collections.Concurrent.ConcurrentStack<PowerShell>();

            if (!string.IsNullOrEmpty(name))
            {
                this.Name = name;
            }

            CreateDSHandler(typeTable);
        }

        /// <summary>
        /// Create a runspacepool object in the disconnected state.
        /// </summary>
        /// <param name="instanceId">Identifies remote session to connect.</param>
        /// <param name="name">Friendly name for runspace pool.</param>
        /// <param name="isDisconnected">Indicates whether the runspacepool is disconnected.</param>
        /// <param name="connectCommands">Array of commands associated with this runspace pool.</param>
        /// <param name="connectionInfo">Connection information for remote server.</param>
        /// <param name="host">PSHost object.</param>
        /// <param name="typeTable">TypeTable for object serialization/deserialization.</param>
        internal RemoteRunspacePoolInternal(Guid instanceId, string name, bool isDisconnected,
            ConnectCommandInfo[] connectCommands, RunspaceConnectionInfo connectionInfo, PSHost host, TypeTable typeTable)
            : base(1, 1)
        {
            if (instanceId == Guid.Empty)
            {
                throw PSTraceSource.NewArgumentException(nameof(instanceId));
            }

            if (connectCommands == null)
            {
                throw PSTraceSource.NewArgumentNullException("ConnectCommandInfo[]");
            }

            if (connectionInfo == null)
            {
                throw PSTraceSource.NewArgumentNullException("WSManConnectionInfo");
            }

            if (connectionInfo is WSManConnectionInfo)
            {
                _connectionInfo = connectionInfo.Clone();
            }
            else
            {
                Dbg.Assert(false, "ConnectionInfo must be WSManConnectionInfo");
            }

            // Create the runspace pool object to have the same instanceId as the remote session.
            this.instanceId = instanceId;

            // This indicates that this is a disconnected remote runspace pool and min/max values
            // are currently unknown. These values will be updated once the object is connected.
            this.minPoolSz = -1;
            this.maxPoolSz = -1;

            PSEtwLog.LogOperationalVerbose(PSEventId.RunspacePoolConstructor,
                    PSOpcode.Constructor, PSTask.CreateRunspace,
                    PSKeyword.UseAlwaysOperational,
                    instanceId.ToString(),
                    minPoolSz.ToString(CultureInfo.InvariantCulture),
                    maxPoolSz.ToString(CultureInfo.InvariantCulture));

            ConnectCommands = connectCommands;
            this.Name = name;
            this.host = host;
            DispatchTable = new DispatchTable<object>();
            _runningPowerShells = new System.Collections.Concurrent.ConcurrentStack<PowerShell>();

            // Create this object in the disconnected state.
            SetRunspacePoolState(new RunspacePoolStateInfo(RunspacePoolState.Disconnected, null));

            CreateDSHandler(typeTable);

            AvailableForConnection = isDisconnected;
        }

        /// <summary>
        /// Helper method to create the dispatchTable and dataStructureHandler objects.
        /// </summary>
        private void CreateDSHandler(TypeTable typeTable)
        {
            DataStructureHandler = new ClientRunspacePoolDataStructureHandler(this, typeTable);

            // register for events from the data structure handler
            DataStructureHandler.RemoteHostCallReceived += HandleRemoteHostCalls;
            DataStructureHandler.StateInfoReceived += HandleStateInfoReceived;
            DataStructureHandler.RSPoolInitInfoReceived += HandleInitInfoReceived;
            DataStructureHandler.ApplicationPrivateDataReceived += HandleApplicationPrivateDataReceived;
            DataStructureHandler.SessionClosing += HandleSessionClosing;
            DataStructureHandler.SessionClosed += HandleSessionClosed;
            DataStructureHandler.SetMaxMinRunspacesResponseReceived += HandleResponseReceived;
            DataStructureHandler.URIRedirectionReported += HandleURIDirectionReported;
            DataStructureHandler.PSEventArgsReceived += HandlePSEventArgsReceived;
            DataStructureHandler.SessionDisconnected += HandleSessionDisconnected;
            DataStructureHandler.SessionReconnected += HandleSessionReconnected;
            DataStructureHandler.SessionRCDisconnecting += HandleSessionRCDisconnecting;
            DataStructureHandler.SessionCreateCompleted += HandleSessionCreateCompleted;
        }

        #endregion Constructors

        #region Properties

        /// <summary>
        /// The connection associated with this runspace pool.
        /// </summary>
        public override RunspaceConnectionInfo ConnectionInfo
        {
            get
            {
                return _connectionInfo;
            }
        }

        /// <summary>
        /// The ClientRunspacePoolDataStructureHandler associated with this
        /// runspace pool.
        /// </summary>
        internal ClientRunspacePoolDataStructureHandler DataStructureHandler { get; private set; }

        /// <summary>
        /// List of CommandConnectInfo objects for each remote running command
        /// associated with this remote runspace pool.
        /// </summary>
        internal ConnectCommandInfo[] ConnectCommands { get; set; } = null;

        /// <summary>
        /// Gets and sets the name string for this runspace pool object.
        /// </summary>
        internal string Name
        {
            get { return _friendlyName; }

            set { _friendlyName = value ?? string.Empty; }
        }

        /// <summary>
        /// Indicates whether this runspace pools viable/available for connection.
        /// </summary>
        internal bool AvailableForConnection { get; private set; }

        /// <summary>
        /// Returns robust connection maximum retry time in milliseconds.
        /// </summary>
        internal int MaxRetryConnectionTime
        {
            get
            {
                return (DataStructureHandler != null) ? DataStructureHandler.MaxRetryConnectionTime : 0;
            }
        }

        /// <summary>
        /// Returns runspace pool availability.
        /// </summary>
        public override RunspacePoolAvailability RunspacePoolAvailability
        {
            get
            {
                RunspacePoolAvailability availability;
                if (stateInfo.State == RunspacePoolState.Disconnected)
                {
                    // Set availability for disconnected runspace pool in the
                    // same way it is set for runspaces.
                    availability = (AvailableForConnection) ?
                            RunspacePoolAvailability.None :     // Disconnected runspacepool available for connection.
                            RunspacePoolAvailability.Busy;      // Disconnected runspacepool unavailable for connection.
                }
                else
                {
                    availability = base.RunspacePoolAvailability;
                }

                return availability;
            }
        }

        /// <summary>
        /// Property to indicate that the debugger associated to this
        /// remote runspace is in debug stop mode.
        /// </summary>
        internal bool IsRemoteDebugStop
        {
            get;
            set;
        }

        #endregion

        #region internal Methods

        /// <summary>
        /// Resets the runspace state on a runspace pool with a single
        /// runspace.
        /// This is currently supported *only* for remote runspaces.
        /// </summary>
        /// <returns>True if successful.</returns>
        internal override bool ResetRunspaceState()
        {
            // Version check.  Reset Runspace is supported only on PSRP protocol
            // version 2.3 or greater.
            Version remoteProtocolVersionDeclaredByServer = PSRemotingProtocolVersion;
            if ((remoteProtocolVersionDeclaredByServer == null) ||
                (remoteProtocolVersionDeclaredByServer < RemotingConstants.ProtocolVersionWin10RTM))
            {
                throw PSTraceSource.NewInvalidOperationException(RunspacePoolStrings.ResetRunspaceStateNotSupportedOnServer);
            }

            long callId = 0;

            lock (syncObject)
            {
                callId = DispatchTable.CreateNewCallId();
                DataStructureHandler.SendResetRunspaceStateToServer(callId);
            }

            // This call blocks until the response is received.
            object response = DispatchTable.GetResponse(callId, false);
            return (bool)response;
        }

        /// <summary>
        /// Sets the maximum number of Runspaces that can be active concurrently
        /// in the pool. All requests above that number remain queued until
        /// runspaces become available.
        /// </summary>
        /// <param name="maxRunspaces">
        /// The maximum number of runspaces in the pool.
        /// </param>
        /// <returns>
        /// true if the change is successful; otherwise, false.
        /// </returns>
        /// <remarks>
        /// You cannot set the number of runspaces to a number smaller than
        /// the minimum runspaces.
        /// </remarks>
        internal override bool SetMaxRunspaces(int maxRunspaces)
        {
            bool isSizeIncreased = false;
            long callId = 0;

            lock (syncObject)
            {
                if (maxRunspaces < minPoolSz || maxRunspaces == maxPoolSz || stateInfo.State == RunspacePoolState.Closed
                    || stateInfo.State == RunspacePoolState.Closing || stateInfo.State == RunspacePoolState.Broken)
                {
                    return false;
                }

                // if the runspace pool is not opened as yet, or is in Disconnected state.
                // just change the value locally. No need to
                // send a message
                if (stateInfo.State == RunspacePoolState.BeforeOpen ||
                    stateInfo.State == RunspacePoolState.Disconnected)
                {
                    maxPoolSz = maxRunspaces;
                    return true;
                }

                // sending the message should be done within the lock
                // to ensure that multiple calls to SetMaxRunspaces
                // will be executed on the server in the order in which
                // they were called in the client
                callId = DispatchTable.CreateNewCallId();

                DataStructureHandler.SendSetMaxRunspacesToServer(maxRunspaces, callId);
            }

            // this call blocks until the response is received
            object response = DispatchTable.GetResponse(callId, false);

            isSizeIncreased = (bool)response;

            if (isSizeIncreased)
            {
                lock (syncObject)
                {
                    maxPoolSz = maxRunspaces;
                }
            }

            return isSizeIncreased;
        }

        /// <summary>
        /// Sets the minimum number of Runspaces that the pool maintains
        /// in anticipation of new requests.
        /// </summary>
        /// <param name="minRunspaces">
        /// The minimum number of runspaces in the pool.
        /// </param>
        /// <returns>
        /// true if the change is successful; otherwise, false.
        /// </returns>
        /// <remarks>
        /// You cannot set the number of idle runspaces to a number smaller than
        /// 1 or greater than maximum number of active runspaces.
        /// </remarks>
        internal override bool SetMinRunspaces(int minRunspaces)
        {
            bool isSizeDecreased = false;
            long callId = 0;

            lock (syncObject)
            {
                if ((minRunspaces < 1) || (minRunspaces > maxPoolSz) || (minRunspaces == minPoolSz)
                    || stateInfo.State == RunspacePoolState.Closed || stateInfo.State == RunspacePoolState.Closing ||
                    stateInfo.State == RunspacePoolState.Broken)
                {
                    return false;
                }

                // if the runspace pool is not opened as yet, or is in Disconnected state.
                // just change the value locally. No need to
                // send a message
                if (stateInfo.State == RunspacePoolState.BeforeOpen ||
                    stateInfo.State == RunspacePoolState.Disconnected)
                {
                    minPoolSz = minRunspaces;
                    return true;
                }

                // sending the message should be done within the lock
                // to ensure that multiple calls to SetMinRunspaces
                // will be executed on the server in the order in which
                // they were called in the client
                callId = DispatchTable.CreateNewCallId();

                DataStructureHandler.SendSetMinRunspacesToServer(minRunspaces, callId);
            }

            // this call blocks until the response is received
            object response = DispatchTable.GetResponse(callId, false);

            isSizeDecreased = (bool)response;

            if (isSizeDecreased)
            {
                lock (syncObject)
                {
                    minPoolSz = minRunspaces;
                }
            }

            return isSizeDecreased;
        }

        /// <summary>
        /// Retrieves the number of runspaces available at the time of calling
        /// this method from the remote server.
        /// </summary>
        /// <returns>The number of runspaces available in the pool.</returns>
        internal override int GetAvailableRunspaces()
        {
            int availableRunspaces = 0;
            long callId = 0;

            lock (syncObject)
            {
                // if the runspace pool is opened we need to
                // get the value from the server, else
                // return maxrunspaces
                if (stateInfo.State == RunspacePoolState.Opened)
                {
                    // sending the message should be done within the lock
                    // to ensure that multiple calls to GetAvailableRunspaces
                    // will be executed on the server in the order in which
                    // they were called in the client
                    callId = DispatchTable.CreateNewCallId();
                }
                else if (stateInfo.State != RunspacePoolState.BeforeOpen && stateInfo.State != RunspacePoolState.Opening)
                {
                    throw new InvalidOperationException(HostInterfaceExceptionsStrings.RunspacePoolNotOpened);
                }
                else
                {
                    return maxPoolSz;
                }

                DataStructureHandler.SendGetAvailableRunspacesToServer(callId);
            }

            // this call blocks until the response is received
            object response = DispatchTable.GetResponse(callId, 0);
            availableRunspaces = (int)response;

            return availableRunspaces;
        }

        /// <summary>
        /// The server sent application private data.  Store the data so that user
        /// can get it later.
        /// </summary>
        /// <param name="eventArgs">Argument describing this event.</param>
        /// <param name="sender">Sender of this event.</param>
        internal void HandleApplicationPrivateDataReceived(object sender,
            RemoteDataEventArgs<PSPrimitiveDictionary> eventArgs)
        {
            this.SetApplicationPrivateData(eventArgs.Data);
        }

        internal void HandleInitInfoReceived(object sender,
                        RemoteDataEventArgs<RunspacePoolInitInfo> eventArgs)
        {
            RunspacePoolStateInfo info = new RunspacePoolStateInfo(RunspacePoolState.Opened, null);

            bool raiseEvents = false;

            lock (syncObject)
            {
                minPoolSz = eventArgs.Data.MinRunspaces;
                maxPoolSz = eventArgs.Data.MaxRunspaces;
                if (stateInfo.State == RunspacePoolState.Connecting)
                {
                    ResetDisconnectedOnExpiresOn();

                    raiseEvents = true;
                    SetRunspacePoolState(info);
                }
            }

            if (raiseEvents)
            {
                // Private application data is sent after (post) connect.  We need
                // to wait for application data before raising the state change
                // Connecting -> Opened event.
                ThreadPool.QueueUserWorkItem(WaitAndRaiseConnectEventsProc, info);
            }
        }

        /// <summary>
        /// The state of the server RunspacePool has changed. Handle
        /// the same and reflect local states accordingly.
        /// </summary>
        /// <param name="eventArgs">Argument describing this event.</param>
        /// <param name="sender">Sender of this event.</param>
        internal void HandleStateInfoReceived(object sender,
            RemoteDataEventArgs<RunspacePoolStateInfo> eventArgs)
        {
            RunspacePoolStateInfo newStateInfo = eventArgs.Data;
            bool raiseEvents = false;

            Dbg.Assert(newStateInfo != null, "state information should not be null");

            if (newStateInfo.State == RunspacePoolState.Opened)
            {
                lock (syncObject)
                {
                    if (stateInfo.State == RunspacePoolState.Opening)
                    {
                        SetRunspacePoolState(newStateInfo);
                        raiseEvents = true;
                    }
                }

                if (raiseEvents)
                {
                    // this needs to be done outside the lock to avoid a
                    // deadlock scenario
                    RaiseStateChangeEvent(stateInfo);
                    SetOpenAsCompleted();
                }
            }
            else if (newStateInfo.State == RunspacePoolState.Closed || newStateInfo.State == RunspacePoolState.Broken)
            {
                bool doClose = false;

                lock (syncObject)
                {
                    if (stateInfo.State == RunspacePoolState.Closed || stateInfo.State == RunspacePoolState.Broken)
                    {
                        // there is nothing to do here
                        return;
                    }

                    if (stateInfo.State == RunspacePoolState.Opening
                     || stateInfo.State == RunspacePoolState.Opened
                     || stateInfo.State == RunspacePoolState.Closing)
                    {
                        doClose = true;
                        SetRunspacePoolState(newStateInfo);
                    }
                }

                if (doClose)
                {
                    // if closeAsyncResult is null, BeginClose is not called. That means
                    // we are getting close event from server, in this case release the
                    // local resources
                    if (_closeAsyncResult == null)
                    {
                        // Close the local resources.
                        DataStructureHandler.CloseRunspacePoolAsync();
                    }

                    // Delay notifying upper layers of finished state change event
                    // until after transport close ack is received (HandleSessionClosed handler).
                }
            }
        }

        /// <summary>
        /// A host call has been proxied from the server which needs to
        /// be executed.
        /// </summary>
        /// <param name="sender">Sender of this event.</param>
        /// <param name="eventArgs">Arguments describing this event.</param>
        internal void HandleRemoteHostCalls(object sender,
            RemoteDataEventArgs<RemoteHostCall> eventArgs)
        {
            if (HostCallReceived != null)
            {
                HostCallReceived.SafeInvoke(sender, eventArgs);
            }
            else
            {
                RemoteHostCall hostCall = eventArgs.Data;

                if (hostCall.IsVoidMethod)
                {
                    hostCall.ExecuteVoidMethod(host);
                }
                else
                {
                    RemoteHostResponse remoteHostResponse = hostCall.ExecuteNonVoidMethod(host);
                    DataStructureHandler.SendHostResponseToServer(remoteHostResponse);
                }
            }
        }

        internal PSHost Host
        {
            get
            {
                return host;
            }
        }

        /// <summary>
        /// Application arguments to use when opening a remote session.
        /// </summary>
        internal PSPrimitiveDictionary ApplicationArguments { get; }

        /// <summary>
        /// Private data to be used by applications built on top of PowerShell.
        ///
        /// Remote runspace pool gets its application private data from the server (when creating the remote runspace pool)
        /// - calling this method on a remote runspace will block until the data is received from the server.
        /// - unless the runspace is disconnected and data hasn't been received in which case it returns null immediately.
        /// </summary>
        internal override PSPrimitiveDictionary GetApplicationPrivateData()
        {
            if (this.RunspacePoolStateInfo.State == RunspacePoolState.Disconnected &&
                !_applicationPrivateDataReceived.WaitOne(0))
            {
                // Runspace pool was disconnected before application data was returned.  Application
                // data cannot be returned with the runspace pool disconnected so return null.
                return null;
            }

            return _applicationPrivateData;
        }

        internal void SetApplicationPrivateData(PSPrimitiveDictionary applicationPrivateData)
        {
            lock (this.syncObject)
            {
                if (_applicationPrivateDataReceived.WaitOne(0))
                {
                    return; // ignore server's attempt to set application private data if it has already been set
                }

                _applicationPrivateData = applicationPrivateData;
                _applicationPrivateDataReceived.Set();

                foreach (Runspace runspace in this.runspaceList)
                {
                    runspace.SetApplicationPrivateData(applicationPrivateData);
                }
            }
        }

        internal override void PropagateApplicationPrivateData(Runspace runspace)
        {
            if (_applicationPrivateDataReceived.WaitOne(0))
            {
                runspace.SetApplicationPrivateData(this.GetApplicationPrivateData());
            }
        }

        private PSPrimitiveDictionary _applicationPrivateData;
        private readonly ManualResetEvent _applicationPrivateDataReceived = new ManualResetEvent(false);

        /// <summary>
        /// This event is raised, when a host call is for a remote runspace
        /// which this runspace pool wraps.
        /// </summary>
        internal event EventHandler<RemoteDataEventArgs<RemoteHostCall>> HostCallReceived;

        /// <summary>
        /// EventHandler used to report connection URI redirections to the application.
        /// </summary>
        internal event EventHandler<RemoteDataEventArgs<Uri>> URIRedirectionReported;

        /// <summary>
        /// Notifies the successful creation of the runspace session.
        /// </summary>
        internal event EventHandler<CreateCompleteEventArgs> SessionCreateCompleted;

        /// <summary>
        /// </summary>
        /// <param name="shell"></param>
        internal void CreatePowerShellOnServerAndInvoke(ClientRemotePowerShell shell)
        {
            DataStructureHandler.CreatePowerShellOnServerAndInvoke(shell);

            // send any input that may be available
            if (!shell.NoInput)
            {
                shell.SendInput();
            }
        }

        /// <summary>
        /// Add a ClientPowerShellDataStructureHandler to ClientRunspaceDataStructureHandler list.
        /// </summary>
        /// <param name="psShellInstanceId">PowerShell Instance Id.</param>
        /// <param name="psDSHandler">ClientPowerShellDataStructureHandler for PowerShell.</param>
        internal void AddRemotePowerShellDSHandler(Guid psShellInstanceId, ClientPowerShellDataStructureHandler psDSHandler)
        {
            DataStructureHandler.AddRemotePowerShellDSHandler(psShellInstanceId, psDSHandler);
        }

        /// <summary>
        /// Returns true if Runspace supports disconnect.
        /// </summary>
        internal bool CanDisconnect
        {
            get
            {
                Version remoteProtocolVersionDeclaredByServer = PSRemotingProtocolVersion;
                if (remoteProtocolVersionDeclaredByServer != null && DataStructureHandler != null)
                {
                    // Disconnect/Connect support is currently only provided by the WSMan transport
                    // that is running PSRP protocol version 2.2 and greater.
                    return (remoteProtocolVersionDeclaredByServer >= RemotingConstants.ProtocolVersionWin8RTM &&
                            DataStructureHandler.EndpointSupportsDisconnect);
                }

                return false;
            }
        }

        /// <summary>
        /// Returns the WinRM protocol version object for this runspace
        /// pool connection.
        /// </summary>
        internal Version PSRemotingProtocolVersion
        {
            get
            {
                Version winRMProtocolVersion = null;

                PSPrimitiveDictionary psPrimitiveDictionary = GetApplicationPrivateData();
                if (psPrimitiveDictionary != null)
                {
                    PSPrimitiveDictionary.TryPathGet(
                        psPrimitiveDictionary,
                        out winRMProtocolVersion,
                        PSVersionInfo.PSVersionTableName,
                        PSVersionInfo.PSRemotingProtocolVersionName);
                }

                return winRMProtocolVersion;
            }
        }

        /// <summary>
        /// Push a running PowerShell onto the stack.
        /// </summary>
        /// <param name="ps">PowerShell.</param>
        internal void PushRunningPowerShell(PowerShell ps)
        {
            Dbg.Assert(ps != null, "Caller should not pass in null reference.");
            _runningPowerShells.Push(ps);
        }

        /// <summary>
        /// Pop the currently running PowerShell from stack.
        /// </summary>
        /// <returns>PowerShell.</returns>
        internal PowerShell PopRunningPowerShell()
        {
            PowerShell powershell;
            if (_runningPowerShells.TryPop(out powershell))
            {
                return powershell;
            }

            return null;
        }

        /// <summary>
        /// Return the current running PowerShell.
        /// </summary>
        /// <returns>PowerShell.</returns>
        internal PowerShell GetCurrentRunningPowerShell()
        {
            PowerShell powershell;
            if (_runningPowerShells.TryPeek(out powershell))
            {
                return powershell;
            }

            return null;
        }

        #endregion Internal Methods

        #region Protected Methods

        /// <summary>
        /// Opens the runspacepool synchronously / asynchronously.
        /// Runspace pool must be opened before it can be used.
        /// </summary>
        /// <param name="isAsync">
        /// true to open asynchronously
        /// </param>
        /// <param name="callback">
        /// A AsyncCallback to call once the BeginOpen completes.
        /// </param>
        /// <param name="asyncState">
        /// A user supplied state to call the <paramref name="callback"/>
        /// with.
        /// </param>
        /// <returns>
        /// asyncResult object to monitor status of the async
        /// open operation. This is returned only if <paramref name="isAsync"/>
        /// is true.
        /// </returns>
        /// <exception cref="InvalidRunspacePoolStateException">
        /// Cannot open RunspacePool because RunspacePool is not in
        /// the BeforeOpen state.
        /// </exception>
        /// <exception cref="OutOfMemoryException">
        /// There is not enough memory available to start this asynchronously.
        /// </exception>
        protected override IAsyncResult CoreOpen(bool isAsync, AsyncCallback callback,
            object asyncState)
        {
            PSEtwLog.SetActivityIdForCurrentThread(this.InstanceId);
            PSEtwLog.LogOperationalVerbose(PSEventId.RunspacePoolOpen, PSOpcode.Open,
                            PSTask.CreateRunspace, PSKeyword.UseAlwaysOperational);

            // Telemetry here - remote session
            ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.RemoteSessionOpen, isAsync.ToString());
#if LEGACYTELEMETRY
            TelemetryAPI.ReportRemoteSessionCreated(_connectionInfo);
#endif

            lock (syncObject)
            {
                AssertIfStateIsBeforeOpen();

                stateInfo = new RunspacePoolStateInfo(RunspacePoolState.Opening, null);
            }

            // BUGBUG: the following comment needs to be validated
            // only one thread will reach here, so no need
            // to lock
            RaiseStateChangeEvent(stateInfo);

            RunspacePoolAsyncResult asyncResult = new RunspacePoolAsyncResult(
                    instanceId, callback, asyncState, true);

            _openAsyncResult = asyncResult;

            // send a message using the data structure handler to open the RunspacePool
            // on the remote server
            DataStructureHandler.CreateRunspacePoolAndOpenAsync();

            return asyncResult;
        }

        #endregion Protected Methods

        #region Public Methods

        /// <summary>
        /// Synchronous open.
        /// </summary>
        public override void Open()
        {
            IAsyncResult asyncResult = BeginOpen(null, null);

            EndOpen(asyncResult);
        }

        /// <summary>
        /// Closes the RunspacePool and cleans all the internal
        /// resources. This will close all the runspaces in the
        /// runspacepool and release all the async operations
        /// waiting for a runspace. If the pool is already closed
        /// or broken or closing this will just return.
        /// </summary>
        /// <exception cref="InvalidRunspacePoolStateException">
        /// Cannot close the RunspacePool because RunspacePool is
        /// in Closing state.
        /// </exception>
        public override void Close()
        {
            // close and wait
            IAsyncResult asyncResult = BeginClose(null, null);
            EndClose(asyncResult);
        }

        /// <summary>
        /// Closes the RunspacePool asynchronously. To get the exceptions
        /// that might have occurred, call EndOpen.
        /// </summary>
        /// <param name="callback">
        /// An AsyncCallback to call once the BeginClose completes
        /// </param>
        /// <param name="asyncState">
        /// A user supplied state to call the <paramref name="callback"/>
        /// with
        /// </param>
        /// <returns>
        /// An AsyncResult object to monitor the state of the async
        /// operation
        /// </returns>
        public override IAsyncResult BeginClose(AsyncCallback callback, object asyncState)
        {
            bool raiseEvents = false;
            bool skipClosing = false;
            RunspacePoolStateInfo copyState = new RunspacePoolStateInfo(RunspacePoolState.BeforeOpen, null);
            RunspacePoolAsyncResult asyncResult = null;

            lock (syncObject)
            {
                if ((stateInfo.State == RunspacePoolState.Closed) ||
                    (stateInfo.State == RunspacePoolState.Broken))
                {
                    skipClosing = true;
                    asyncResult = new RunspacePoolAsyncResult(instanceId, callback, asyncState, false);
                }
                else if (stateInfo.State == RunspacePoolState.BeforeOpen)
                {
                    copyState = stateInfo = new RunspacePoolStateInfo(RunspacePoolState.Closed, null);
                    raiseEvents = true;
                    skipClosing = true;
                    _closeAsyncResult = null;
                    asyncResult = new RunspacePoolAsyncResult(instanceId, callback, asyncState, false);
                }
                else if (stateInfo.State == RunspacePoolState.Opened ||
                         stateInfo.State == RunspacePoolState.Opening)
                {
                    copyState = stateInfo = new RunspacePoolStateInfo(RunspacePoolState.Closing, null);
                    _closeAsyncResult = new RunspacePoolAsyncResult(instanceId, callback, asyncState, false);
                    asyncResult = _closeAsyncResult;
                    raiseEvents = true;
                }
                else if (stateInfo.State == RunspacePoolState.Disconnected ||
                         stateInfo.State == RunspacePoolState.Disconnecting ||
                         stateInfo.State == RunspacePoolState.Connecting)
                {
                    // Continue with closing so the PSRP layer is aware that the client side session is
                    // being closed.  This will result in a broken session on the client.
                    _closeAsyncResult = new RunspacePoolAsyncResult(instanceId, callback, asyncState, false);
                    asyncResult = _closeAsyncResult;
                }
                else if (stateInfo.State == RunspacePoolState.Closing)
                {
                    return _closeAsyncResult;
                }
            }

            // raise the events outside the lock
            if (raiseEvents)
            {
                RaiseStateChangeEvent(copyState);
            }

            if (!skipClosing)
            {
                // SetRunspacePoolState(new RunspacePoolStateInfo(RunspacePoolState.Closing, null), true);

                // send a message using the data structure handler to close the RunspacePool
                // on the remote server
                DataStructureHandler.CloseRunspacePoolAsync();
            }
            else
            {
                // signal the wait handle
                asyncResult.SetAsCompleted(null);
            }

            return asyncResult;
        }

        /// <summary>
        /// Synchronous disconnect.
        /// </summary>
        public override void Disconnect()
        {
            IAsyncResult asyncResult = BeginDisconnect(null, null);

            EndDisconnect(asyncResult);
        }

        /// <summary>
        /// Asynchronous disconnect.
        /// </summary>
        /// <param name="callback">AsyncCallback object.</param>
        /// <param name="state">State object.</param>
        /// <returns>IAsyncResult.</returns>
        public override IAsyncResult BeginDisconnect(AsyncCallback callback, object state)
        {
            if (!CanDisconnect)
            {
                throw PSTraceSource.NewInvalidOperationException(RunspacePoolStrings.DisconnectNotSupportedOnServer);
            }

            RunspacePoolState currentState;
            bool raiseEvents = false;
            lock (syncObject)
            {
                currentState = stateInfo.State;
                if (currentState == RunspacePoolState.Opened)
                {
                    RunspacePoolStateInfo newStateInfo = new RunspacePoolStateInfo(RunspacePoolState.Disconnecting, null);

                    SetRunspacePoolState(newStateInfo);
                    raiseEvents = true;
                }
            }

            // Raise events outside of lock.
            if (raiseEvents)
            {
                RaiseStateChangeEvent(this.stateInfo);
            }

            if (currentState == RunspacePoolState.Opened)
            {
                RunspacePoolAsyncResult asyncResult = new RunspacePoolAsyncResult(
                    instanceId, callback, state, false);

                _disconnectAsyncResult = asyncResult;
                DataStructureHandler.DisconnectPoolAsync();

                // Return local reference to async object since the class member can
                // be asynchronously nulled if the session closes suddenly.
                return asyncResult;
            }
            else
            {
                string message = StringUtil.Format(RunspacePoolStrings.InvalidRunspacePoolState, RunspacePoolState.Opened, stateInfo.State);
                InvalidRunspacePoolStateException invalidStateException = new InvalidRunspacePoolStateException(message,
                        stateInfo.State, RunspacePoolState.Opened);

                throw invalidStateException;
            }
        }

        /// <summary>
        /// Waits for BeginDisconnect operation to complete.
        /// </summary>
        /// <param name="asyncResult">IAsyncResult object.</param>
        public override void EndDisconnect(IAsyncResult asyncResult)
        {
            if (asyncResult == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(asyncResult));
            }

            RunspacePoolAsyncResult rsAsyncResult = asyncResult as RunspacePoolAsyncResult;

            if ((rsAsyncResult == null) ||
                (rsAsyncResult.OwnerId != instanceId) ||
                (rsAsyncResult.IsAssociatedWithAsyncOpen))
            {
                throw PSTraceSource.NewArgumentException(nameof(asyncResult),
                                                         RunspacePoolStrings.AsyncResultNotOwned,
                                                         "IAsyncResult",
                                                         "BeginOpen");
            }

            rsAsyncResult.EndInvoke();
        }

        /// <summary>
        /// Synchronous connect.
        /// </summary>
        public override void Connect()
        {
            IAsyncResult asyncResult = BeginConnect(null, null);

            EndConnect(asyncResult);
        }

        /// <summary>
        /// Asynchronous connect.
        /// </summary>
        /// <param name="callback">ASyncCallback object.</param>
        /// <param name="state">State Object.</param>
        /// <returns>IAsyncResult.</returns>
        public override IAsyncResult BeginConnect(AsyncCallback callback, object state)
        {
            if (!AvailableForConnection)
            {
                throw PSTraceSource.NewInvalidOperationException(RunspacePoolStrings.CannotConnect);
            }

            RunspacePoolState currentState;
            bool raiseEvents = false;
            lock (syncObject)
            {
                currentState = stateInfo.State;
                if (currentState == RunspacePoolState.Disconnected)
                {
                    RunspacePoolStateInfo newStateInfo = new RunspacePoolStateInfo(RunspacePoolState.Connecting, null);

                    SetRunspacePoolState(newStateInfo);
                    raiseEvents = true;
                }
            }

            // Raise events outside of lock.
            if (raiseEvents)
            {
                RaiseStateChangeEvent(this.stateInfo);
            }

            raiseEvents = false;

            if (currentState == RunspacePoolState.Disconnected)
            {
                // Assign to local variable to ensure we always pass a non-null value.
                // The async class members can be nulled out if the session closes suddenly.
                RunspacePoolAsyncResult ret = new RunspacePoolAsyncResult(
                    instanceId, callback, state, false);

                if (_canReconnect)
                {
                    // This indicates a reconnect scenario where this object instance was previously
                    // disconnected.
                    _reconnectAsyncResult = ret;
                    DataStructureHandler.ReconnectPoolAsync();
                }
                else
                {
                    // This indicates a reconstruction scenario where this object was created
                    // in the disconnect state and is being connected for the first time.
                    _openAsyncResult = ret;
                    DataStructureHandler.ConnectPoolAsync();
                }

                if (raiseEvents)
                {
                    RaiseStateChangeEvent(this.stateInfo);
                }

                return ret;
            }
            else
            {
                string message = StringUtil.Format(RunspacePoolStrings.InvalidRunspacePoolState, RunspacePoolState.Disconnected, stateInfo.State);
                InvalidRunspacePoolStateException invalidStateException = new InvalidRunspacePoolStateException(message,
                        stateInfo.State, RunspacePoolState.Disconnected);

                throw invalidStateException;
            }
        }

        /// <summary>
        /// Waits for BeginConnect to complete.
        /// </summary>
        /// <param name="asyncResult">IAsyncResult object.</param>
        public override void EndConnect(IAsyncResult asyncResult)
        {
            if (asyncResult == null)
            {
                throw PSTraceSource.NewArgumentNullException(nameof(asyncResult));
            }

            RunspacePoolAsyncResult rsAsyncResult = asyncResult as RunspacePoolAsyncResult;

            if ((rsAsyncResult == null) ||
                (rsAsyncResult.OwnerId != instanceId) ||
                (rsAsyncResult.IsAssociatedWithAsyncOpen))
            {
                throw PSTraceSource.NewArgumentException(nameof(asyncResult),
                                                         RunspacePoolStrings.AsyncResultNotOwned,
                                                         "IAsyncResult",
                                                         "BeginOpen");
            }

            rsAsyncResult.EndInvoke();
        }

        /// <summary>
        /// Creates an array of PowerShell objects that are in the Disconnected state for
        /// all currently disconnected running commands associated with this runspace pool.
        /// </summary>
        /// <returns>Array of PowerShell objects.</returns>
        public override Collection<PowerShell> CreateDisconnectedPowerShells(RunspacePool runspacePool)
        {
            Collection<PowerShell> psCollection = new Collection<PowerShell>();

            if (ConnectCommands == null)
            {
                // Throw error indicating that this runspacepool is not configured for
                // reconstructing commands.
                string msg = StringUtil.Format(RunspacePoolStrings.CannotReconstructCommands, this.Name);
                throw new InvalidRunspacePoolStateException(msg);
            }

            // Get list of all disconnected commands associated with this runspace pool.
            foreach (ConnectCommandInfo connectCmdInfo in ConnectCommands)
            {
                psCollection.Add(new PowerShell(connectCmdInfo, runspacePool));
            }

            return psCollection;
        }

        /// <summary>
        /// Returns RunspacePool capabilities.
        /// </summary>
        /// <returns>RunspacePoolCapability.</returns>
        public override RunspacePoolCapability GetCapabilities()
        {
            RunspacePoolCapability returnCaps = RunspacePoolCapability.Default;

            if (CanDisconnect)
            {
                returnCaps |= RunspacePoolCapability.SupportsDisconnect;
            }

            return returnCaps;
        }

        #endregion Public Methods

        #region Static methods

        internal static RunspacePool[] GetRemoteRunspacePools(RunspaceConnectionInfo connectionInfo, PSHost host, TypeTable typeTable)
        {
            if (!(connectionInfo is WSManConnectionInfo wsmanConnectionInfoParam))
            {
                // Disconnect-Connect currently only supported by WSMan.
                throw new NotSupportedException();
            }

            List<RunspacePool> discRunspacePools = new List<RunspacePool>();

            // Enumerate all runspacepools
            Collection<PSObject> runspaceItems = RemoteRunspacePoolEnumeration.GetRemotePools(wsmanConnectionInfoParam);
            foreach (PSObject rsObject in runspaceItems)
            {
                // Create a new WSMan connection info object for each returned runspace pool.
                WSManConnectionInfo wsmanConnectionInfo = wsmanConnectionInfoParam.Copy();

                PSPropertyInfo pspShellId = rsObject.Properties["ShellId"];
                PSPropertyInfo pspState = rsObject.Properties["State"];
                PSPropertyInfo pspName = rsObject.Properties["Name"];
                PSPropertyInfo pspResourceUri = rsObject.Properties["ResourceUri"];

                if (pspShellId == null || pspState == null || pspName == null || pspResourceUri == null)
                {
                    continue;
                }

                string strName = pspName.Value.ToString();
                string strShellUri = pspResourceUri.Value.ToString();
                bool isDisconnected = pspState.Value.ToString().Equals("Disconnected", StringComparison.OrdinalIgnoreCase);
                Guid shellId = Guid.Parse(pspShellId.Value.ToString());

                // Filter returned items for PowerShell sessions.
                if (!strShellUri.StartsWith(WSManNativeApi.ResourceURIPrefix, StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                // Update wsmanconnection information with server settings.
                UpdateWSManConnectionInfo(wsmanConnectionInfo, rsObject);
                // Ensure that EnableNetworkAccess property is always enabled for reconstructed runspaces.
                wsmanConnectionInfo.EnableNetworkAccess = true;

                // Compute runspace DisconnectedOn and ExpiresOn fields.
                if (isDisconnected)
                {
                    DateTime? disconnectedOn;
                    DateTime? expiresOn;
                    ComputeDisconnectedOnExpiresOn(rsObject, out disconnectedOn, out expiresOn);
                    wsmanConnectionInfo.DisconnectedOn = disconnectedOn;
                    wsmanConnectionInfo.ExpiresOn = expiresOn;
                }

                List<ConnectCommandInfo> connectCmdInfos = new List<ConnectCommandInfo>();

                // Enumerate all commands on runspace pool.
                Collection<PSObject> commandItems;
                try
                {
                    commandItems = RemoteRunspacePoolEnumeration.GetRemoteCommands(shellId, wsmanConnectionInfo);
                }
                catch (CmdletInvocationException e)
                {
                    if (e.InnerException != null && e.InnerException is InvalidOperationException)
                    {
                        // If we cannot successfully retrieve command information then this runspace
                        // object we are building is invalid and must be skipped.
                        continue;
                    }

                    throw;
                }

                foreach (PSObject cmdObject in commandItems)
                {
                    PSPropertyInfo pspCommandId = cmdObject.Properties["CommandId"];
                    PSPropertyInfo pspCommandLine = cmdObject.Properties["CommandLine"];

                    if (pspCommandId == null)
                    {
                        Dbg.Assert(false, "Should not get an empty command Id from a remote runspace pool.");
                        continue;
                    }

                    string cmdLine = (pspCommandLine != null) ? pspCommandLine.Value.ToString() : string.Empty;
                    Guid cmdId = Guid.Parse(pspCommandId.Value.ToString());

                    connectCmdInfos.Add(new ConnectCommandInfo(cmdId, cmdLine));
                }

                // At this point we don't know if the runspace pool we want to connect to has just one runspace
                // (a RemoteRunspace/PSSession) or multiple runspaces in its pool.  We do have an array of
                // running command information which will indicate a runspace pool if the count is gt one.
                RunspacePool runspacePool = new RunspacePool(isDisconnected, shellId, strName,
                    connectCmdInfos.ToArray(), wsmanConnectionInfo, host, typeTable);
                discRunspacePools.Add(runspacePool);
            }

            return discRunspacePools.ToArray();
        }

        internal static RunspacePool GetRemoteRunspacePool(RunspaceConnectionInfo connectionInfo, Guid sessionId, Guid? commandId, PSHost host, TypeTable typeTable)
        {
            List<ConnectCommandInfo> connectCmdInfos = new List<ConnectCommandInfo>();
            if (commandId != null)
            {
                connectCmdInfos.Add(new ConnectCommandInfo(commandId.Value, string.Empty));
            }

            return new RunspacePool(true, sessionId, string.Empty, connectCmdInfos.ToArray(), connectionInfo, host, typeTable);
        }

        private static void UpdateWSManConnectionInfo(
            WSManConnectionInfo wsmanConnectionInfo,
            PSObject rsInfoObject)
        {
            PSPropertyInfo pspIdleTimeOut = rsInfoObject.Properties["IdleTimeOut"];
            PSPropertyInfo pspBufferMode = rsInfoObject.Properties["BufferMode"];
            PSPropertyInfo pspResourceUri = rsInfoObject.Properties["ResourceUri"];
            PSPropertyInfo pspLocale = rsInfoObject.Properties["Locale"];
            PSPropertyInfo pspDataLocale = rsInfoObject.Properties["DataLocale"];
            PSPropertyInfo pspCompressionMode = rsInfoObject.Properties["CompressionMode"];
            PSPropertyInfo pspEncoding = rsInfoObject.Properties["Encoding"];
            PSPropertyInfo pspProfile = rsInfoObject.Properties["ProfileLoaded"];
            PSPropertyInfo pspMaxIdleTimeout = rsInfoObject.Properties["MaxIdleTimeout"];

            if (pspIdleTimeOut != null)
            {
                int idleTimeout;
                if (GetTimeIntValue(pspIdleTimeOut.Value as string, out idleTimeout))
                {
                    wsmanConnectionInfo.IdleTimeout = idleTimeout;
                }
            }

            if (pspBufferMode != null)
            {
                string bufferingMode = pspBufferMode.Value as string;
                if (bufferingMode != null)
                {
                    OutputBufferingMode outputBufferingMode;
                    if (Enum.TryParse<OutputBufferingMode>(bufferingMode, out outputBufferingMode))
                    {
                        // Update connection info.
                        wsmanConnectionInfo.OutputBufferingMode = outputBufferingMode;
                    }
                }
            }

            if (pspResourceUri != null)
            {
                string strShellUri = pspResourceUri.Value as string;
                if (strShellUri != null)
                {
                    wsmanConnectionInfo.ShellUri = strShellUri;
                }
            }

            if (pspLocale != null)
            {
                string localString = pspLocale.Value as string;
                if (localString != null)
                {
                    try
                    {
                        wsmanConnectionInfo.UICulture = new CultureInfo(localString);
                    }
                    catch (ArgumentException)
                    { }
                }
            }

            if (pspDataLocale != null)
            {
                string dataLocalString = pspDataLocale.Value as string;
                if (dataLocalString != null)
                {
                    try
                    {
                        wsmanConnectionInfo.Culture = new CultureInfo(dataLocalString);
                    }
                    catch (ArgumentException)
                    { }
                }
            }

            if (pspCompressionMode != null)
            {
                string compressionModeString = pspCompressionMode.Value as string;
                if (compressionModeString != null)
                {
                    wsmanConnectionInfo.UseCompression = !compressionModeString.Equals("NoCompression", StringComparison.OrdinalIgnoreCase);
                }
            }

            if (pspEncoding != null)
            {
                string encodingString = pspEncoding.Value as string;
                if (encodingString != null)
                {
                    wsmanConnectionInfo.UseUTF16 = encodingString.Equals("UTF16", StringComparison.OrdinalIgnoreCase);
                }
            }

            if (pspProfile != null)
            {
                string machineProfileLoadedString = pspProfile.Value as string;
                if (machineProfileLoadedString != null)
                {
                    wsmanConnectionInfo.NoMachineProfile = !machineProfileLoadedString.Equals("Yes", StringComparison.OrdinalIgnoreCase);
                }
            }

            if (pspMaxIdleTimeout != null)
            {
                int maxIdleTimeout;
                if (GetTimeIntValue(pspMaxIdleTimeout.Value as string, out maxIdleTimeout))
                {
                    wsmanConnectionInfo.MaxIdleTimeout = maxIdleTimeout;
                }
            }
        }

        private static void ComputeDisconnectedOnExpiresOn(
            PSObject rsInfoObject,
            out DateTime? disconnectedOn,
            out DateTime? expiresOn)
        {
            PSPropertyInfo pspIdleTimeOut = rsInfoObject.Properties["IdleTimeOut"];
            PSPropertyInfo pspShellInactivity = rsInfoObject.Properties["ShellInactivity"];

            if (pspIdleTimeOut != null && pspShellInactivity != null)
            {
                string shellInactivityString = pspShellInactivity.Value as string;
                int idleTimeout;
                if ((shellInactivityString != null) &&
                    GetTimeIntValue(pspIdleTimeOut.Value as string, out idleTimeout))
                {
                    try
                    {
                        TimeSpan shellInactivityTime = Xml.XmlConvert.ToTimeSpan(shellInactivityString);
                        TimeSpan idleTimeoutTime = TimeSpan.FromSeconds(idleTimeout / 1000);

                        if (idleTimeoutTime > shellInactivityTime)
                        {
                            DateTime now = DateTime.Now;
                            disconnectedOn = now.Subtract(shellInactivityTime);
                            expiresOn = disconnectedOn.Value.Add(idleTimeoutTime);

                            return;
                        }
                    }
                    catch (FormatException)
                    { }
                    catch (ArgumentOutOfRangeException)
                    { }
                    catch (OverflowException)
                    { }
                }
            }

            disconnectedOn = null;
            expiresOn = null;
        }

        private static bool GetTimeIntValue(string timeString, out int value)
        {
            if (timeString != null)
            {
                string timeoutString = timeString.Replace("PT", string.Empty).Replace("S", string.Empty);
                try
                {
                    // Convert time from seconds to milliseconds.
                    int idleTimeout = (int)(Convert.ToDouble(timeoutString, CultureInfo.InvariantCulture) * 1000);
                    if (idleTimeout > 0)
                    {
                        value = idleTimeout;
                        return true;
                    }
                }
                catch (FormatException)
                { }
                catch (OverflowException)
                { }
            }

            value = 0;
            return false;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Set the new runspace pool state based on the state of the
        /// server RunspacePool.
        /// </summary>
        /// <param name="newStateInfo">state information object
        /// describing the state change at the server RunspacePool</param>
        private void SetRunspacePoolState(RunspacePoolStateInfo newStateInfo)
        {
            SetRunspacePoolState(newStateInfo, false);
        }

        /// <summary>
        /// Set the new runspace pool state based on the state of the
        /// server RunspacePool and raise events if required.
        /// </summary>
        /// <param name="newStateInfo">state information object
        /// describing the state change at the server RunspacePool</param>
        /// <param name="raiseEvents">Raise state changed events if true.</param>
        private void SetRunspacePoolState(RunspacePoolStateInfo newStateInfo, bool raiseEvents)
        {
            stateInfo = newStateInfo;

            // Update the availableForConnection variable based on state change.
            AvailableForConnection = (stateInfo.State == RunspacePoolState.Disconnected ||
                                           stateInfo.State == RunspacePoolState.Opened);

            if (raiseEvents)
            {
                RaiseStateChangeEvent(newStateInfo);
            }
        }

        private void HandleSessionDisconnected(object sender, RemoteDataEventArgs<Exception> eventArgs)
        {
            bool stateChange = false;
            lock (this.syncObject)
            {
                if (stateInfo.State == RunspacePoolState.Disconnecting)
                {
                    UpdateDisconnectedExpiresOn();

                    SetRunspacePoolState(new RunspacePoolStateInfo(RunspacePoolState.Disconnected, eventArgs.Data));
                    stateChange = true;
                }

                // Set boolean indicating this object has previous connection state and so
                // can be reconnected as opposed to the alternative where the connection
                // state has to be reconstructed then connected.
                _canReconnect = true;
            }

            // Do state change work outside of lock.
            if (stateChange)
            {
                RaiseStateChangeEvent(this.stateInfo);
                SetDisconnectAsCompleted();
            }
        }

        private void SetDisconnectAsCompleted()
        {
            if (_disconnectAsyncResult != null && !_disconnectAsyncResult.IsCompleted)
            {
                _disconnectAsyncResult.SetAsCompleted(stateInfo.Reason);
                _disconnectAsyncResult = null;
            }
        }

        private void HandleSessionReconnected(object sender, RemoteDataEventArgs<Exception> eventArgs)
        {
            bool stateChange = false;
            lock (this.syncObject)
            {
                if (stateInfo.State == RunspacePoolState.Connecting)
                {
                    ResetDisconnectedOnExpiresOn();

                    SetRunspacePoolState(new RunspacePoolStateInfo(RunspacePoolState.Opened, null));
                    stateChange = true;
                }
            }

            // Do state change work outside of lock.
            if (stateChange)
            {
                RaiseStateChangeEvent(this.stateInfo);
                SetReconnectAsCompleted();
            }
        }

        private void SetReconnectAsCompleted()
        {
            if (_reconnectAsyncResult != null && !_reconnectAsyncResult.IsCompleted)
            {
                _reconnectAsyncResult.SetAsCompleted(stateInfo.Reason);
                _reconnectAsyncResult = null;
            }
        }

        /// <summary>
        /// The session is closing set the state and reason accordingly.
        /// </summary>
        /// <param name="sender">Sender of this event, unused.</param>
        /// <param name="eventArgs">Arguments describing this event.</param>
        private void HandleSessionClosing(object sender, RemoteDataEventArgs<Exception> eventArgs)
        {
            // just capture the reason for closing here..handle the session closed event
            // to change state appropriately.
            _closingReason = eventArgs.Data;
        }

        /// <summary>
        /// The session closed, set the state and reason accordingly.
        /// </summary>
        /// <param name="sender">Sender of this event, unused.</param>
        /// <param name="eventArgs">Arguments describing this event.</param>
        private void HandleSessionClosed(object sender, RemoteDataEventArgs<Exception> eventArgs)
        {
            if (eventArgs.Data != null)
            {
                _closingReason = eventArgs.Data;
            }

            // Set state under lock.
            RunspacePoolState prevState;
            RunspacePoolStateInfo finishedStateInfo;
            lock (syncObject)
            {
                prevState = stateInfo.State;

                switch (prevState)
                {
                    case RunspacePoolState.Opening:
                    case RunspacePoolState.Opened:
                    case RunspacePoolState.Disconnecting:
                    case RunspacePoolState.Disconnected:
                    case RunspacePoolState.Connecting:
                        // Since RunspacePool is not in closing state, this close is
                        // happening because of data structure handler error. Set the state to broken.
                        SetRunspacePoolState(new RunspacePoolStateInfo(RunspacePoolState.Broken, _closingReason));
                        break;

                    case RunspacePoolState.Closing:
                        SetRunspacePoolState(new RunspacePoolStateInfo(RunspacePoolState.Closed, _closingReason));
                        break;
                }

                finishedStateInfo = new RunspacePoolStateInfo(stateInfo.State, stateInfo.Reason);
            }

            // Raise notification event outside of lock.
            try
            {
                RaiseStateChangeEvent(finishedStateInfo);
            }
            catch (Exception)
            {
                // Don't throw exception on notification thread.
            }

            // Check if we have either an existing disconnect or connect async object
            // and if so make sure they are set to completed since this is a
            // final state for the runspace pool.
            SetDisconnectAsCompleted();
            SetReconnectAsCompleted();

            // Ensure an existing Close async object is completed.
            SetCloseAsCompleted();
        }

        /// <summary>
        /// Set the async result for open as completed.
        /// </summary>
        private void SetOpenAsCompleted()
        {
            RunspacePoolAsyncResult tempOpenAsyncResult = _openAsyncResult;
            _openAsyncResult = null;
            if (tempOpenAsyncResult != null && !tempOpenAsyncResult.IsCompleted)
            {
                tempOpenAsyncResult.SetAsCompleted(stateInfo.Reason);
            }
        }

        /// <summary>
        /// Set the async result for close as completed.
        /// </summary>
        private void SetCloseAsCompleted()
        {
            // abort all pending calls.
            DispatchTable.AbortAllCalls();

            if (_closeAsyncResult != null)
            {
                _closeAsyncResult.SetAsCompleted(stateInfo.Reason);
                _closeAsyncResult = null;
            }

            // Ensure that openAsyncResult is completed and that
            // any error is thrown on the calling thread.
            // The session can be closed at any time, including
            // during Open processing.
            SetOpenAsCompleted();

            // Ensure private application data wait is released.
            try
            {
                _applicationPrivateDataReceived.Set();
            }
            catch (ObjectDisposedException) { }
        }

        /// <summary>
        /// When a response to a SetMaxRunspaces or SetMinRunspaces is received,
        /// from the server, this method sets the response and thereby unblocks
        /// corresponding call.
        /// </summary>
        /// <param name="sender">Sender of this message, unused.</param>
        /// <param name="eventArgs">Contains response and call id.</param>
        private void HandleResponseReceived(object sender, RemoteDataEventArgs<PSObject> eventArgs)
        {
            PSObject data = eventArgs.Data;
            object response = RemotingDecoder.GetPropertyValue<object>(data, RemoteDataNameStrings.RunspacePoolOperationResponse);
            long callId = RemotingDecoder.GetPropertyValue<long>(data, RemoteDataNameStrings.CallId);
            DispatchTable.SetResponse(callId, response);
        }

        /// <summary>
        /// When the client remote session reports a URI redirection, this method will report the
        /// message to the user as a Warning using Host method calls.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="eventArgs"></param>
        private void HandleURIDirectionReported(object sender, RemoteDataEventArgs<Uri> eventArgs)
        {
            WSManConnectionInfo wsmanConnectionInfo = _connectionInfo as WSManConnectionInfo;
            if (wsmanConnectionInfo != null)
            {
                wsmanConnectionInfo.ConnectionUri = eventArgs.Data;
                URIRedirectionReported.SafeInvoke(this, eventArgs);
            }
        }

        /// <summary>
        /// When the server sends a PSEventArgs this method will add it to the local event queue.
        /// </summary>
        private void HandlePSEventArgsReceived(object sender, RemoteDataEventArgs<PSEventArgs> e)
        {
            OnForwardEvent(e.Data);
        }

        /// <summary>
        /// A session disconnect has been initiated by the WinRM robust connection layer.  Set
        /// internal state to Disconnecting.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandleSessionRCDisconnecting(object sender, RemoteDataEventArgs<Exception> e)
        {
            Dbg.Assert(this.stateInfo.State == RunspacePoolState.Opened,
                "RC disconnect should only occur for runspace pools in the Opened state.");

            lock (this.syncObject)
            {
                SetRunspacePoolState(new RunspacePoolStateInfo(RunspacePoolState.Disconnecting, e.Data));
            }

            RaiseStateChangeEvent(this.stateInfo);
        }

        /// <summary>
        /// The session has been successfully created.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="eventArgs"></param>
        private void HandleSessionCreateCompleted(object sender, CreateCompleteEventArgs eventArgs)
        {
            // Update connectionInfo with updated information from the transport.
            if (eventArgs != null)
            {
                _connectionInfo.IdleTimeout = eventArgs.ConnectionInfo.IdleTimeout;
                _connectionInfo.MaxIdleTimeout = eventArgs.ConnectionInfo.MaxIdleTimeout;
                WSManConnectionInfo wsmanConnectionInfo = _connectionInfo as WSManConnectionInfo;
                if (wsmanConnectionInfo != null)
                {
                    wsmanConnectionInfo.OutputBufferingMode =
                        ((WSManConnectionInfo)eventArgs.ConnectionInfo).OutputBufferingMode;
                }
            }

            // Forward event.
            SessionCreateCompleted.SafeInvoke<CreateCompleteEventArgs>(this, eventArgs);
        }

        private void ResetDisconnectedOnExpiresOn()
        {
            // Reset DisconnectedOn/ExpiresOn
            WSManConnectionInfo wsManConnectionInfo = _connectionInfo as WSManConnectionInfo;
            wsManConnectionInfo?.NullDisconnectedExpiresOn();
        }

        private void UpdateDisconnectedExpiresOn()
        {
            // Set DisconnectedOn/ExpiresOn for disconnected session.
            WSManConnectionInfo wsManConnectionInfo = _connectionInfo as WSManConnectionInfo;
            wsManConnectionInfo?.SetDisconnectedExpiresOnToNow();
        }

        /// <summary>
        /// Waits for application private data from server before raising
        /// event:  Connecting->Opened state changed event.
        /// </summary>
        /// <param name="state"></param>
        private void WaitAndRaiseConnectEventsProc(object state)
        {
            RunspacePoolStateInfo info = state as RunspacePoolStateInfo;
            Dbg.Assert(info != null, "State -> Event arguments cannot be null.");

            // Wait for private application data to arrive from server.
            try
            {
                _applicationPrivateDataReceived.WaitOne();
            }
            catch (ObjectDisposedException) { }

            // Raise state changed event.
            try
            {
                RaiseStateChangeEvent(info);
            }
            catch (Exception)
            {
            }

            // Set Opened async object.
            SetOpenAsCompleted();
        }

        #endregion Private Methods

        #region Private Members

        private readonly RunspaceConnectionInfo _connectionInfo;     // connection info with which this
        // runspace is created
        // data structure handler handling
        private RunspacePoolAsyncResult _openAsyncResult; // async result object generated on
        // CoreOpen
        private RunspacePoolAsyncResult _closeAsyncResult; // async result object generated by
        // BeginClose
        private Exception _closingReason;                       // reason for a Closing state transition
        private RunspacePoolAsyncResult _disconnectAsyncResult; // async result object generated on CoreDisconnect
        private RunspacePoolAsyncResult _reconnectAsyncResult;  // async result object generated on CoreReconnect
        private bool _isDisposed;

        private DispatchTable<object> DispatchTable { get; }

        private bool _canReconnect;
        private string _friendlyName = string.Empty;

        private readonly System.Collections.Concurrent.ConcurrentStack<PowerShell> _runningPowerShells;

        #endregion Private Members

        #region IDisposable

        /// <summary>
        /// Public method for Dispose.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);

            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Release all resources.
        /// </summary>
        /// <param name="disposing">If true, release all managed resources.</param>
        public override void Dispose(bool disposing)
        {
            // dispose the base class before disposing dataStructure handler.
            base.Dispose(disposing);
            if (!_isDisposed)
            {
                _isDisposed = true;
                DataStructureHandler.Dispose(disposing);
                _applicationPrivateDataReceived.Dispose();
            }
        }

        #endregion IDisposable
    }

    #region ConnectCommandInfo class

    /// <summary>
    /// Class defining a remote command to connect to.
    /// </summary>
    internal class ConnectCommandInfo
    {
        /// <summary>
        /// Remote command instance Id.
        /// </summary>
        public Guid CommandId { get; } = Guid.Empty;

        /// <summary>
        /// Remote command string.
        /// </summary>
        public string Command { get; } = string.Empty;

        /// <summary>
        /// Constructs a remote command object.
        /// </summary>
        /// <param name="cmdId">Command instance Id.</param>
        /// <param name="cmdStr">Command string.</param>
        public ConnectCommandInfo(Guid cmdId, string cmdStr)
        {
            CommandId = cmdId;
            Command = cmdStr;
        }
    }

    #endregion

    #region RemoteRunspacePoolEnumeration class

    /// <summary>
    /// Enumerates remote runspacepools (Shells) and running commands
    /// using Get-WSManInstance cmdlet.
    /// </summary>
    internal static class RemoteRunspacePoolEnumeration
    {
        /// <summary>
        /// Gets an array of XmlElement objects representing all
        /// disconnected runspace pools on the indicated server.
        /// </summary>
        /// <param name="wsmanConnectionInfo">Specifies the remote server to connect to.</param>]
        /// <returns>Collection of XmlElement objects.</returns>
        internal static Collection<PSObject> GetRemotePools(WSManConnectionInfo wsmanConnectionInfo)
        {
            Collection<PSObject> result;
            using (PowerShell powerShell = PowerShell.Create())
            {
                // Enumerate remote runspaces using the Get-WSManInstance cmdlet.
                powerShell.AddCommand("Get-WSManInstance");

                // Add parameters to enumerate Shells (runspace pools).
                powerShell.AddParameter("ResourceURI", "Shell");
                powerShell.AddParameter("Enumerate", true);

                // Add parameters for server connection.
                powerShell.AddParameter("ComputerName", wsmanConnectionInfo.ComputerName);
                powerShell.AddParameter("Authentication", ConvertPSAuthToWSManAuth(wsmanConnectionInfo.AuthenticationMechanism));
                if (wsmanConnectionInfo.Credential != null)
                {
                    powerShell.AddParameter("Credential", wsmanConnectionInfo.Credential);
                }

                if (wsmanConnectionInfo.CertificateThumbprint != null)
                {
                    powerShell.AddParameter("CertificateThumbprint", wsmanConnectionInfo.CertificateThumbprint);
                }

                if (wsmanConnectionInfo.PortSetting != -1)
                {
                    powerShell.AddParameter("Port", wsmanConnectionInfo.Port);
                }

                if (CheckForSSL(wsmanConnectionInfo))
                {
                    powerShell.AddParameter("UseSSL", true);
                }

                if (!string.IsNullOrEmpty(wsmanConnectionInfo.AppName))
                {
                    // Remove prepended path character.
                    string appName = wsmanConnectionInfo.AppName.TrimStart('/');
                    powerShell.AddParameter("ApplicationName", appName);
                }

                powerShell.AddParameter("SessionOption", GetSessionOptions(wsmanConnectionInfo));

                result = powerShell.Invoke();
            }

            return result;
        }

        /// <summary>
        /// Gets an array of XmlElement objects representing each running command
        /// on the specified runspace pool with the shellid Guid.
        /// </summary>
        /// <param name="shellId">Guid of shellId (runspacepool Id).</param>
        /// <param name="wsmanConnectionInfo">Specifies the remote server to connect to.</param>]
        /// <returns>Collection of XmlElement objects.</returns>
        internal static Collection<PSObject> GetRemoteCommands(Guid shellId, WSManConnectionInfo wsmanConnectionInfo)
        {
            Collection<PSObject> result;
            using (PowerShell powerShell = PowerShell.Create())
            {
                // Enumerate remote runspace commands using the Get-WSManInstance cmdlet.
                powerShell.AddCommand("Get-WSManInstance");

                // Add parameters to enumerate commands.
                string filterStr = string.Create(CultureInfo.InvariantCulture, $"ShellId='{shellId.ToString().ToUpperInvariant()}'");
                powerShell.AddParameter("ResourceURI", @"Shell/Command");
                powerShell.AddParameter("Enumerate", true);
                powerShell.AddParameter("Dialect", "Selector");
                powerShell.AddParameter("Filter", filterStr);

                // Add parameters for server connection.
                powerShell.AddParameter("ComputerName", wsmanConnectionInfo.ComputerName);
                powerShell.AddParameter("Authentication", ConvertPSAuthToWSManAuth(wsmanConnectionInfo.AuthenticationMechanism));
                if (wsmanConnectionInfo.Credential != null)
                {
                    powerShell.AddParameter("Credential", wsmanConnectionInfo.Credential);
                }

                if (wsmanConnectionInfo.CertificateThumbprint != null)
                {
                    powerShell.AddParameter("CertificateThumbprint", wsmanConnectionInfo.CertificateThumbprint);
                }

                if (wsmanConnectionInfo.PortSetting != -1)
                {
                    powerShell.AddParameter("Port", wsmanConnectionInfo.Port);
                }

                if (CheckForSSL(wsmanConnectionInfo))
                {
                    powerShell.AddParameter("UseSSL", true);
                }

                if (!string.IsNullOrEmpty(wsmanConnectionInfo.AppName))
                {
                    // Remove prepended path character.
                    string appName = wsmanConnectionInfo.AppName.TrimStart('/');
                    powerShell.AddParameter("ApplicationName", appName);
                }

                powerShell.AddParameter("SessionOption", GetSessionOptions(wsmanConnectionInfo));

                result = powerShell.Invoke();
            }

            return result;
        }

        /// <summary>
        /// Use the WSMan New-WSManSessionOption cmdlet to create a session options
        /// object used for Get-WSManInstance queries.
        /// </summary>
        /// <param name="wsmanConnectionInfo">WSManConnectionInfo.</param>
        /// <returns>WSMan session options object.</returns>
        private static object GetSessionOptions(WSManConnectionInfo wsmanConnectionInfo)
        {
            Collection<PSObject> result;
            using (PowerShell powerShell = PowerShell.Create())
            {
                powerShell.AddCommand("New-WSManSessionOption");

                if (wsmanConnectionInfo.ProxyAccessType != ProxyAccessType.None)
                {
                    powerShell.AddParameter("ProxyAccessType", "Proxy" + wsmanConnectionInfo.ProxyAccessType.ToString());
                    powerShell.AddParameter("ProxyAuthentication", wsmanConnectionInfo.ProxyAuthentication.ToString());

                    if (wsmanConnectionInfo.ProxyCredential != null)
                    {
                        powerShell.AddParameter("ProxyCredential", wsmanConnectionInfo.ProxyCredential);
                    }
                }

                // New-WSManSessionOption uses the SPNPort number here to enable SPN
                // server authentication.  It looks like any value > 0 will enable
                // this.  Since the Port property always returns a valid port value (>0)
                // just pass the WSManConnectionInfo port parameter.
                if (wsmanConnectionInfo.IncludePortInSPN)
                {
                    powerShell.AddParameter("SPNPort", wsmanConnectionInfo.Port);
                }

                powerShell.AddParameter("SkipCACheck", wsmanConnectionInfo.SkipCACheck);
                powerShell.AddParameter("SkipCNCheck", wsmanConnectionInfo.SkipCNCheck);
                powerShell.AddParameter("SkipRevocationCheck", wsmanConnectionInfo.SkipRevocationCheck);

                powerShell.AddParameter("OperationTimeout", wsmanConnectionInfo.OperationTimeout);
                powerShell.AddParameter("NoEncryption", wsmanConnectionInfo.NoEncryption);
                powerShell.AddParameter("UseUTF16", wsmanConnectionInfo.UseUTF16);

                result = powerShell.Invoke();
            }

            return result[0].BaseObject;
        }

        private static bool CheckForSSL(WSManConnectionInfo wsmanConnectionInfo)
        {
            return (!string.IsNullOrEmpty(wsmanConnectionInfo.Scheme) &&
                    wsmanConnectionInfo.Scheme.Contains(WSManConnectionInfo.HttpsScheme, StringComparison.OrdinalIgnoreCase));
        }

        private static int ConvertPSAuthToWSManAuth(AuthenticationMechanism psAuth)
        {
            int wsmanAuth;

            switch (psAuth)
            {
                case AuthenticationMechanism.Default:
                    wsmanAuth = 0x1;
                    break;

                case AuthenticationMechanism.Basic:
                    wsmanAuth = 0x8;
                    break;

                case AuthenticationMechanism.Digest:
                    wsmanAuth = 0x2;
                    break;

                case AuthenticationMechanism.Credssp:
                    wsmanAuth = 0x80;
                    break;

                case AuthenticationMechanism.Kerberos:
                    wsmanAuth = 0x10;
                    break;

                case AuthenticationMechanism.Negotiate:
                    wsmanAuth = 0x4;
                    break;

                default:
                    wsmanAuth = 0x1;
                    break;
            }

            return wsmanAuth;
        }
    }

    #endregion
}
