/// <summary>
        ///     Adds a new message connection from an incoming connection.
        /// </summary>
        /// <remarks>
        ///     This method will be invoked from <see cref="ListenerHandler"/> upon receipt of an incoming 'P' connection only.
        ///     Because this connection is fully established by the time it is passed to this method, it must supersede any cached
        ///     connection, as it will be the most recently established connection as tracked by the remote user.
        /// </remarks>
        /// <param name="username">The username of the user from which the connection originated.</param>
        /// <param name="incomingConnection">The the accepted connection.</param>
        /// <returns>The operation context.</returns>
        public async Task AddOrUpdateMessageConnectionAsync(string username, IConnection incomingConnection)
        {
            var c = incomingConnection;

            try
            {
                await MessageConnectionDictionary.AddOrUpdate(
                    username,
                    new Lazy <Task <IMessageConnection> >(() => GetConnection()),
                    (key, cachedConnectionRecord) => new Lazy <Task <IMessageConnection> >(() => GetConnection(cachedConnectionRecord))).Value.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                var msg = $"Failed to establish an inbound message connection to {username} ({c.IPEndPoint}): {ex.Message}";
                Diagnostic.Debug($"{msg} (type: {c.Type}, id: {c.Id})");
                Diagnostic.Debug($"Purging message connection cache of failed connection to {username} ({c.IPEndPoint}).");
                MessageConnectionDictionary.TryRemove(username, out _);
                throw new ConnectionException(msg, ex);
            }

            async Task <IMessageConnection> GetConnection(Lazy <Task <IMessageConnection> > cachedConnectionRecord = null)
            {
                Diagnostic.Debug($"Inbound message connection to {username} ({c.IPEndPoint}) accepted. (type: {c.Type}, id: {c.Id})");

                var connection = ConnectionFactory.GetMessageConnection(
                    username,
                    c.IPEndPoint,
                    SoulseekClient.Options.PeerConnectionOptions,
                    c.HandoffTcpClient());

                Diagnostic.Debug($"Inbound message connection to {username} ({connection.IPEndPoint}) handed off. (old: {c.Id}, new: {connection.Id})");
                c.Dispose();

                connection.Type             = ConnectionTypes.Inbound | ConnectionTypes.Direct;
                connection.MessageRead     += SoulseekClient.PeerMessageHandler.HandleMessageRead;
                connection.MessageReceived += SoulseekClient.PeerMessageHandler.HandleMessageReceived;
                connection.MessageWritten  += SoulseekClient.PeerMessageHandler.HandleMessageWritten;
                connection.Disconnected    += MessageConnection_Disconnected;

                if (cachedConnectionRecord != null)
                {
                    if (PendingInboundIndirectConnectionDictionary.TryGetValue(username, out var pendingCts))
                    {
                        // cancel any connection pending due to a ConnectToPeer message; we don't want it to succeed because the
                        // remote client would supersede this connection with it.
                        Diagnostic.Debug($"Cancelling pending inbound indirect message connection to {username}");
                        pendingCts.Cancel();
                    }

                    try
                    {
                        // because we cancelled any pending connection above, the Lazy<> function has completed executing and we
                        // know that awaiting .Value will return immediately, allowing us to tear down the existing connection.
                        var cachedConnection = await cachedConnectionRecord.Value.ConfigureAwait(false);

                        cachedConnection.Disconnected -= MessageConnection_Disconnected;
                        Diagnostic.Debug($"Superseding cached message connection to {username} ({cachedConnection.IPEndPoint}) (old: {cachedConnection.Id}, new: {connection.Id}");
                    }
                    catch
                    {
                        // noop
                    }
                }

                try
                {
                    connection.StartReadingContinuously();
                }
                catch
                {
                    connection.Dispose();
                    throw;
                }

                Diagnostic.Debug($"Message connection to {username} ({connection.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})");
                return(connection);
            }
        }
        /// <summary>
        ///     Adds a new message connection from an incoming connection.
        /// </summary>
        /// <remarks>
        ///     This method will be invoked from <see cref="ListenerHandler"/> upon receipt of an incoming unsolicited message
        ///     only. Because this connection is fully established by the time it is passed to this method, it must supersede any
        ///     cached connection, as it will be the most recently established connection as tracked by the remote user.
        /// </remarks>
        /// <param name="username">The username of the user from which the connection originated.</param>
        /// <param name="incomingConnection">The the accepted connection.</param>
        /// <returns>The operation context.</returns>
        public async Task AddMessageConnectionAsync(string username, IConnection incomingConnection)
        {
            var c = incomingConnection;

            try
            {
                await MessageConnectionDictionary.AddOrUpdate(
                    username,
                    new Lazy <Task <IMessageConnection> >(() => GetConnection()),
                    (key, cachedConnectionRecord) => new Lazy <Task <IMessageConnection> >(() => GetConnection(cachedConnectionRecord))).Value.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                var msg = $"Failed to establish an inbound message connection to {username} ({c.IPEndPoint}): {ex.Message}";
                Diagnostic.Debug($"{msg} (type: {c.Type}, id: {c.Id})");
                Diagnostic.Debug($"Purging message connection cache of failed connection to {username} ({c.IPEndPoint}).");
                MessageConnectionDictionary.TryRemove(username, out _);
                throw new ConnectionException(msg, ex);
            }

            async Task <IMessageConnection> GetConnection(Lazy <Task <IMessageConnection> > cachedConnectionRecord = null)
            {
                Diagnostic.Debug($"Inbound message connection to {username} ({c.IPEndPoint}) accepted. (type: {c.Type}, id: {c.Id})");

                var connection = ConnectionFactory.GetMessageConnection(
                    username,
                    c.IPEndPoint,
                    SoulseekClient.Options.PeerConnectionOptions,
                    c.HandoffTcpClient());

                connection.Type            = ConnectionTypes.Inbound | ConnectionTypes.Direct;
                connection.MessageRead    += SoulseekClient.PeerMessageHandler.HandleMessageRead;
                connection.MessageWritten += SoulseekClient.PeerMessageHandler.HandleMessageWritten;
                connection.Disconnected   += MessageConnection_Disconnected;

                Diagnostic.Debug($"Inbound message connection to {username} ({connection.IPEndPoint}) handed off. (old: {c.Id}, new: {connection.Id})");

                if (cachedConnectionRecord != null)
                {
                    // because the cache is Lazy<>, the cached entry may be either a connected or pending connection.
                    // if we try to reference .Value before the cached function is dispositioned we'll get stuck waiting for it,
                    // which will prevent this code from superseding the connection until the pending connection times out.
                    // to get around this the pending connection dictionary was added, allowing us to tell if the connection is still pending.
                    // if so, we can just cancel the token and move on.
                    if (PendingInboundIndirectConnectionDictionary.TryGetValue(username, out var pendingCts))
                    {
                        Diagnostic.Debug($"Cancelling pending inbound indirect message connection to {username}");
                        pendingCts.Cancel();
                    }
                    else
                    {
                        // if there's no entry in the pending connection dictionary, the Lazy<> function has completed executing and we know that
                        // awaiting .Value will return immediately, allowing us to tear down the disconnected event handler.
                        try
                        {
                            var cachedConnection = await cachedConnectionRecord.Value.ConfigureAwait(false);

                            cachedConnection.Disconnected -= MessageConnection_Disconnected;
                            Diagnostic.Debug($"Superseding cached message connection to {username} ({cachedConnection.IPEndPoint}) (old: {cachedConnection.Id}, new: {connection.Id}");
                        }
                        catch
                        {
                            // noop
                        }
                    }
                }

                connection.StartReadingContinuously();

                Diagnostic.Debug($"Message connection to {username} ({connection.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})");
                return(connection);
            }
        }
Example #3
0
        /// <summary>
        ///     Adds a new message connection from an incoming connection.
        /// </summary>
        /// <remarks>
        ///     This method will be invoked from <see cref="ListenerHandler"/> upon receipt of an incoming unsolicited message
        ///     only. Because this connection is fully established by the time it is passed to this method, it must supersede any
        ///     cached connection, as it will be the most recently established connection as tracked by the remote user.
        /// </remarks>
        /// <param name="username">The username of the user from which the connection originated.</param>
        /// <param name="incomingConnection">The the accepted connection.</param>
        /// <returns>The operation context.</returns>
        public async Task AddMessageConnectionAsync(string username, IConnection incomingConnection)
        {
            var c = incomingConnection;

            try
            {
                await MessageConnectionDictionary.AddOrUpdate(
                    username,
                    new Lazy <Task <IMessageConnection> >(() => GetConnection()),
                    (key, cachedConnectionRecord) => new Lazy <Task <IMessageConnection> >(() => GetConnection(cachedConnectionRecord))).Value.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                var msg = $"Failed to establish an inbound message connection to {username} ({c.IPEndPoint}): {ex.Message}";
                Diagnostic.Debug($"{msg} (type: {c.Type}, id: {c.Id})");
                Diagnostic.Debug($"Purging message connection cache of failed connection to {username} ({c.IPEndPoint}).");
                MessageConnectionDictionary.TryRemove(username, out _);
                throw new ConnectionException(msg, ex);
            }

            async Task <IMessageConnection> GetConnection(Lazy <Task <IMessageConnection> > cachedConnectionRecord = null)
            {
                Diagnostic.Debug($"Inbound message connection to {username} ({c.IPEndPoint}) accepted. (type: {c.Type}, id: {c.Id})");

                var connection = ConnectionFactory.GetMessageConnection(
                    username,
                    c.IPEndPoint,
                    SoulseekClient.Options.PeerConnectionOptions,
                    c.HandoffTcpClient());

                connection.Type          = ConnectionTypes.Inbound | ConnectionTypes.Direct;
                connection.MessageRead  += SoulseekClient.PeerMessageHandler.HandleMessageRead;
                connection.Disconnected += MessageConnection_Disconnected;

                Diagnostic.Debug($"Inbound message connection to {username} ({connection.IPEndPoint}) handed off. (old: {c.Id}, new: {connection.Id})");

                if (cachedConnectionRecord != null)
                {
                    if (PendingInboundIndirectConnectionDictionary.TryGetValue(username, out var pendingCts))
                    {
                        Diagnostic.Debug($"Cancelling pending inbound indirect message connection to {username}");
                        pendingCts.Cancel();
                    }
                    else
                    {
                        try
                        {
                            var cachedConnection = await cachedConnectionRecord.Value.ConfigureAwait(false);

                            cachedConnection.Disconnected -= MessageConnection_Disconnected;
                            Diagnostic.Debug($"Superseding cached message connection to {username} ({cachedConnection.IPEndPoint}) (old: {cachedConnection.Id}, new: {connection.Id}");
                        }
                        catch
                        {
                            // noop
                        }
                    }
                }

                connection.StartReadingContinuously();

                Diagnostic.Debug($"Message connection to {username} ({connection.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})");
                return(connection);
            }
        }