/// <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); } }
/// <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); } }