Add configurable media-aware managed session reaping

This commit is contained in:
2026-09-15 22:58:16 +02:00
parent 05eacb3092
commit 274b85025c
14 changed files with 291 additions and 32 deletions
+56 -14
View File
@@ -19,6 +19,8 @@ public sealed class VoiceServer : IAsyncDisposable
private readonly IReadOnlyList<Voicecat.V1.Channel> channels;
private readonly bool allowGuests;
private readonly string name;
private readonly VoiceServerOptions options;
private readonly TimeProvider clock;
private readonly CancellationTokenSource shutdown = new();
private readonly object gate = new();
private readonly Dictionary<ulong, Session> sessions = [];
@@ -27,6 +29,7 @@ public sealed class VoiceServer : IAsyncDisposable
private uint nextUser;
private uint nextSsrc;
private readonly Task accepting;
private readonly Task reaping;
private int disposed;
public IPEndPoint EndPoint => (IPEndPoint)listener.LocalEndPoint!;
@@ -34,9 +37,16 @@ public sealed class VoiceServer : IAsyncDisposable
public event Action<Exception>? ConnectionFailed;
public VoiceServer(string directory, IPEndPoint endpoint, bool allowGuests = true, string name = "VoiceCat Server")
: this(directory, endpoint, new VoiceServerOptions { AllowGuests = allowGuests, Name = name }) { }
public VoiceServer(string directory, IPEndPoint endpoint, VoiceServerOptions options, TimeProvider? timeProvider = null)
{
this.allowGuests = allowGuests;
this.name = name;
ArgumentNullException.ThrowIfNull(options);
options.Validate();
this.options = options;
clock = timeProvider ?? TimeProvider.System;
allowGuests = options.AllowGuests;
name = options.Name;
credentials = ServerCredentials.LoadOrCreate(directory, name);
try
{
@@ -44,7 +54,7 @@ public sealed class VoiceServer : IAsyncDisposable
channels = accounts.LoadChannels();
listener = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
listener.Bind(endpoint);
listener.Listen(64);
listener.Listen(options.MaximumConnections);
media = new((IPEndPoint)listener.LocalEndPoint!);
media.Failed += exception => ConnectionFailed?.Invoke(exception);
}
@@ -57,6 +67,7 @@ public sealed class VoiceServer : IAsyncDisposable
throw;
}
accepting = AcceptAsync();
reaping = ReapAsync();
}
private async Task AcceptAsync()
@@ -68,11 +79,11 @@ public sealed class VoiceServer : IAsyncDisposable
Socket socket = await listener.AcceptAsync(shutdown.Token).ConfigureAwait(false);
lock (gate)
{
if (sessions.Count >= 64) { socket.Dispose(); continue; }
if (sessions.Count >= options.MaximumConnections) { socket.Dispose(); continue; }
socket.NoDelay = true;
string address = ((IPEndPoint)socket.RemoteEndPoint!).Address.ToString();
var connection = new TlsControlConnection(socket, credentials.CreateTlsSession(), shutdown.Token);
var session = new Session(++nextSession, connection, address);
var connection = new TlsControlConnection(socket, credentials.CreateTlsSession(), shutdown.Token, options.HandshakeTimeout);
var session = new Session(++nextSession, connection, address, new(clock));
sessions.Add(session.Id, session);
connections.RemoveAll(task => task.IsCompleted);
connections.Add(HandleAsync(session));
@@ -88,6 +99,7 @@ public sealed class VoiceServer : IAsyncDisposable
{
await foreach (Envelope envelope in session.Connection.ReadAsync(shutdown.Token).ConfigureAwait(false))
{
session.Activity.Touch();
if (envelope.Ping is not null)
{
session.Connection.TrySend(new() { RequestId = envelope.RequestId, Pong = new() { Nonce = envelope.Ping.Nonce } });
@@ -101,7 +113,7 @@ public sealed class VoiceServer : IAsyncDisposable
Reject(session, "Unsupported protocol version or banned address.");
break;
}
session.Media = new(RandomNumberGenerator.GetBytes(16), await session.Connection.TakeMediaCryptoAsync(shutdown.Token).ConfigureAwait(false));
session.Media = new(RandomNumberGenerator.GetBytes(16), await session.Connection.TakeMediaCryptoAsync(shutdown.Token).ConfigureAwait(false), session.Activity);
var hello = new ServerHello { ProtoVersion = 2, ServerName = name, ServerVersion = "0.1.0-dotnet", UdpPort = checked((uint)media.EndPoint.Port), ServerIdentityFingerprint = ByteString.CopyFrom(SHA256.HashData(credentials.Identity.PublicKey)) };
if (allowGuests) hello.AuthMethods.Add("guest");
hello.AuthMethods.Add("password");
@@ -159,6 +171,28 @@ public sealed class VoiceServer : IAsyncDisposable
session.Connection.CompleteWrites();
}
private async Task ReapAsync()
{
if (options.IdleTimeout == TimeSpan.Zero) return;
using var timer = new PeriodicTimer(options.ReaperInterval, clock);
try
{
while (await timer.WaitForNextTickAsync(shutdown.Token).ConfigureAwait(false))
{
lock (gate)
{
foreach (Session session in sessions.Values)
{
if (session.Closing || !session.Activity.IsExpired(options.IdleTimeout)) continue;
session.Closing = true;
Reject(session, "Receive idle timeout.");
}
}
}
}
catch (OperationCanceledException) when (shutdown.IsCancellationRequested) { }
}
private async Task AuthenticateAsync(Session session, ulong requestId, AuthRequest request)
{
User? user = null;
@@ -321,23 +355,31 @@ public sealed class VoiceServer : IAsyncDisposable
listener.Dispose();
try
{
await accepting.ConfigureAwait(false);
Task[] pending;
lock (gate) pending = connections.ToArray();
await Task.WhenAll(pending).ConfigureAwait(false);
await Task.WhenAll(accepting, reaping).ConfigureAwait(false);
}
finally
{
try { await media.DisposeAsync().ConfigureAwait(false); }
finally { accounts.Dispose(); credentials.Dispose(); shutdown.Dispose(); }
try
{
Task[] pending;
lock (gate) pending = connections.ToArray();
await Task.WhenAll(pending).ConfigureAwait(false);
}
finally
{
try { await media.DisposeAsync().ConfigureAwait(false); }
finally { accounts.Dispose(); credentials.Dispose(); shutdown.Dispose(); }
}
}
}
private sealed class Session(ulong id, TlsControlConnection connection, string address)
private sealed class Session(ulong id, TlsControlConnection connection, string address, SessionActivity activity)
{
public ulong Id { get; } = id;
public TlsControlConnection Connection { get; } = connection;
public string Address { get; } = address;
public SessionActivity Activity { get; } = activity;
public bool Closing { get; set; }
public bool HelloReceived { get; set; }
public User? User { get; set; }
public MediaPeer? Media { get; set; }