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(); } } }