using System.Text.RegularExpressions;
using Fleet.Core;
using Fleet.Server;
using MoShiro.Protocol;
namespace MoShiro.Backend.Application;
/// 某个 Probe 现在连着的一个 Agent(Agent 上报)。
public sealed record ProbeConnection(NodeId AgentId, string AgentName, bool AgentOnline, string? Version, string? Remote, long SinceMs, string IpVersion = IpVersions.V4);
public sealed record ProbeView(ProbeRecord Probe, bool Online, IReadOnlyList Connections)
{
public bool Revoked => Probe.RevokedAtMs is not null;
}
///
/// Probe:用加入令牌注册(密钥由 Probe 自己生成,Backend 从不接触私钥),可以有多个,每个都能改名、吊销、删除。
/// 所有未吊销的 Probe 的对等面公钥都会写进每个 Agent 的期望状态(peers);吊销、删除之后,Agent 立刻断开对应会话,Probe 到面板的连接也被断开。
/// 要换密钥就吊销后重新注册(Probe 的 id 会变)。
/// 一条线路是一对(Probe,Agent)。
///
public sealed partial class ProbeService
{
private readonly IProbeStore _probes;
private readonly IAgentStore _agents;
private readonly AgentRuntime _runtime;
private readonly IAgentGateway _gateway;
private readonly TimeProvider _time;
private readonly NodeInfoService _nodes;
private readonly LinkService _links;
public ProbeService(IProbeStore probes, IAgentStore agents, AgentRuntime runtime, IAgentGateway gateway, TimeProvider time, NodeInfoService nodes, LinkService links)
{
_links = links;
_nodes = nodes;
_probes = probes;
_agents = agents;
_runtime = runtime;
_gateway = gateway;
_time = time;
}
public async ValueTask> ListAsync(CancellationToken ct)
{
IReadOnlyList probes = await _probes.ListAsync(ct);
IReadOnlyList agents = await _agents.ListAsync(ct);
return [.. probes.Select(p => View(p, agents))];
}
public async ValueTask GetAsync(string id, CancellationToken ct) =>
await _probes.GetAsync(id, ct) is { } probe ? View(probe, await _agents.ListAsync(ct)) : null;
public async ValueTask> RenameAsync(string id, string name, CancellationToken ct)
{
if (await _probes.GetAsync(id, ct) is not { } probe)
{
return Result.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
string trimmed = name.Trim();
if (!NamePattern().IsMatch(trimmed))
{
return ValidationError.Fail("name", "must be 1-64 characters: letters (any language), digits, '.', '_' or '-'");
}
if ((await _probes.ListAsync(ct)).Any(p => p.Id != id && string.Equals(p.Name, trimmed, StringComparison.OrdinalIgnoreCase)))
{
return Result.Fail(MoShiroErrors.NameConflict, $"a probe named {trimmed} already exists");
}
await _probes.SaveAsync(probe with { Name = trimmed }, ct);
return Result.Ok((await GetAsync(id, ct))!);
}
/// 吊销:保留记录和历史数据,公钥从所有 Agent 的名单里移除,Agent 立即断开它的会话。
public async ValueTask> RevokeAsync(string id, CancellationToken ct)
{
if (await _probes.GetAsync(id, ct) is null)
{
return Result.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
await _probes.RevokeAsync(id, _time.GetUtcNow().ToUnixTimeMilliseconds(), ct);
// 吊销会阻止未来认证;也断开当前直连 Backend 的长会话,不等待 Probe 心跳超时。
await _gateway.DisconnectAsync(new NodeId(id), "probe revoked", ct);
_gateway.NotifyAllTaskSetsChanged();
return Result.Ok((await GetAsync(id, ct))!);
}
/// 删除:Probe 身份和它的线路从面板消失;已经入库的数据按保留策略自然过期。
public async ValueTask DeleteAsync(string id, CancellationToken ct)
{
bool deleted = await _probes.DeleteAsync(id, ct);
if (deleted)
{
await _nodes.ForgetAsync(id, ct);
await _links.ForgetNodeAsync(id, ct);
_gateway.NotifyAllTaskSetsChanged();
}
return deleted;
}
/// 让某个 Agent 断开这个 Probe(Probe 会按退避重连,除非它已经被吊销)。
public async ValueTask> DisconnectAsync(string probeId, NodeId agentId, CancellationToken ct)
{
if (await _probes.GetAsync(probeId, ct) is null)
{
return Result.Fail(MoShiroErrors.ProbeNotFound, "probe not found");
}
if (await _agents.GetAsync(agentId, ct) is null)
{
return Result.Fail(MoShiroErrors.AgentNotFound, "agent not found");
}
return await _gateway.DisconnectProbeAsync(agentId, new ProbeId(probeId), ct);
}
private ProbeView View(ProbeRecord probe, IReadOnlyList agents)
{
var connections = new List();
foreach ((NodeId agentId, ConnectedProbeInfo info) in _runtime.ConnectionsOf(probe.Id))
{
AgentRecord? agent = agents.FirstOrDefault(a => a.Id == agentId);
if (agent is not null)
{
connections.Add(new ProbeConnection(agentId, agent.Name, _gateway.IsOnline(agentId), info.Version, info.Remote, info.SinceMs, info.IpVersion));
}
}
return new ProbeView(probe, _gateway.IsOnline(new NodeId(probe.Id)), [.. connections.OrderBy(c => c.AgentName, StringComparer.Ordinal)]);
}
[GeneratedRegex(@"^[\p{L}\p{M}\p{N}._-]{1,64}$")]
private static partial Regex NamePattern();
}