using System.Collections.Concurrent;
using Fleet.Core;
using MoShiro.Protocol;
namespace MoShiro.Agent;
///
/// 拨号侧的组装点:每个拨号目标(监听节点 × IP 族)一个 。目标列表变化时只调和连接:
/// 新增的建立,移除的断开,地址或证书指纹变化的重建,其余不受影响。
///
public sealed class DialerHost : IDisposable
{
private readonly DialEnv _env;
private readonly Dictionary _connections = [];
private readonly ConcurrentDictionary _pendingRetries = new(StringComparer.Ordinal);
private readonly SemaphoreSlim _changed = new(0);
private readonly TimeProvider _time;
private readonly INodeLog _log;
public DialerHost(DialEnv env, TimeProvider time, INodeLog log)
{
_env = env;
_time = time;
_log = log;
env.Directory.Changed += () => _changed.Release();
}
public DialDirectory Directory => _env.Directory;
/// 某条路径的控制会话是否已经建立、QUIC 数据通道是否可发送(测试和状态查看用)。
public bool IsConnected(string listenerId, string ipVersion = IpVersions.V4)
{
lock (_connections)
{
return _connections.TryGetValue(listenerId + "~" + ipVersion, out Running? running) && running.Connection.IsConnected;
}
}
public DialConnection? Connection(string listenerId, string ipVersion = IpVersions.V4)
{
lock (_connections)
{
return _connections.TryGetValue(listenerId + "~" + ipVersion, out Running? running) ? running.Connection : null;
}
}
/// 每条路径当前的线路健康(随心跳上报)。
public List SnapshotLinks()
{
DialConnection[] connections;
lock (_connections)
{
connections = [.. _connections.Values.Select(r => r.Connection)];
}
return [.. connections.Select(c => c.Health())];
}
/// 管理端手动重试:跳过这个监听节点所有路径的退避。目标列表还没到时,30 秒内到达就补上。
public void RetryListener(string listenerId, bool queueIfMissing = true)
{
DialConnection[] matches;
lock (_connections)
{
matches = [.. _connections.Values.Select(r => r.Connection).Where(c => c.Endpoint.NodeId == listenerId)];
}
if (matches.Length == 0)
{
if (queueIfMissing)
{
_pendingRetries[listenerId] = _time.GetUtcNow().AddSeconds(30).ToUnixTimeMilliseconds();
}
_log.Warn("manual link retry requested but no matching dial target is configured", ("listener", listenerId));
return;
}
_pendingRetries.TryRemove(listenerId, out _);
foreach (DialConnection connection in matches)
{
connection.RetryNow();
}
}
public async Task RunAsync(CancellationToken ct)
{
try
{
while (!ct.IsCancellationRequested)
{
await ReconcileAsync(_env.Directory.Targets, ct);
RetryPending();
await _changed.WaitAsync(ct);
}
}
catch (OperationCanceledException)
{
// 停止
}
finally
{
await ReconcileAsync([], CancellationToken.None);
}
}
public void Dispose() => _changed.Dispose();
private void RetryPending()
{
long now = _time.GetUtcNow().ToUnixTimeMilliseconds();
foreach ((string listenerId, long expiresAt) in _pendingRetries.ToArray())
{
if (expiresAt <= now)
{
_pendingRetries.TryRemove(listenerId, out _);
}
else
{
RetryListener(listenerId, queueIfMissing: false);
}
}
}
private async Task ReconcileAsync(IReadOnlyList targets, CancellationToken ct)
{
var wanted = targets.ToDictionary(t => t.PathKey);
var stopped = new List();
lock (_connections)
{
foreach ((string id, Running running) in _connections.ToList())
{
if (!wanted.TryGetValue(id, out DialEndpoint? endpoint) || !DialDirectory.Equal(endpoint, running.Connection.Endpoint))
{
stopped.Add(running);
_connections.Remove(id);
}
}
}
foreach (Running running in stopped)
{
await running.StopAsync();
}
if (ct.IsCancellationRequested)
{
return;
}
lock (_connections)
{
foreach ((string id, DialEndpoint endpoint) in wanted)
{
if (!_connections.ContainsKey(id))
{
_connections[id] = Running.Start(new DialConnection(endpoint, _env));
}
}
}
}
private sealed class Running : IDisposable
{
private readonly CancellationTokenSource _cts = new();
private Task _run = Task.CompletedTask;
private Running(DialConnection connection)
{
Connection = connection;
}
public DialConnection Connection { get; }
public static Running Start(DialConnection connection)
{
var running = new Running(connection);
running._run = Task.Run(() => connection.RunAsync(running._cts.Token), CancellationToken.None);
return running;
}
public async Task StopAsync()
{
await _cts.CancelAsync();
try
{
await _run.WaitAsync(TimeSpan.FromSeconds(10));
}
catch (Exception ex) when (ex is OperationCanceledException or TimeoutException)
{
// 没有及时退出:不再等
}
Dispose();
}
public void Dispose()
{
Connection.Dispose();
_cts.Dispose();
}
}
}