Namespaces, assemblies, projects and directories, the solution, binaries, images, systemd units, config paths, environment variables, protocol method names, the media type and the raw chunk magic all move to MoShiro/moshiro. Existing data and deployments are not compatible and must be recreated. Also fix the architecture tests after the solution layout change and make the update tests independent of their order.
197 lines
6.7 KiB
C#
197 lines
6.7 KiB
C#
using Fleet.Core;
|
|
using Fleet.Hosting;
|
|
using System.Text.Json;
|
|
using Fleet.Identity;
|
|
using Fleet.Link;
|
|
using Fleet.Rpc;
|
|
using MoShiro.Measurement;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Probe;
|
|
|
|
/// <summary>
|
|
/// Probe 的组装点:到面板的长连接(身份、Agent 列表、状态)+ 每个 Agent 一个测量连接(认证、重连、角色)。没有全局状态,测试可以在同一进程里启动多个。
|
|
/// Agent 列表变化时只调和连接:新增的建立,移除的断开,地址或证书指纹变化的重建,其余不受影响。
|
|
/// </summary>
|
|
public sealed class ProbeApp : IDisposable
|
|
{
|
|
public const string Version = MoShiro.Protocol.BuildInfo.Version;
|
|
|
|
private readonly ConnectionEnv _env;
|
|
private readonly Dictionary<string, Running> _connections = [];
|
|
private readonly SemaphoreSlim _changed = new(0);
|
|
private readonly INodeLog _log;
|
|
private readonly FleetLink _link;
|
|
|
|
public ProbeApp(ProbeFile file, string configDirectory, TimeProvider time, INodeLog log)
|
|
{
|
|
_log = log;
|
|
var directory = new AgentDirectory(new ConfigStore(configDirectory), configDirectory, log);
|
|
NodeIdentity identity = NodeIdentity.FromPkcs8(Convert.FromBase64String(file.PrivateKey));
|
|
_env = new ConnectionEnv(identity, file.ProbeId, file.Limits, directory, new MonoClock(time), time, log);
|
|
directory.Changed += () => _changed.Release();
|
|
|
|
var update = new UpdateBridge(configDirectory, new ConfigStore(configDirectory).Write, log);
|
|
var options = new FleetLinkOptions
|
|
{
|
|
Server = new Uri(file.Server),
|
|
Identity = identity,
|
|
NodeId = new NodeId(file.ProbeId),
|
|
NodeVersion = Version,
|
|
Capabilities = [MoShiroCapabilities.Base],
|
|
HeartbeatExtra = () => JsonSerializer.SerializeToElement(new ProbeHeartbeatExtra { Update = update.ReadStatus() }, MoShiroJsonContext.Default.ProbeHeartbeatExtra),
|
|
HelloMeta = () => JsonSerializer.SerializeToElement(new ProbeHelloMeta { Platform = PlatformDetector.Detect() }, MoShiroJsonContext.Default.ProbeHelloMeta),
|
|
};
|
|
var list = new StateHandler<ProbeAgentList>(
|
|
ProbeAgentList.StateType,
|
|
MoShiroJsonContext.Default.ProbeAgentList,
|
|
(version, doc, _) =>
|
|
{
|
|
directory.Apply(doc);
|
|
return ValueTask.FromResult(new StateSyncResult { AppliedVersion = version });
|
|
});
|
|
var router = new RpcRouter<LinkContext>()
|
|
.OnNotify(MoShiroMethods.UpdateOffer, MoShiroJsonContext.Default.UpdateOfferParams, (_, offer, _) =>
|
|
{
|
|
update.WriteOffer(offer);
|
|
return ValueTask.CompletedTask;
|
|
})
|
|
.OnNotify(MoShiroMethods.UpdateApply, MoShiroJsonContext.Default.UpdateOfferParams, (_, _, _) =>
|
|
{
|
|
update.RequestApply();
|
|
return ValueTask.CompletedTask;
|
|
});
|
|
_link = new FleetLink(options, router, [list], new Outbox(1 << 20), time, log);
|
|
}
|
|
|
|
/// <summary>到面板的连接(测试和状态查看用)。</summary>
|
|
public FleetLink Link => _link;
|
|
|
|
public AgentDirectory Directory => _env.Directory;
|
|
|
|
/// <summary>某个 Agent 的会话是否已经建立(测试和状态查看用)。</summary>
|
|
public bool IsConnected(string agentId)
|
|
{
|
|
lock (_connections)
|
|
{
|
|
return _connections.TryGetValue(agentId, out Running? running) && running.Connection.IsConnected;
|
|
}
|
|
}
|
|
|
|
public AgentConnection? Connection(string agentId)
|
|
{
|
|
lock (_connections)
|
|
{
|
|
return _connections.TryGetValue(agentId, out Running? running) ? running.Connection : null;
|
|
}
|
|
}
|
|
|
|
public async Task RunAsync(CancellationToken ct)
|
|
{
|
|
if (_env.Directory.Agents.Count == 0)
|
|
{
|
|
_log.Info("no agents known yet, waiting for the panel to send the list");
|
|
}
|
|
|
|
Task link = _link.RunAsync(ct);
|
|
try
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
await ReconcileAsync(_env.Directory.Agents, ct);
|
|
await _changed.WaitAsync(ct);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 停止
|
|
}
|
|
finally
|
|
{
|
|
await ReconcileAsync([], CancellationToken.None);
|
|
await Task.WhenAny(link, Task.Delay(TimeSpan.FromSeconds(5), CancellationToken.None));
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_env.Identity.Dispose();
|
|
_changed.Dispose();
|
|
}
|
|
|
|
private async Task ReconcileAsync(IReadOnlyList<KnownAgent> agents, CancellationToken ct)
|
|
{
|
|
var wanted = agents.ToDictionary(a => a.AgentId);
|
|
var stopped = new List<Running>();
|
|
lock (_connections)
|
|
{
|
|
foreach ((string id, Running running) in _connections.ToList())
|
|
{
|
|
if (!wanted.TryGetValue(id, out KnownAgent? endpoint) || !AgentDirectory.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, KnownAgent endpoint) in wanted)
|
|
{
|
|
if (!_connections.ContainsKey(id))
|
|
{
|
|
_connections[id] = Running.Start(new AgentConnection(endpoint, _env));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class Running : IDisposable
|
|
{
|
|
private readonly CancellationTokenSource _cts = new();
|
|
private Task _run = Task.CompletedTask;
|
|
|
|
private Running(AgentConnection connection)
|
|
{
|
|
Connection = connection;
|
|
}
|
|
|
|
public AgentConnection Connection { get; }
|
|
|
|
public static Running Start(AgentConnection 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() => _cts.Dispose();
|
|
}
|
|
}
|