/// <summary> /// Returns an existing, or gets a new connection using the details in the specified /// <paramref name="connectToPeerResponse"/> and pierces the remote peer's firewall. /// </summary> /// <remarks> /// This method will be invoked from <see cref="Messaging.Handlers.ServerMessageHandler"/> upon receipt of an /// unsolicited <see cref="ConnectToPeerResponse"/> of type 'P' only. This connection should only be initiated if /// there is no existing connection; superseding should be avoided if possible. /// </remarks> /// <param name="connectToPeerResponse">The response that solicited the connection.</param> /// <returns>The operation context, including the new or updated connection.</returns> public async Task <IMessageConnection> GetOrAddMessageConnectionAsync(ConnectToPeerResponse connectToPeerResponse) { bool cached = true; var r = connectToPeerResponse; try { var connection = await MessageConnectionDictionary.GetOrAdd( r.Username, key => new Lazy <Task <IMessageConnection> >(() => GetConnection())).Value.ConfigureAwait(false); if (cached) { Diagnostic.Debug($"Retrieved cached message connection to {r.Username} ({r.IPEndPoint}) (type: {connection.Type}, id: {connection.Id})"); } return(connection); } catch (Exception ex) { var msg = $"Failed to establish an inbound indirect message connection to {r.Username} ({r.IPEndPoint}): {ex.Message}"; Diagnostic.Debug(msg); Diagnostic.Debug($"Purging message connection cache of failed connection to {r.Username} ({r.IPEndPoint})."); MessageConnectionDictionary.TryRemove(r.Username, out _); throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection() { cached = false; Diagnostic.Debug($"Attempting inbound indirect message connection to {r.Username} ({r.IPEndPoint}) for token {r.Token}"); var connection = ConnectionFactory.GetMessageConnection( r.Username, r.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Indirect; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.MessageReceived += SoulseekClient.PeerMessageHandler.HandleMessageReceived; connection.MessageWritten += SoulseekClient.PeerMessageHandler.HandleMessageWritten; connection.Disconnected += MessageConnection_Disconnected; using (var cts = new CancellationTokenSource()) { // add a record to the pending dictionary so we can tell whether the following code is waiting PendingInboundIndirectConnectionDictionary.AddOrUpdate(r.Username, cts, (username, existingCts) => cts); try { await connection.ConnectAsync(cts.Token).ConfigureAwait(false); var request = new PierceFirewall(r.Token); await connection.WriteAsync(request, cts.Token).ConfigureAwait(false); } catch { connection.Dispose(); throw; } finally { // let everyone know this code is done executing and that .Value of the containing cache is safe to await with no delay. PendingInboundIndirectConnectionDictionary.TryRemove(r.Username, out _); } } Diagnostic.Debug($"Message connection to {r.Username} ({r.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }
/// <summary> /// Returns an existing, or gets a new connection using the details in the specified /// <paramref name="connectToPeerResponse"/> and pierces the remote peer's firewall. /// </summary> /// <remarks> /// This method will be invoked from <see cref="Messaging.Handlers.ServerMessageHandler"/> upon receipt of an /// unsolicited <see cref="ConnectToPeerResponse"/> of type 'P' only. This connection should only be initiated if /// there is no existing connection; superseding should be avoided if possible. /// </remarks> /// <param name="connectToPeerResponse">The response that solicited the connection.</param> /// <returns>The operation context, including the new or updated connection.</returns> public async Task <IMessageConnection> GetOrAddMessageConnectionAsync(ConnectToPeerResponse connectToPeerResponse) { bool cached = true; var r = connectToPeerResponse; try { var connection = await MessageConnectionDictionary.GetOrAdd( r.Username, key => new Lazy <Task <IMessageConnection> >(() => GetConnection())).Value.ConfigureAwait(false); if (cached) { Diagnostic.Debug($"Retrieved cached message connection to {r.Username} ({r.IPEndPoint}) (type: {connection.Type}, id: {connection.Id})"); } return(connection); } catch (Exception ex) { var msg = $"Failed to establish an inbound indirect message connection to {r.Username} ({r.IPEndPoint}): {ex.Message}"; Diagnostic.Debug(msg); // only purge the connection if the thrown exception is something other than OperationCanceledException. if this // is thrown then a direct connection superseded this connection while it was being established, and // ChildConnectionDictionary contains the new, direct connection. if (!(ex is OperationCanceledException)) { Diagnostic.Debug($"Purging message connection cache of failed connection to {r.Username} ({r.IPEndPoint})."); // remove the current record, which *should* be the one we added above. MessageConnectionDictionary.TryRemove(r.Username, out var removed); try { var connection = await removed.Value.ConfigureAwait(false); // if the connection we removed is Direct, then a direct connection managed to come in after this attempt // had timed out or failed, but before that connection was able to cancel the pending token this should be // an extreme edge case, but log it as a warning so we can see how common it is. if (connection.Type.HasFlag(ConnectionTypes.Direct)) { Diagnostic.Warning($"Erroneously purged direct message connection to {r.Username} upon indirect failure"); MessageConnectionDictionary.TryAdd(r.Username, removed); } } catch { // noop } } throw new ConnectionException(msg, ex); } async Task <IMessageConnection> GetConnection() { cached = false; Diagnostic.Debug($"Attempting inbound indirect message connection to {r.Username} ({r.IPEndPoint}) for token {r.Token}"); var connection = ConnectionFactory.GetMessageConnection( r.Username, r.IPEndPoint, SoulseekClient.Options.PeerConnectionOptions); connection.Type = ConnectionTypes.Inbound | ConnectionTypes.Indirect; connection.MessageRead += SoulseekClient.PeerMessageHandler.HandleMessageRead; connection.MessageReceived += SoulseekClient.PeerMessageHandler.HandleMessageReceived; connection.MessageWritten += SoulseekClient.PeerMessageHandler.HandleMessageWritten; using (var cts = new CancellationTokenSource()) { // add a record to the pending dictionary so we can tell whether the following code is waiting PendingInboundIndirectConnectionDictionary.AddOrUpdate(r.Username, cts, (username, existingCts) => cts); try { await connection.ConnectAsync(cts.Token).ConfigureAwait(false); var request = new PierceFirewall(r.Token); await connection.WriteAsync(request.ToByteArray(), cts.Token).ConfigureAwait(false); } catch { connection.Dispose(); throw; } finally { // let everyone know this code is done executing and that .Value of the containing cache is safe to await // with no delay. PendingInboundIndirectConnectionDictionary.TryRemove(r.Username, out _); } } connection.Disconnected += MessageConnection_Disconnected; Diagnostic.Debug($"Message connection to {r.Username} ({r.IPEndPoint}) established. (type: {connection.Type}, id: {connection.Id})"); return(connection); } }