using MiGu.Server.Configs;
using MiGu.Server.Launcher;
namespace MiGu.Server.Logs;
///
/// 对齐 Simple3 LogCleaner:定时删除 log/ 下过期 *.log,并检测日志盘剩余空间(边沿告警,写日志不弹窗)。
///
public sealed class LogCleanupService : IHostedService, IDisposable
{
private readonly ConfigStore _store;
private readonly Simple3Launcher _launcher;
private readonly IConfiguration _config;
private readonly ILogger _log;
private readonly object _cleanLock = new();
private readonly object _scheduleLock = new();
private Timer? _cleanTimer;
private Timer? _diskTimer;
private bool _diskBelow;
public LogCleanupService(
ConfigStore store,
Simple3Launcher launcher,
IConfiguration config,
ILogger log)
{
_store = store;
_launcher = launcher;
_config = config;
_log = log;
}
public Task StartAsync(CancellationToken cancellationToken)
{
try
{
var opt = _store.GetLogCleanup();
if (opt.Enabled && opt.RunOnStartup)
RunCleanSafely(opt, "启动清理");
ApplySchedule();
CheckDisk(opt);
}
catch (Exception ex)
{
_log.LogWarning(ex, "LogCleanupService 启动失败");
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
lock (_scheduleLock)
{
_cleanTimer?.Dispose();
_cleanTimer = null;
_diskTimer?.Dispose();
_diskTimer = null;
}
return Task.CompletedTask;
}
public void Dispose()
{
_cleanTimer?.Dispose();
_diskTimer?.Dispose();
}
public LogCleanupOptions GetOptions() => _store.GetLogCleanup();
public LogCleanupOptions SaveOptions(LogCleanupOptions options)
{
var saved = _store.PutLogCleanup(options);
_ = saved;
var opt = _store.GetLogCleanup();
ApplySchedule();
CheckDisk(opt, forceReeval: true);
return opt;
}
public void ApplySchedule()
{
var opt = _store.GetLogCleanup();
lock (_scheduleLock)
{
_cleanTimer?.Dispose();
_cleanTimer = null;
if (opt.Enabled)
{
var hours = opt.CheckIntervalHours <= 0 ? 1 : opt.CheckIntervalHours;
var period = TimeSpan.FromHours(hours);
_cleanTimer = new Timer(_ => RunCleanSafely(_store.GetLogCleanup(), "定时清理"),
null, period, period);
}
_diskTimer?.Dispose();
var mins = opt.DiskCheckIntervalMinutes <= 0 ? 30 : opt.DiskCheckIntervalMinutes;
var diskPeriod = TimeSpan.FromMinutes(mins);
_diskTimer = new Timer(_ => CheckDisk(_store.GetLogCleanup()),
null, diskPeriod, diskPeriod);
}
}
public CleanResult CleanNow()
{
var opt = _store.GetLogCleanup();
var r = CleanOnce(opt);
if (r.Error != null)
_log.LogWarning("立即清理失败:{Error}", r.Error);
else if (r.DeletedFiles > 0)
_log.LogInformation("立即清理删除 {Count} 个日志文件,释放 {Mb:F1} MB", r.DeletedFiles, r.FreedMB);
else
_log.LogInformation("立即清理完成:没有过期日志");
return r;
}
public DiskStatus GetDiskStatus()
{
var opt = _store.GetLogCleanup();
var known = TryGetFreeGB(out var freeGB, out var drive);
var below = known && opt.DiskAlertEnabled && freeGB < opt.DiskFreeAlertGB;
return new DiskStatus(known, drive, freeGB, opt.DiskAlertEnabled, opt.DiskFreeAlertGB, below);
}
private void RunCleanSafely(LogCleanupOptions opt, string reason)
{
if (!opt.Enabled && reason != "立即清理") return;
var r = CleanOnce(opt);
if (r.Error != null)
_log.LogWarning("[{Reason}] 失败:{Error}", reason, r.Error);
else if (r.DeletedFiles > 0)
_log.LogInformation("[{Reason}] 删除 {Count} 个日志文件,释放 {Mb:F1} MB", reason, r.DeletedFiles, r.FreedMB);
}
private CleanResult CleanOnce(LogCleanupOptions opt)
{
opt.Clamp();
lock (_cleanLock)
{
try
{
var (root, _, error) = ResolveLogRoot();
if (root == null)
return new CleanResult(0, 0, error ?? "无法定位日志目录");
if (!Directory.Exists(root))
return new CleanResult(0, 0, null);
var cutoff = DateTime.Now.AddDays(-opt.RetentionDays);
var deleted = 0;
long freed = 0;
foreach (var f in Directory.EnumerateFiles(root, "*.log", SearchOption.AllDirectories))
{
try
{
var fi = new FileInfo(f);
if (fi.LastWriteTime >= cutoff) continue;
var len = fi.Length;
fi.Delete();
deleted++;
freed += len;
}
catch
{
/* 占用/权限:跳过 */
}
}
TryRemoveEmptyDirs(root);
return new CleanResult(deleted, freed, null);
}
catch (Exception ex)
{
return new CleanResult(0, 0, ex.Message);
}
}
}
private static void TryRemoveEmptyDirs(string root)
{
try
{
foreach (var dir in Directory.EnumerateDirectories(root, "*", SearchOption.AllDirectories)
.OrderByDescending(d => d.Length))
{
try
{
if (!Directory.EnumerateFileSystemEntries(dir).Any())
Directory.Delete(dir, false);
}
catch { /* 忽略单目录失败 */ }
}
}
catch { /* 忽略枚举失败 */ }
}
private void CheckDisk(LogCleanupOptions opt, bool forceReeval = false)
{
if (forceReeval) _diskBelow = false;
if (!opt.DiskAlertEnabled)
{
_diskBelow = false;
return;
}
if (!TryGetFreeGB(out var freeGB, out var drive))
return;
if (freeGB < opt.DiskFreeAlertGB)
{
if (_diskBelow) return;
_diskBelow = true;
_log.LogWarning(
"磁盘剩余空间不足:{Drive} 仅剩 {Free:F1} GB(低于阈值 {Threshold:F0} GB)。请及时清理磁盘,或在系统配置中调低日志保留天数 / 立即清理。",
drive, freeGB, opt.DiskFreeAlertGB);
}
else
{
_diskBelow = false;
}
}
private bool TryGetFreeGB(out double freeGB, out string driveName)
{
freeGB = 0;
driveName = "";
var (root, _, _) = ResolveLogRoot();
if (root == null) return false;
try
{
var driveRoot = Path.GetPathRoot(root);
if (string.IsNullOrEmpty(driveRoot)) return false;
var di = new DriveInfo(driveRoot);
driveName = di.Name;
freeGB = di.AvailableFreeSpace / 1024.0 / 1024.0 / 1024.0;
return true;
}
catch
{
return false;
}
}
private (string? root, string? workdir, string? error) ResolveLogRoot()
{
var overrideRoot = _config["Logs:Root"];
if (!string.IsNullOrWhiteSpace(overrideRoot))
{
var r = Path.GetFullPath(overrideRoot);
return (r, Path.GetDirectoryName(r), null);
}
var wd = _launcher.ResolveWorkingDirectory();
if (string.IsNullOrWhiteSpace(wd))
return (null, null, "未能定位 Simple3 工作目录");
return (Path.GetFullPath(Path.Combine(wd, "log")), wd, null);
}
public readonly record struct CleanResult(int DeletedFiles, long FreedBytes, string? Error)
{
public double FreedMB => FreedBytes / 1024.0 / 1024.0;
}
public readonly record struct DiskStatus(
bool Known,
string Drive,
double FreeGB,
bool AlertEnabled,
double AlertGB,
bool BelowThreshold);
}