From 1071357131fe4f415eb14933bce405cf6b8fad8c Mon Sep 17 00:00:00 2001 From: JP Dillingham Date: Sun, 5 Jul 2026 14:41:12 -0500 Subject: [PATCH 1/5] default governor to null --- src/Network/Tcp/Connection.cs | 2 +- src/Network/Tcp/IConnection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Network/Tcp/Connection.cs b/src/Network/Tcp/Connection.cs index 7b90f2019..883c610e8 100644 --- a/src/Network/Tcp/Connection.cs +++ b/src/Network/Tcp/Connection.cs @@ -418,7 +418,7 @@ public Task ReadAsync(long length, CancellationToken? cancellationToken /// is not connected. /// /// Thrown when an unexpected error occurs. - public Task ReadAsync(long length, Stream outputStream, Func> governor, Action reporter = null, CancellationToken? cancellationToken = null) + public Task ReadAsync(long length, Stream outputStream, Func> governor = null, Action reporter = null, CancellationToken? cancellationToken = null) { if (length < 0) { diff --git a/src/Network/Tcp/IConnection.cs b/src/Network/Tcp/IConnection.cs index d5028f36d..bd9d6083d 100644 --- a/src/Network/Tcp/IConnection.cs +++ b/src/Network/Tcp/IConnection.cs @@ -165,7 +165,7 @@ internal interface IConnection : IDisposable /// is not connected. /// /// Thrown when an unexpected error occurs. - Task ReadAsync(long length, Stream outputStream, Func> governor, Action reporter = null, CancellationToken? cancellationToken = null); + Task ReadAsync(long length, Stream outputStream, Func> governor = null, Action reporter = null, CancellationToken? cancellationToken = null); /// /// Waits for the connection to disconnect, returning the message or throwing the Exception which caused the disconnect. From 135b97ef286c8352b181f228c1fc4ad1643970fe Mon Sep 17 00:00:00 2001 From: JP Dillingham Date: Sun, 5 Jul 2026 14:41:39 -0500 Subject: [PATCH 2/5] add RegisterMessagingHandlingOverride --- src/Network/IMessageConnection.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Network/IMessageConnection.cs b/src/Network/IMessageConnection.cs index ca8384cf3..fd94a6f66 100644 --- a/src/Network/IMessageConnection.cs +++ b/src/Network/IMessageConnection.cs @@ -24,6 +24,7 @@ namespace Soulseek.Network { using System; + using System.IO; using System.Threading; using System.Threading.Tasks; using Soulseek.Messaging.Messages; @@ -84,6 +85,21 @@ internal interface IMessageConnection : IConnection /// string Username { get; } + /// + /// Registers an override for handling of the specified , which will divert the + /// received data packets to the specified instead of the attached message handler, + /// and will invoke the specified when the message has been fully recieved. + /// + /// + /// Registrations are added to a FIFO queue internally, and messages will be streamed to handlers in the order + /// they are registered and received. There is no way to guarantee that the remote client will respond in + /// chronological order, so avoid using this for messages that are variable in this way (e.g. search responses). + /// + /// The message code of the message for which to override handling. + /// The stream to write the message data to. + /// The callback to invoke when the message has been fully received. + void RegisterMessageHandlingOverride(int messageCode, Stream stream, Action callback); + /// /// Begins the internal continuous read loop, if it has not yet started. /// From 644775377ca6b30ecd6bd671f3343f7d157a632c Mon Sep 17 00:00:00 2001 From: JP Dillingham Date: Sun, 5 Jul 2026 14:42:00 -0500 Subject: [PATCH 3/5] implement RegisterMessageHandlingOverride --- src/Network/MessageConnection.cs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Network/MessageConnection.cs b/src/Network/MessageConnection.cs index 34d88e087..03758d7ae 100644 --- a/src/Network/MessageConnection.cs +++ b/src/Network/MessageConnection.cs @@ -140,6 +140,33 @@ internal MessageConnection(IPEndPoint ipEndPoint, ConnectionOptions options = nu /// public string Username { get; } = string.Empty; + private ConcurrentDictionary> MessageHandlingOverrideRegistrations { get; } = new ConcurrentDictionary>(); + + /// + /// Registers an override for handling of the specified , which will divert the + /// received data packets to the specified instead of the attached message handler, + /// and will invoke the specified when the message has been fully recieved. + /// + /// + /// Registrations are added to a FIFO queue internally, and messages will be streamed to handlers in the order + /// they are registered and received. There is no way to guarantee that the remote client will respond in + /// chronological order, so avoid using this for messages that are variable in this way (e.g. search responses). + /// + /// The message code of the message for which to override handling. + /// The stream to write the message data to. + /// The callback to invoke when the message has been fully received. + public void RegisterMessageHandlingOverride(int messageCode, Stream stream, Action callback) + { + MessageHandlingOverrideRegistrations.AddOrUpdate( + key: messageCode, + addValue: new ConcurrentQueue<(Stream Stream, Action Callback)>(new[] { (stream, callback) }), + updateValueFactory: (k, v) => + { + v.Enqueue((stream, callback)); + return v; + }); + } + /// /// Begins the internal continuous read loop, if it has not yet started. /// From 92cb9c0eb0eea6c184dfa8ef847a5bf8f19bf4aa Mon Sep 17 00:00:00 2001 From: JP Dillingham Date: Sun, 5 Jul 2026 14:42:43 -0500 Subject: [PATCH 4/5] check registrations and implement streaming override if present --- src/Network/MessageConnection.cs | 42 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Network/MessageConnection.cs b/src/Network/MessageConnection.cs index 03758d7ae..e76eebbbe 100644 --- a/src/Network/MessageConnection.cs +++ b/src/Network/MessageConnection.cs @@ -24,10 +24,13 @@ namespace Soulseek.Network { using System; + using System.Collections.Concurrent; using System.Collections.Generic; + using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; + using Soulseek.Messaging; using Soulseek.Messaging.Messages; using Soulseek.Network.Tcp; @@ -263,23 +266,36 @@ void RaiseMessageDataRead(object sender, ConnectionDataEventArgs e) DataRead += RaiseMessageDataRead; - var payloadBytes = await ReadAsync(length - CodeLength, CancellationToken.None).ConfigureAwait(false); - message.AddRange(payloadBytes); - - var messageBytes = message.ToArray(); - - if (SoulseekClient.RaiseEventsAsynchronously) + // if a message stream 'hook' has been installed via InstallMessageStreamHook, stream the remainder + // of the message to the provided stream. the caller will be notified that the read is complete + // via MessageRead -> PeerMessageHandler.HandleMessageRead -> regular message handling + // the caller must avoid trying to use the browse response, since it would have been streamed instead of passed + if (BitConverter.ToInt32(codeBytes) == (int)MessageCode.Peer.BrowseResponse + && MessageHandlingOverrideRegistrations.TryGetValue((int)MessageCode.Peer.BrowseResponse, out var queue) + && queue.TryDequeue(out var entry)) { - Task.Run(() => - { - Interlocked.CompareExchange(ref MessageRead, null, null)? - .Invoke(this, new MessageEventArgs(messageBytes)); - }, CancellationToken.None).Forget(); + await ReadAsync(length - CodeLength, entry.Stream, cancellationToken: CancellationToken.None).ConfigureAwait(false); + entry.Callback(); } else { - Interlocked.CompareExchange(ref MessageRead, null, null)? - .Invoke(this, new MessageEventArgs(messageBytes)); + var payloadBytes = await ReadAsync(length - CodeLength, CancellationToken.None).ConfigureAwait(false); + message.AddRange(payloadBytes); + var messageBytes = message.ToArray(); + + if (SoulseekClient.RaiseEventsAsynchronously) + { + Task.Run(() => + { + Interlocked.CompareExchange(ref MessageRead, null, null)? + .Invoke(this, new MessageEventArgs(messageBytes)); + }, CancellationToken.None).Forget(); + } + else + { + Interlocked.CompareExchange(ref MessageRead, null, null)? + .Invoke(this, new MessageEventArgs(messageBytes)); + } } } finally From caafd58d8bc93f798ac84eea5a5098edce9a5fa1 Mon Sep 17 00:00:00 2001 From: JP Dillingham Date: Sun, 5 Jul 2026 14:51:10 -0500 Subject: [PATCH 5/5] stub BrowseAsync overload that accepts a delegate --- src/ISoulseekClient.cs | 2 ++ src/SoulseekClient.cs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/ISoulseekClient.cs b/src/ISoulseekClient.cs index 6f9e2301b..773cfaf9f 100644 --- a/src/ISoulseekClient.cs +++ b/src/ISoulseekClient.cs @@ -416,6 +416,8 @@ public interface ISoulseekClient : IDisposable, IDiagnosticGenerator /// Thrown when an exception is encountered during the operation. Task BrowseAsync(string username, BrowseOptions options = null, CancellationToken? cancellationToken = null); + Task BrowseAsync(string username, Action directoryHandler, BrowseOptions options = null, CancellationToken? cancellationToken = null); + /// /// Asynchronously changes the password for the currently logged in user. /// diff --git a/src/SoulseekClient.cs b/src/SoulseekClient.cs index 63e451dd8..27da8bc88 100644 --- a/src/SoulseekClient.cs +++ b/src/SoulseekClient.cs @@ -777,6 +777,11 @@ public Task BrowseAsync(string username, BrowseOptions options = return BrowseInternalAsync(username, options, cancellationToken ?? CancellationToken.None); } + public async Task BrowseAsync(string username, Action directoryHandler, BrowseOptions options = null, CancellationToken cancellationToken = default) + { + // todo: implement me! + } + /// /// Asynchronously changes the password for the currently logged in user. ///