using System.Text.Json;
using Fleet.Core;
using Fleet.Hosting;
using MoShiro.Protocol;
namespace MoShiro.Agent;
///
/// 本节点要拨的监听节点列表:由面板作为期望状态(dial_targets)直接下发;面板连不上时用上一次保存的 dial_targets.json。
/// 列表变化时用 写回 agents.json(这是配置,不是测量数据;只在变化时写,保护路由器的闪存)。
///
public sealed class DialDirectory
{
public const string FileName = "dial_targets.json";
private readonly object _gate = new();
private readonly ConfigStore _store;
private readonly INodeLog _log;
private readonly TrustAnchor _trust;
private List _targets = [];
private long _version;
public DialDirectory(ConfigStore store, string configDirectory, INodeLog log, TrustAnchor? trust = null)
{
_trust = trust ?? TrustAnchor.None;
_store = store;
_log = log;
string cache = Path.Combine(configDirectory, FileName);
if (File.Exists(cache))
{
try
{
DialCache saved = NodeConfig.Load(cache, AgentConfigContext.Default.DialCache);
_targets = saved.Targets;
_version = saved.Version;
}
catch (Exception ex) when (ex is IOException or InvalidDataException or JsonException or InvalidOperationException)
{
_log.Warn("dial_targets.json is unreadable, waiting for the panel", ("err", ex.Message));
}
}
}
/// 列表变化(调和连接)。
public event Action? Changed;
public long Version
{
get
{
lock (_gate)
{
return _version;
}
}
}
public IReadOnlyList Targets
{
get
{
lock (_gate)
{
return [.. _targets];
}
}
}
/// 应用面板下发的列表。版本不比当前新的忽略(乱序到达的旧推送)。
public void Apply(DialTargets result)
{
var next = new List();
_trust.Update(result.TrustChain);
foreach (DialTarget target in result.Targets)
{
if (!string.IsNullOrWhiteSpace(target.Host))
{
next.Add(new DialEndpoint { NodeId = target.NodeId.Value, Host = target.Host, TcpPort = target.TcpPort, QuicPorts = [.. target.QuicPorts], PublicKey = target.PublicKey });
}
if (!string.IsNullOrWhiteSpace(target.Ipv6Host))
{
next.Add(new DialEndpoint { NodeId = target.NodeId.Value, Host = target.Ipv6Host, TcpPort = target.TcpPort, QuicPorts = [.. target.QuicPorts], PublicKey = target.PublicKey, IpVersion = IpVersions.V6 });
}
}
lock (_gate)
{
if (result.Version < _version || (result.Version == _version && Same(next, _targets)))
{
return;
}
_version = result.Version;
_targets = next;
}
try
{
_store.Write(FileName, JsonSerializer.SerializeToUtf8Bytes(new DialCache { Version = result.Version, Targets = next }, AgentConfigContext.Default.DialCache));
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
_log.Warn("could not save dial_targets.json", ("err", ex.Message));
}
_log.Info("dial targets updated", ("version", result.Version), ("targets", next.Count));
Changed?.Invoke();
}
private static bool Same(List a, List b) =>
a.Count == b.Count && a.Zip(b).All(p => Equal(p.First, p.Second));
public static bool Equal(DialEndpoint a, DialEndpoint b) =>
a.NodeId == b.NodeId && a.IpVersion == b.IpVersion && a.Host == b.Host && a.TcpPort == b.TcpPort && a.QuicPorts.SequenceEqual(b.QuicPorts) && a.PublicKey == b.PublicKey;
}