Files
MoShiro/backend/fleet/src/Fleet.Hosting/RestartSchedule.cs
T
justme 9bcef56497
ci/woodpecker/push/main-amd64-build Pipeline was successful
feat: daily node restart controlled from the admin, connect timings as a default line task [CI]
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.
2026-10-07 14:56:50 +08:00

106 lines
3.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
namespace Fleet.Hosting;
/// <summary>
/// 每天固定时刻有序退出一次,由 systemd、容器引擎或 procd 的「总是重启」拉起。写法 <c>HH:mm</c>(按 UTC)、
/// <c>HH:mm±hh:mm</c> 或 <c>HH:mm</c> 加 <c>Z</c>:容器里没有时区数据库,所以偏移要写明,例如 <c>04:00+08:00</c>。
/// 进程自己退出,不需要任何执行外部命令的能力。
/// </summary>
public sealed class RestartSchedule
{
/// <summary>环境变量,设置了就覆盖配置文件里的值。</summary>
public const string EnvironmentVariable = "MOSHIRO_RESTART_AT";
/// <summary>刚启动的一段时间内不会触发:重启之后不会因为时刻还没过去而再重启一次,也避免重启风暴。</summary>
public static readonly TimeSpan MinimumUptime = TimeSpan.FromMinutes(10);
private readonly TimeSpan _time;
private readonly TimeSpan _offset;
private RestartSchedule(TimeSpan time, TimeSpan offset)
{
_time = time;
_offset = offset;
}
public static bool TryParse(string? text, out RestartSchedule? schedule)
{
schedule = null;
string value = text?.Trim() ?? "";
// HH:mm,后面可以跟 Z 或 ±hh:mm
if (value.Length < 5 || value[2] != ':' ||
!TryTwoDigits(value, 0, out int hour) || !TryTwoDigits(value, 3, out int minute) || hour > 23 || minute > 59)
{
return false;
}
TimeSpan offset = TimeSpan.Zero;
string rest = value[5..];
if (rest.Length > 0 && rest != "Z")
{
if (rest.Length != 6 || rest[0] is not ('+' or '-') || rest[3] != ':' ||
!TryTwoDigits(rest, 1, out int offsetHours) || !TryTwoDigits(rest, 4, out int offsetMinutes) || offsetMinutes > 59)
{
return false;
}
offset = new TimeSpan(offsetHours, offsetMinutes, 0);
if (offset > TimeSpan.FromHours(14))
{
return false;
}
if (rest[0] == '-')
{
offset = -offset;
}
}
schedule = new RestartSchedule(new TimeSpan(hour, minute, 0), offset);
return true;
}
private static bool TryTwoDigits(string text, int start, out int value)
{
value = 0;
if (!char.IsAsciiDigit(text[start]) || !char.IsAsciiDigit(text[start + 1]))
{
return false;
}
value = (text[start] - '0') * 10 + (text[start + 1] - '0');
return true;
}
/// <summary>
/// 环境变量优先,其次配置文件的值;都没有就不定时重启。写错了抛 <see cref="InvalidOperationException"/>,
/// 启动时就能看到,而不是悄悄不生效。
/// </summary>
public static RestartSchedule? Resolve(string? configured)
{
string? text = Environment.GetEnvironmentVariable(EnvironmentVariable);
if (string.IsNullOrWhiteSpace(text))
{
text = configured;
}
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
return TryParse(text, out RestartSchedule? schedule)
? schedule
: throw new InvalidOperationException($"restart_at must look like 04:00, 04:00Z or 04:00+08:00 (got '{text}')");
}
/// <summary>严格晚于 <paramref name="now"/> 的下一个触发时刻。</summary>
public DateTimeOffset NextAfter(DateTimeOffset now)
{
DateTimeOffset local = now.ToOffset(_offset);
DateTimeOffset candidate = new DateTimeOffset(local.Year, local.Month, local.Day, 0, 0, 0, _offset) + _time;
return candidate <= local ? candidate.AddDays(1) : candidate;
}
}