feat: daily node restart controlled from the admin, connect timings as a default line task [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

Agents and Probes exit in order at a configured time (default 04:30 UTC+8, switch and time zone in the admin settings) and the service manager starts them again; a local restart_at or MOSHIRO_RESTART_AT wins. The connect kind (TCP connect and TLS handshake timings) is now a default continuous line template, with a section on the line page.
This commit is contained in:
justme committed 2026-10-07 14:56:50 +08:00
1 parent 388619b8b6
commit 9bcef56497
37 files changed
+928 -101

No files matched your search

@@ -53,11 +53,13 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
private readonly AgentService _service;
private readonly NodeInfoService _nodes;
private readonly ReleaseService _releases;
private readonly ScheduleService _schedule;
private readonly IProbeStore _probes;
public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases, IProbeStore probes)
public MoShiroNodeEventSink(IAgentStore agents, AgentRuntime runtime, IEventStore events, LiveHub live, AgentService service, NodeInfoService nodes, ReleaseService releases, ScheduleService schedule, IProbeStore probes)
{
_releases = releases;
_schedule = schedule;
_probes = probes;
_nodes = nodes;
_agents = agents;
@@ -144,6 +146,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
}
await _releases.OfferToNodeAsync(id, ct);
await _schedule.SendToNodeAsync(id, ct);
break;
case NodeEventKind.Offline:
_live.Publish(new NodeSignal("probe", id.Value, false, nodeEvent.AtMs));
@@ -214,6 +217,7 @@ public sealed class MoShiroNodeEventSink : INodeEventSink
AgentRecord? after = await _agents.GetAsync(id, ct);
_nodes.ObserveAgentInBackground(id.Value, after?.PublicHost, meta.Platform);
await _releases.OfferToNodeAsync(id, ct);
await _schedule.SendToNodeAsync(id, ct);
if (before is not null && after is not null
&& (!before.TlsSpki.AsSpan().SequenceEqual(after.TlsSpki) || before.ProbeTcpPort != after.ProbeTcpPort || !before.ProbeUdpPorts.SequenceEqual(after.ProbeUdpPorts) || before.Ipv6Supported != after.Ipv6Supported)
&& after.Status == NodeStatus.Active)
@@ -0,0 +1,54 @@
using System.Globalization;
using Fleet.Core;
namespace MoShiro.Backend.Application;
/// <summary>
/// 节点的定时重启:设置在管理端(开关、时刻、时区偏移),节点连上时补发、设置变化时群发给所有在线节点。
/// 面板只告诉节点「几点退出」,节点自己到点有序退出、由服务管理器拉起;节点本地写了设置时以本地为准。
/// </summary>
public sealed class ScheduleService
{
private readonly SettingsService _settings;
private readonly IAgentGateway _gateway;
public ScheduleService(SettingsService settings, IAgentGateway gateway)
{
_settings = settings;
_gateway = gateway;
}
/// <summary>形如 <c>04:30+08:00</c>;开关关着时为空。</summary>
public async ValueTask<string?> RestartAtAsync(CancellationToken ct)
{
if (!await _settings.GetBoolAsync(SettingsService.RestartEnabled, ct))
{
return null;
}
return Format(
await _settings.GetIntAsync(SettingsService.RestartHour, ct),
await _settings.GetIntAsync(SettingsService.RestartMinute, ct),
await _settings.GetIntAsync(SettingsService.RestartOffsetMinutes, ct));
}
public static string Format(int hour, int minute, int offsetMinutes)
{
int abs = Math.Abs(offsetMinutes);
return string.Create(CultureInfo.InvariantCulture, $"{hour:00}:{minute:00}{(offsetMinutes < 0 ? '-' : '+')}{abs / 60:00}:{abs % 60:00}");
}
/// <summary>节点刚连上:补发当前设置(节点离线时改的设置也不会错过)。</summary>
public async ValueTask SendToNodeAsync(NodeId node, CancellationToken ct) =>
await _gateway.NotifyScheduleAsync(node, await RestartAtAsync(ct), ct);
/// <summary>管理端改了设置:通知所有在线节点。</summary>
public async ValueTask SendToAllAsync(CancellationToken ct)
{
string? restartAt = await RestartAtAsync(ct);
foreach (NodeId node in _gateway.OnlineNodes)
{
await _gateway.NotifyScheduleAsync(node, restartAt, ct);
}
}
}