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.
106 lines
3.7 KiB
C#
106 lines
3.7 KiB
C#
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;
|
||
}
|
||
}
|