/// <summary> /// Sends the pending response matching the specified <paramref name="responseToken"/>, if one exists. /// </summary> /// <remarks> /// This overload is called by the listener when an incoming connection is established with a pierce firewall token, /// and if that token doesn't match a pending solicitation, and if the token matches a cached search response. In this case, /// the connection is retrieved from the cache and used to send the response. /// </remarks> /// <param name="responseToken">The token matching the pending response to send.</param> /// <returns>The operation context, including a value indicating whether a response was successfully sent.</returns> public async Task <bool> TryRespondAsync(int responseToken) { if (SoulseekClient.Options.SearchResponseCache != default) { bool cached; (string Username, int Token, string Query, SearchResponse SearchResponse)record; try { cached = SoulseekClient.Options.SearchResponseCache.TryRemove(responseToken, out record); } catch (Exception ex) { Diagnostic.Warning($"Error retrieving cached search response {responseToken}: {ex.Message}", ex); return(false); } if (cached) { var(username, token, query, searchResponse) = record; try { var peerConnection = await SoulseekClient.PeerConnectionManager.GetCachedMessageConnectionAsync(username).ConfigureAwait(false); await peerConnection.WriteAsync(searchResponse.ToByteArray()).ConfigureAwait(false); Diagnostic.Debug($"Sent cached response {responseToken} containing {searchResponse.FileCount + searchResponse.LockedFileCount} files to {username} for query '{query}' with token {token}"); ResponseDelivered?.Invoke(this, new SearchRequestResponseEventArgs(username, token, query, searchResponse)); return(true); } catch (Exception ex) { Diagnostic.Debug($"Failed to send cached search response {responseToken} to {username} for query '{query}' with token {token}: {ex.Message}", ex); ResponseDeliveryFailed?.Invoke(this, new SearchRequestResponseEventArgs(username, token, query, searchResponse)); } } } return(false); }
/// <summary> /// Responds to the given search request, if a response could be resolved and matche(s) were found. /// </summary> /// <param name="username">The username of the requesting user.</param> /// <param name="token">The token for the search request.</param> /// <param name="query">The search query.</param> /// <returns>The operation context, including a value indicating whether a response was successfully sent.</returns> public async Task <bool> TryRespondAsync(string username, int token, string query) { RequestReceived?.Invoke(this, new SearchRequestEventArgs(username, token, query)); if (SoulseekClient.Options.SearchResponseResolver == default) { return(false); } SearchResponse searchResponse = null; try { searchResponse = await SoulseekClient.Options.SearchResponseResolver(username, token, SearchQuery.FromText(query)).ConfigureAwait(false); } catch (Exception ex) { Diagnostic.Warning($"Error resolving search response for query '{query}' requested by {username} with token {token}: {ex.Message}", ex); return(false); } if (searchResponse == null || searchResponse.FileCount + searchResponse.LockedFileCount <= 0) { return(false); } try { Diagnostic.Debug($"Resolved {searchResponse.FileCount} files for query '{query}' with token {token} from {username}"); var endpoint = await SoulseekClient.GetUserEndPointAsync(username).ConfigureAwait(false); var responseToken = SoulseekClient.GetNextToken(); IMessageConnection peerConnection = default; try { // attempt to connect and send the results immediately. either a direct connection succeeds, or a user // responds to a solicited connection request prior to the configured connection timeout. peerConnection = await SoulseekClient.PeerConnectionManager.GetOrAddMessageConnectionAsync(username, endpoint, solicitationToken : responseToken, CancellationToken.None).ConfigureAwait(false); } catch { // direct connection failed, and user did not respond to the solicited connection request before the timeout, // but may respond later. cache the result along with the solicitation token that was sent so we can attempt a // "second chance" delivery of results if (SoulseekClient.Options.SearchResponseCache != default) { try { SoulseekClient.Options.SearchResponseCache.AddOrUpdate(responseToken, (username, token, query, searchResponse)); Diagnostic.Debug($"Failed to connect to {username} with solicitation token {responseToken} to deliver search results for query '{query}' with token {token}. Cached response for potential delayed delivery."); } catch (Exception ex) { Diagnostic.Warning($"Error caching undelivered search response {responseToken} for query '{query}' requested by {username} with token {token}: {ex.Message}", ex); } } throw; } await peerConnection.WriteAsync(searchResponse.ToByteArray()).ConfigureAwait(false); Diagnostic.Debug($"Sent response containing {searchResponse.FileCount + searchResponse.LockedFileCount} files to {username} for query '{query}' with token {token}"); ResponseDelivered?.Invoke(this, new SearchRequestResponseEventArgs(username, token, query, searchResponse)); return(true); } catch (Exception ex) { Diagnostic.Debug($"Failed to send search response to {username} for query '{query}' with token {token}: {ex.Message}", ex); } return(false); }