1111 lines
46 KiB
C#
1111 lines
46 KiB
C#
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.IO;
|
||
using System.Threading;
|
||
using System.Threading.Channels;
|
||
using System.Threading.Tasks;
|
||
using System.Linq;
|
||
using Microsoft.Extensions.Configuration;
|
||
using System.Collections.Generic;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Logging.Abstractions;
|
||
using Serilog;
|
||
using Serilog.Events;
|
||
using Serilog.Extensions.Logging;
|
||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||
|
||
namespace JiShe.CollectBus.PluginFileWatcher
|
||
{
|
||
// 删除配置类的定义
|
||
|
||
class Program
|
||
{
|
||
// 配置对象,从appsettings.json加载
|
||
private static FileMonitorConfig _config = new FileMonitorConfig();
|
||
|
||
// 全局计数器和动态配置参数
|
||
private static long _totalEventsProcessed = 0;
|
||
|
||
// 监控器健康状态追踪
|
||
private static DateTime _lastEventTime = DateTime.UtcNow;
|
||
private static int _watcherRestartAttempts = 0;
|
||
private static FileSystemWatcher _activeWatcher = null;
|
||
|
||
// 事件存储和回放
|
||
private static EventStorage _eventStorage = null;
|
||
private static EventReplaySession _currentReplaySession = null;
|
||
private static ILogger _logger = NullLogger.Instance;
|
||
|
||
static async Task Main(string[] args)
|
||
{
|
||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||
|
||
// 检查是否以数据库工具模式运行
|
||
if (args.Length > 0 && args[0].Equals("db", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
// 运行数据库工具
|
||
await RunDatabaseUtilityAsync(args.Skip(1).ToArray());
|
||
return;
|
||
}
|
||
|
||
// 加载配置文件
|
||
LoadConfiguration();
|
||
|
||
// 配置Serilog
|
||
ConfigureSerilog();
|
||
|
||
// 初始化日志记录器
|
||
_logger = new SerilogLoggerFactory().CreateLogger("FileMonitor");
|
||
|
||
Log.Information("高性能文件监控程序启动中...");
|
||
|
||
// 初始化事件存储
|
||
if (_config.EventStorage.EnableEventStorage)
|
||
{
|
||
_eventStorage = new EventStorage(_config, _logger);
|
||
Log.Information("事件存储已启用,存储目录:{Directory}", _config.EventStorage.StorageDirectory);
|
||
}
|
||
|
||
// 打印配置信息
|
||
LogConfiguration();
|
||
|
||
// 获取监控路径:优先命令行参数,其次配置文件,最后使用当前目录
|
||
string pathToMonitor = args.Length > 0 ? args[0] :
|
||
!string.IsNullOrEmpty(_config.General.DefaultMonitorPath) ?
|
||
_config.General.DefaultMonitorPath : Directory.GetCurrentDirectory();
|
||
|
||
// 命令行参数可以覆盖配置文件
|
||
if (args.Length > 1 && args[1].Equals("--no-filter", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_config.General.EnableFileFiltering = false;
|
||
Log.Information("通过命令行参数禁用文件类型过滤,将监控所有文件");
|
||
}
|
||
else if (_config.General.EnableFileFiltering)
|
||
{
|
||
Log.Information("已启用文件类型过滤,仅监控以下类型: {Extensions}", string.Join(", ", _config.FileFilters.AllowedExtensions));
|
||
}
|
||
|
||
if (!Directory.Exists(pathToMonitor))
|
||
{
|
||
Log.Warning("错误:监控目录 '{Path}' 不存在。", pathToMonitor);
|
||
Log.Information("尝试创建该目录...");
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(pathToMonitor);
|
||
Log.Information("已成功创建目录: {Path}", pathToMonitor);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "创建目录失败: {Message}", ex.Message);
|
||
Log.Information("将使用当前目录作为监控路径。");
|
||
pathToMonitor = Directory.GetCurrentDirectory();
|
||
}
|
||
}
|
||
|
||
Log.Information("开始监控目录: {Path}", pathToMonitor);
|
||
Log.Information("按 'Q' 退出程序,按 'R' 重新加载配置,按 'H' 检查监控器健康状态。");
|
||
if (_config.EventStorage.EnableEventStorage && _config.EventStorage.EnableEventReplay)
|
||
{
|
||
Log.Information("按 'P' 开始/暂停回放,按 'S' 停止回放,按 'B' 查询事件。");
|
||
}
|
||
|
||
// 创建一个容量由配置文件指定的有界通道
|
||
var channel = Channel.CreateBounded<FileSystemEventArgs>(new BoundedChannelOptions(_config.Performance.ChannelCapacity)
|
||
{
|
||
FullMode = BoundedChannelFullMode.DropOldest // 当通道满时丢弃旧事件
|
||
});
|
||
|
||
using var cts = new CancellationTokenSource();
|
||
|
||
// 启动内存监控
|
||
var memoryMonitorTask = StartMemoryMonitorAsync(cts.Token);
|
||
|
||
// 启动文件监控器
|
||
var monitorTask = StartFileMonitorAsync(pathToMonitor, channel.Writer, cts.Token);
|
||
|
||
// 启动健康监控任务
|
||
var healthMonitorTask = _config.Robustness.EnableAutoRecovery ?
|
||
StartWatcherHealthMonitorAsync(pathToMonitor, channel.Writer, cts.Token) : Task.CompletedTask;
|
||
|
||
// 启动事件处理器
|
||
var processorTask = ProcessEventsAsync(channel.Reader, cts.Token);
|
||
|
||
// 等待用户按下键退出或重新加载配置
|
||
bool needRestart = false;
|
||
while (true)
|
||
{
|
||
var key = Console.ReadKey(true).Key;
|
||
if (key == ConsoleKey.Q)
|
||
{
|
||
break; // 退出循环
|
||
}
|
||
else if (key == ConsoleKey.R)
|
||
{
|
||
Log.Information("正在重新加载配置...");
|
||
LoadConfiguration();
|
||
// 重新配置Serilog
|
||
ConfigureSerilog();
|
||
LogConfiguration();
|
||
Log.Information("配置已重新加载,部分配置需要重启程序才能生效。");
|
||
}
|
||
else if (key == ConsoleKey.H)
|
||
{
|
||
// 手动检查监控器健康状态
|
||
bool isHealthy = _activeWatcher != null &&
|
||
FileWatcherUtils.IsWatcherHealthy(_activeWatcher, _lastEventTime, _config.Robustness);
|
||
Log.Information("监控器健康状态: {Status}", (isHealthy ? "正常" : "异常"));
|
||
Log.Information("上次事件时间: {Time}", _lastEventTime);
|
||
Log.Information("重启次数: {Count}/{MaxCount}", _watcherRestartAttempts, _config.Robustness.MaxRestartAttempts);
|
||
Log.Information("已处理事件总数: {Count}", _totalEventsProcessed);
|
||
}
|
||
else if (key == ConsoleKey.P && _config.EventStorage.EnableEventStorage && _config.EventStorage.EnableEventReplay)
|
||
{
|
||
await HandleEventReplayToggleAsync();
|
||
}
|
||
else if (key == ConsoleKey.S && _config.EventStorage.EnableEventStorage && _config.EventStorage.EnableEventReplay)
|
||
{
|
||
await StopReplayAsync();
|
||
}
|
||
else if (key == ConsoleKey.B && _config.EventStorage.EnableEventStorage)
|
||
{
|
||
await QueryEventsAsync();
|
||
}
|
||
|
||
await Task.Delay(100);
|
||
}
|
||
|
||
// 取消所有任务
|
||
cts.Cancel();
|
||
|
||
// 停止回放(如果正在进行)
|
||
await StopReplayAsync();
|
||
|
||
// 释放事件存储
|
||
_eventStorage?.Dispose();
|
||
|
||
try
|
||
{
|
||
var tasks = new List<Task> { monitorTask, processorTask, memoryMonitorTask };
|
||
if (_config.Robustness.EnableAutoRecovery)
|
||
{
|
||
tasks.Add(healthMonitorTask);
|
||
}
|
||
|
||
await Task.WhenAll(tasks);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
Log.Information("程序已正常退出。");
|
||
}
|
||
|
||
// 关闭Serilog
|
||
Log.CloseAndFlush();
|
||
}
|
||
|
||
// 配置Serilog
|
||
private static void ConfigureSerilog()
|
||
{
|
||
var configuration = new ConfigurationBuilder()
|
||
.SetBasePath(Directory.GetCurrentDirectory())
|
||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||
.Build();
|
||
|
||
var loggerConfig = new LoggerConfiguration()
|
||
.ReadFrom.Configuration(configuration);
|
||
|
||
// 根据配置设置最小日志级别
|
||
LogEventLevel minimumLevel = LogEventLevel.Information;
|
||
if (!string.IsNullOrEmpty(_config.Logging?.LogLevel))
|
||
{
|
||
switch (_config.Logging.LogLevel.ToLower())
|
||
{
|
||
case "verbose":
|
||
minimumLevel = LogEventLevel.Verbose;
|
||
break;
|
||
case "debug":
|
||
minimumLevel = LogEventLevel.Debug;
|
||
break;
|
||
case "information":
|
||
minimumLevel = LogEventLevel.Information;
|
||
break;
|
||
case "warning":
|
||
minimumLevel = LogEventLevel.Warning;
|
||
break;
|
||
case "error":
|
||
minimumLevel = LogEventLevel.Error;
|
||
break;
|
||
case "fatal":
|
||
minimumLevel = LogEventLevel.Fatal;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果配置文件中没有Serilog节,使用默认配置
|
||
var hasConfig = configuration.GetSection("Serilog").Exists();
|
||
if (!hasConfig)
|
||
{
|
||
loggerConfig
|
||
.MinimumLevel.Is(minimumLevel)
|
||
.WriteTo.Console(
|
||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
|
||
)
|
||
.WriteTo.File(
|
||
Path.Combine(_config.Logging?.LogDirectory ?? "Logs", "filemonitor-.log"),
|
||
rollingInterval: RollingInterval.Day,
|
||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] {Message:lj}{NewLine}{Exception}",
|
||
retainedFileCountLimit: _config.Logging?.RetainedLogDays ?? 30
|
||
);
|
||
}
|
||
|
||
Log.Logger = loggerConfig.CreateLogger();
|
||
}
|
||
|
||
// 查询事件
|
||
private static async Task QueryEventsAsync()
|
||
{
|
||
if (_eventStorage == null)
|
||
{
|
||
Log.Warning("事件存储未启用");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
Log.Information("\n================ 事件查询 ================");
|
||
Console.WriteLine("请输入查询参数,直接回车使用默认值");
|
||
|
||
// 收集查询参数
|
||
var queryParams = new EventQueryParams();
|
||
|
||
Console.Write("开始时间 (yyyy-MM-dd HH:mm:ss),默认24小时前: ");
|
||
string startTimeStr = Console.ReadLine();
|
||
if (!string.IsNullOrEmpty(startTimeStr))
|
||
{
|
||
if (DateTime.TryParse(startTimeStr, out var startTime))
|
||
{
|
||
queryParams.StartTime = startTime;
|
||
}
|
||
else
|
||
{
|
||
Log.Warning("无效的日期格式,使用默认值");
|
||
queryParams.StartTime = DateTime.Now.AddDays(-1);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
queryParams.StartTime = DateTime.Now.AddDays(-1);
|
||
}
|
||
|
||
Console.Write("结束时间 (yyyy-MM-dd HH:mm:ss),默认当前时间: ");
|
||
string endTimeStr = Console.ReadLine();
|
||
if (!string.IsNullOrEmpty(endTimeStr))
|
||
{
|
||
if (DateTime.TryParse(endTimeStr, out var endTime))
|
||
{
|
||
queryParams.EndTime = endTime;
|
||
}
|
||
else
|
||
{
|
||
Log.Warning("无效的日期格式,使用默认值");
|
||
}
|
||
}
|
||
|
||
Console.Write("事件类型 (Created, Modified, Deleted, Renamed, All),默认All: ");
|
||
string eventTypeStr = Console.ReadLine();
|
||
if (!string.IsNullOrEmpty(eventTypeStr) && eventTypeStr.ToLower() != "all")
|
||
{
|
||
if (Enum.TryParse<FileEventType>(eventTypeStr, true, out var eventType))
|
||
{
|
||
queryParams.EventType = eventType;
|
||
}
|
||
else
|
||
{
|
||
Log.Warning("无效的事件类型,使用默认值All");
|
||
}
|
||
}
|
||
|
||
Console.Write("路径过滤 (包含此文本的路径),默认无: ");
|
||
string pathFilter = Console.ReadLine();
|
||
if (!string.IsNullOrEmpty(pathFilter))
|
||
{
|
||
queryParams.PathFilter = pathFilter;
|
||
}
|
||
|
||
Console.Write("文件扩展名过滤 (.txt, .exe等),默认无: ");
|
||
string extensionFilter = Console.ReadLine();
|
||
if (!string.IsNullOrEmpty(extensionFilter))
|
||
{
|
||
queryParams.ExtensionFilter = extensionFilter;
|
||
}
|
||
|
||
Console.Write("每页显示数量,默认20: ");
|
||
string pageSizeStr = Console.ReadLine();
|
||
if (!string.IsNullOrEmpty(pageSizeStr) && int.TryParse(pageSizeStr, out var pageSize))
|
||
{
|
||
queryParams.PageSize = pageSize;
|
||
}
|
||
else
|
||
{
|
||
queryParams.PageSize = 20;
|
||
}
|
||
|
||
// 执行查询
|
||
var result = await _eventStorage.QueryEventsAsync(queryParams);
|
||
|
||
Log.Information("查询结果 (共{Count}条记录):", result.TotalCount);
|
||
Log.Information("时间范围: {StartTime} 至 {EndTime}", result.StartTime, result.EndTime);
|
||
Console.WriteLine("---------------------------------------------");
|
||
|
||
if (result.Events.Count == 0)
|
||
{
|
||
Log.Warning("未找到符合条件的事件");
|
||
}
|
||
else
|
||
{
|
||
foreach (var evt in result.Events)
|
||
{
|
||
string eventType = evt.EventType.ToString();
|
||
string timeStr = evt.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||
|
||
Console.WriteLine($"[{timeStr}] [{eventType}] {evt.FileName}");
|
||
Console.WriteLine($" 路径: {evt.Directory}");
|
||
|
||
if (evt.EventType == FileEventType.Renamed && !string.IsNullOrEmpty(evt.OldFileName))
|
||
{
|
||
Console.WriteLine($" 原名称: {evt.OldFileName}");
|
||
}
|
||
|
||
Console.WriteLine();
|
||
}
|
||
|
||
if (result.HasMore)
|
||
{
|
||
Log.Warning("... 还有更多记录未显示 ...");
|
||
}
|
||
}
|
||
|
||
// 询问是否开始回放
|
||
if (result.Events.Count > 0 && _config.EventStorage.EnableEventReplay)
|
||
{
|
||
Console.Write("\n是否要回放这些事件? (Y/N): ");
|
||
string replayAnswer = Console.ReadLine();
|
||
if (!string.IsNullOrEmpty(replayAnswer) && replayAnswer.ToUpper().StartsWith("Y"))
|
||
{
|
||
await StartReplayAsync(queryParams);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "查询事件时出错: {Message}", ex.Message);
|
||
}
|
||
|
||
Console.WriteLine("=============================================\n");
|
||
}
|
||
|
||
// 处理回放切换(开始/暂停)
|
||
private static async Task HandleEventReplayToggleAsync()
|
||
{
|
||
if (_eventStorage == null) return;
|
||
|
||
try
|
||
{
|
||
if (_currentReplaySession == null)
|
||
{
|
||
// 如果没有活动的回放会话,创建一个简单的查询参数并开始回放
|
||
var queryParams = new EventQueryParams
|
||
{
|
||
StartTime = DateTime.Now.AddHours(-1),
|
||
PageSize = 1000
|
||
};
|
||
|
||
await StartReplayAsync(queryParams);
|
||
}
|
||
else if (_currentReplaySession.IsPaused)
|
||
{
|
||
// 恢复回放
|
||
await _currentReplaySession.ResumeAsync();
|
||
Log.Information("已恢复事件回放");
|
||
}
|
||
else
|
||
{
|
||
// 暂停回放
|
||
await _currentReplaySession.PauseAsync();
|
||
Log.Information("已暂停事件回放");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "处理回放时出错: {Message}", ex.Message);
|
||
}
|
||
}
|
||
|
||
// 开始回放
|
||
private static async Task StartReplayAsync(EventQueryParams queryParams)
|
||
{
|
||
if (_eventStorage == null || !_config.EventStorage.EnableEventReplay) return;
|
||
|
||
try
|
||
{
|
||
// 停止现有回放
|
||
await StopReplayAsync();
|
||
|
||
// 创建回放处理器
|
||
Func<FileEvent, Task> replayHandler = async (e) =>
|
||
{
|
||
// 在控制台上显示回放的事件
|
||
string eventType = e.EventType.ToString();
|
||
string timeStr = e.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||
|
||
Log.Information("[回放] [{Time}] [{Type}] {FileName}", timeStr, eventType, e.FileName);
|
||
|
||
if (_config.Logging.LogFileEventDetails)
|
||
{
|
||
Log.Debug(" 路径: {Path}", e.Directory);
|
||
|
||
if (e.EventType == FileEventType.Renamed && !string.IsNullOrEmpty(e.OldFileName))
|
||
{
|
||
Log.Debug(" 原名称: {OldName}", e.OldFileName);
|
||
}
|
||
}
|
||
|
||
// 这里可以添加实际的文件操作,比如创建、删除文件等
|
||
// 但在此示例中,我们只是显示事件而不执行实际操作
|
||
|
||
await Task.CompletedTask;
|
||
};
|
||
|
||
// 开始回放会话
|
||
_currentReplaySession = await _eventStorage.StartReplayAsync(
|
||
queryParams,
|
||
replayHandler,
|
||
CancellationToken.None);
|
||
|
||
// 如果会话创建成功,开始回放
|
||
if (_currentReplaySession != null)
|
||
{
|
||
await _currentReplaySession.StartAsync();
|
||
Log.Information("开始回放事件,共 {Count} 个事件", _currentReplaySession.TotalEvents);
|
||
Log.Information("回放速度: {Speed}", (_config.EventStorage.ReplaySpeedFactor > 1 ?
|
||
$"{_config.EventStorage.ReplaySpeedFactor}x (加速)" :
|
||
$"{_config.EventStorage.ReplaySpeedFactor}x (减速)"));
|
||
}
|
||
else
|
||
{
|
||
Log.Warning("未找到符合条件的事件,回放未开始");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "开始回放时出错: {Message}", ex.Message);
|
||
}
|
||
}
|
||
|
||
// 停止回放
|
||
private static async Task StopReplayAsync()
|
||
{
|
||
if (_currentReplaySession != null)
|
||
{
|
||
try
|
||
{
|
||
await _currentReplaySession.StopAsync();
|
||
Log.Information("已停止事件回放");
|
||
|
||
// 输出回放统计
|
||
Log.Information("回放统计: 共处理 {Processed}/{Total} 个事件",
|
||
_currentReplaySession.ProcessedEvents,
|
||
_currentReplaySession.TotalEvents);
|
||
|
||
TimeSpan duration = (_currentReplaySession.EndTime ?? DateTime.Now) - _currentReplaySession.StartTime;
|
||
Log.Information("回放持续时间: {Duration:F1} 秒", duration.TotalSeconds);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "停止回放时出错: {Message}", ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
_currentReplaySession.Dispose();
|
||
_currentReplaySession = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载配置文件
|
||
private static void LoadConfiguration()
|
||
{
|
||
try
|
||
{
|
||
// 检查配置文件是否存在
|
||
string configPath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
|
||
if (!File.Exists(configPath))
|
||
{
|
||
Log.Warning("配置文件不存在,使用默认配置。");
|
||
|
||
// 使用默认配置
|
||
_config = new FileMonitorConfig
|
||
{
|
||
General = new GeneralConfig
|
||
{
|
||
EnableFileFiltering = true,
|
||
MemoryMonitorIntervalMinutes = 1,
|
||
DefaultMonitorPath = Path.Combine(Directory.GetCurrentDirectory(), "MonitorFiles")
|
||
},
|
||
FileFilters = new FileFiltersConfig
|
||
{
|
||
AllowedExtensions = new[] { ".dll" },
|
||
ExcludedDirectories = new[] { "bin", "obj", "node_modules" },
|
||
IncludeSubdirectories = true
|
||
},
|
||
Performance = new PerformanceConfig
|
||
{
|
||
MemoryCleanupThreshold = 5000,
|
||
ChannelCapacity = 1000,
|
||
EventDebounceTimeSeconds = 3,
|
||
MaxDictionarySize = 10000,
|
||
CleanupIntervalSeconds = 5,
|
||
ProcessingDelayMs = 5
|
||
},
|
||
Robustness = new RobustnessConfig
|
||
{
|
||
EnableAutoRecovery = true,
|
||
WatcherHealthCheckIntervalSeconds = 30,
|
||
WatcherTimeoutSeconds = 60,
|
||
MaxRestartAttempts = 3,
|
||
RestartDelaySeconds = 5,
|
||
EnableFileLockDetection = true,
|
||
LockedFileStrategy = "Retry",
|
||
FileLockRetryCount = 3,
|
||
FileLockRetryDelayMs = 500
|
||
},
|
||
NotifyFilters = new List<string> { "LastWrite", "FileName", "DirectoryName", "CreationTime" },
|
||
EventStorage = new EventStorageConfig
|
||
{
|
||
EnableEventStorage = false,
|
||
StorageDirectory = Path.Combine(Directory.GetCurrentDirectory(), "EventStorage"),
|
||
EnableEventReplay = false,
|
||
ReplaySpeedFactor = 1
|
||
},
|
||
Logging = new LoggingConfig
|
||
{
|
||
LogLevel = "Information",
|
||
LogFileEventDetails = false,
|
||
RetainedLogDays = 30,
|
||
LogDirectory = "Logs"
|
||
}
|
||
};
|
||
|
||
return;
|
||
}
|
||
|
||
var builder = new ConfigurationBuilder()
|
||
.SetBasePath(Directory.GetCurrentDirectory())
|
||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
||
|
||
var configuration = builder.Build();
|
||
|
||
// 绑定配置到对象
|
||
var config = new FileMonitorConfig();
|
||
configuration.GetSection("FileMonitorConfig").Bind(config);
|
||
|
||
// 更新全局配置
|
||
_config = config;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "加载配置文件失败: {Message}", ex.Message);
|
||
Log.Warning("将使用默认配置。");
|
||
|
||
// 使用默认配置
|
||
_config = new FileMonitorConfig
|
||
{
|
||
General = new GeneralConfig
|
||
{
|
||
EnableFileFiltering = true,
|
||
MemoryMonitorIntervalMinutes = 1,
|
||
DefaultMonitorPath = Path.Combine(Directory.GetCurrentDirectory(), "MonitorFiles")
|
||
},
|
||
FileFilters = new FileFiltersConfig
|
||
{
|
||
AllowedExtensions = new[] { ".dll" },
|
||
ExcludedDirectories = new[] { "bin", "obj", "node_modules" },
|
||
IncludeSubdirectories = true
|
||
},
|
||
Performance = new PerformanceConfig
|
||
{
|
||
MemoryCleanupThreshold = 5000,
|
||
ChannelCapacity = 1000,
|
||
EventDebounceTimeSeconds = 3,
|
||
MaxDictionarySize = 10000,
|
||
CleanupIntervalSeconds = 5,
|
||
ProcessingDelayMs = 5
|
||
},
|
||
Robustness = new RobustnessConfig
|
||
{
|
||
EnableAutoRecovery = true,
|
||
WatcherHealthCheckIntervalSeconds = 30,
|
||
WatcherTimeoutSeconds = 60,
|
||
MaxRestartAttempts = 3,
|
||
RestartDelaySeconds = 5,
|
||
EnableFileLockDetection = true,
|
||
LockedFileStrategy = "Retry",
|
||
FileLockRetryCount = 3,
|
||
FileLockRetryDelayMs = 500
|
||
},
|
||
NotifyFilters = new List<string> { "LastWrite", "FileName", "DirectoryName", "CreationTime" },
|
||
EventStorage = new EventStorageConfig
|
||
{
|
||
EnableEventStorage = false,
|
||
StorageDirectory = Path.Combine(Directory.GetCurrentDirectory(), "EventStorage"),
|
||
EnableEventReplay = false,
|
||
ReplaySpeedFactor = 1
|
||
},
|
||
Logging = new LoggingConfig
|
||
{
|
||
LogLevel = "Information",
|
||
LogFileEventDetails = false,
|
||
RetainedLogDays = 30,
|
||
LogDirectory = "Logs"
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
// 打印当前配置信息
|
||
private static void LogConfiguration()
|
||
{
|
||
Log.Information("当前配置信息:");
|
||
Log.Information(" 默认监控路径: {Path}", _config.General.DefaultMonitorPath);
|
||
Log.Information(" 文件过滤: {Status}", (_config.General.EnableFileFiltering ? "启用" : "禁用"));
|
||
if (_config.General.EnableFileFiltering)
|
||
{
|
||
Log.Information(" 监控文件类型: {Types}", string.Join(", ", _config.FileFilters.AllowedExtensions));
|
||
Log.Information(" 排除目录: {Dirs}", string.Join(", ", _config.FileFilters.ExcludedDirectories));
|
||
}
|
||
Log.Information(" 内存监控间隔: {Minutes}分钟", _config.General.MemoryMonitorIntervalMinutes);
|
||
Log.Information(" 内存清理阈值: 每{Threshold}个事件", _config.Performance.MemoryCleanupThreshold);
|
||
Log.Information(" 通道容量: {Capacity}", _config.Performance.ChannelCapacity);
|
||
Log.Information(" 事件去抖时间: {Seconds}秒", _config.Performance.EventDebounceTimeSeconds);
|
||
Log.Information(" 自动恢复: {Status}", (_config.Robustness.EnableAutoRecovery ? "启用" : "禁用"));
|
||
Log.Information(" 文件锁检测: {Status}", (_config.Robustness.EnableFileLockDetection ? "启用" : "禁用"));
|
||
if (_config.Robustness.EnableFileLockDetection)
|
||
{
|
||
Log.Information(" 锁定文件策略: {Strategy}", _config.Robustness.LockedFileStrategy);
|
||
}
|
||
Log.Information(" 日志级别: {Level}", _config.Logging?.LogLevel ?? "Information");
|
||
}
|
||
|
||
// 检查文件是否应该被处理的方法
|
||
private static bool ShouldProcessFile(string filePath)
|
||
{
|
||
if (!_config.General.EnableFileFiltering)
|
||
return true;
|
||
|
||
if (string.IsNullOrEmpty(filePath))
|
||
return false;
|
||
|
||
// 检查是否在排除目录中
|
||
foreach (var excludedDir in _config.FileFilters.ExcludedDirectories)
|
||
{
|
||
if (filePath.Contains($"{Path.DirectorySeparatorChar}{excludedDir}{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) ||
|
||
filePath.EndsWith($"{Path.DirectorySeparatorChar}{excludedDir}", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
string extension = Path.GetExtension(filePath);
|
||
return _config.FileFilters.AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
|
||
// 添加内存监控方法
|
||
private static async Task StartMemoryMonitorAsync(CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
// 等待指定时间
|
||
await Task.Delay(TimeSpan.FromMinutes(_config.General.MemoryMonitorIntervalMinutes), cancellationToken);
|
||
|
||
// 获取当前内存使用情况
|
||
long currentMemory = GC.GetTotalMemory(false) / (1024 * 1024); // MB
|
||
|
||
Log.Information("内存使用: {Memory} MB, 已处理事件: {EventCount}", currentMemory, _totalEventsProcessed);
|
||
|
||
// 强制执行垃圾回收
|
||
GC.Collect();
|
||
GC.WaitForPendingFinalizers();
|
||
|
||
// 显示垃圾回收后的内存
|
||
long afterGCMemory = GC.GetTotalMemory(true) / (1024 * 1024); // MB
|
||
Log.Debug("内存清理后: {Memory} MB (释放: {Released} MB)", afterGCMemory, currentMemory - afterGCMemory);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// 预期的取消异常
|
||
}
|
||
}
|
||
|
||
// 创建文件监控器
|
||
private static FileSystemWatcher CreateFileWatcher(string path, ChannelWriter<FileSystemEventArgs> writer, CancellationToken cancellationToken)
|
||
{
|
||
// 创建一个新的文件系统监控器
|
||
var watcher = new FileSystemWatcher(path);
|
||
|
||
// 从配置设置NotifyFilters
|
||
NotifyFilters notifyFilters = NotifyFilters.LastWrite; // 默认值
|
||
foreach (var filterName in _config.NotifyFilters)
|
||
{
|
||
if (Enum.TryParse<NotifyFilters>(filterName, out var filter))
|
||
{
|
||
notifyFilters |= filter;
|
||
}
|
||
}
|
||
|
||
watcher.NotifyFilter = notifyFilters;
|
||
watcher.IncludeSubdirectories = _config.FileFilters.IncludeSubdirectories;
|
||
watcher.EnableRaisingEvents = true;
|
||
|
||
// 设置文件过滤器,如果启用了过滤
|
||
if (_config.General.EnableFileFiltering && _config.FileFilters.AllowedExtensions.Length > 0)
|
||
{
|
||
// 在FileSystemWatcher级别设置过滤器,这样可以减少系统生成的事件数量
|
||
// 只能设置一个扩展名作为Filter,所以我们选择第一个
|
||
string firstExtension = _config.FileFilters.AllowedExtensions[0];
|
||
watcher.Filter = $"*{firstExtension}";
|
||
Log.Information("已设置文件系统监控过滤器: *{Filter}", firstExtension);
|
||
|
||
if (_config.FileFilters.AllowedExtensions.Length > 1)
|
||
{
|
||
Log.Warning("注意: FileSystemWatcher只支持单一过滤器,其他文件类型将在事件处理时过滤。");
|
||
}
|
||
}
|
||
|
||
// 注册事件处理程序
|
||
watcher.Created += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
|
||
watcher.Changed += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
|
||
watcher.Deleted += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
|
||
watcher.Renamed += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
|
||
watcher.Error += (sender, e) =>
|
||
{
|
||
Log.Error(e.GetException(), "文件监控错误: {Message}", e.GetException().Message);
|
||
};
|
||
|
||
return watcher;
|
||
}
|
||
|
||
// 文件事件处理
|
||
private static void HandleFileEvent(object sender, FileSystemEventArgs e, ChannelWriter<FileSystemEventArgs> writer, CancellationToken cancellationToken)
|
||
{
|
||
// 更新最后事件时间
|
||
_lastEventTime = DateTime.UtcNow;
|
||
|
||
// 如果启用了事件存储,记录事件
|
||
_eventStorage?.RecordEvent(e);
|
||
|
||
try
|
||
{
|
||
// 如果是退出信号,忽略后续处理
|
||
if (cancellationToken.IsCancellationRequested)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 只处理指定类型的文件,如果启用了文件过滤
|
||
if (!ShouldProcessFile(e.FullPath))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 尝试将事件写入通道,如果满了就丢弃
|
||
// 不等待,以免阻塞文件系统事件
|
||
if (!writer.TryWrite(e))
|
||
{
|
||
Log.Warning("警告: 事件处理队列已满,部分事件被丢弃");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 捕获任何异常,防止崩溃
|
||
Log.Error(ex, "处理文件事件时出错: {Message}", ex.Message);
|
||
}
|
||
}
|
||
|
||
private static async Task StartFileMonitorAsync(string path, ChannelWriter<FileSystemEventArgs> writer, CancellationToken cancellationToken)
|
||
{
|
||
// 使用TaskCompletionSource来控制任务的完成
|
||
var tcs = new TaskCompletionSource<bool>();
|
||
|
||
try
|
||
{
|
||
// 创建并启动监控器
|
||
_activeWatcher = CreateFileWatcher(path, writer, cancellationToken);
|
||
|
||
// 注册取消回调
|
||
cancellationToken.Register(() =>
|
||
{
|
||
try
|
||
{
|
||
if (_activeWatcher != null)
|
||
{
|
||
// 确保所有资源正确清理
|
||
_activeWatcher.EnableRaisingEvents = false;
|
||
_activeWatcher.Dispose();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "关闭文件监控器时出错: {Message}", ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
tcs.TrySetResult(true);
|
||
}
|
||
});
|
||
|
||
// 等待任务被取消或完成
|
||
await tcs.Task;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "启动文件监控器时出错: {Message}", ex.Message);
|
||
tcs.TrySetException(ex);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private static async Task ProcessEventsAsync(ChannelReader<FileSystemEventArgs> reader, CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
// 使用配置的字典大小
|
||
var recentlyProcessed = new ConcurrentDictionary<string, DateTime>();
|
||
|
||
// 清理计时器,频率由配置决定
|
||
using var cleanupTimer = new Timer(_ =>
|
||
{
|
||
try
|
||
{
|
||
var cutoff = DateTime.UtcNow.AddSeconds(-_config.Performance.EventDebounceTimeSeconds);
|
||
int removedCount = 0;
|
||
|
||
// 限制字典大小
|
||
if (recentlyProcessed.Count > _config.Performance.MaxDictionarySize)
|
||
{
|
||
// 找出最旧的条目删除
|
||
var oldestItems = recentlyProcessed
|
||
.OrderBy(kv => kv.Value)
|
||
.Take(recentlyProcessed.Count - _config.Performance.MaxDictionarySize / 2);
|
||
|
||
foreach (var item in oldestItems)
|
||
{
|
||
DateTime dummy;
|
||
if (recentlyProcessed.TryRemove(item.Key, out dummy))
|
||
{
|
||
removedCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 常规清理
|
||
foreach (var key in recentlyProcessed.Keys)
|
||
{
|
||
if (recentlyProcessed.TryGetValue(key, out var time) && time < cutoff)
|
||
{
|
||
DateTime dummy;
|
||
if (recentlyProcessed.TryRemove(key, out dummy))
|
||
{
|
||
removedCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (removedCount > 0)
|
||
{
|
||
Log.Debug("已清理 {Count} 个过期事件记录", removedCount);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "清理定时器错误: {Message}", ex.Message);
|
||
}
|
||
}, null, TimeSpan.FromSeconds(_config.Performance.CleanupIntervalSeconds),
|
||
TimeSpan.FromSeconds(_config.Performance.CleanupIntervalSeconds));
|
||
|
||
// 从通道读取事件并处理
|
||
await foreach (var e in reader.ReadAllAsync(cancellationToken))
|
||
{
|
||
// 再次确认文件扩展名,双重检查
|
||
if (!ShouldProcessFile(e.FullPath))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 增加计数器
|
||
long count = Interlocked.Increment(ref _totalEventsProcessed);
|
||
|
||
// 周期性清理内存
|
||
if (count % _config.Performance.MemoryCleanupThreshold == 0)
|
||
{
|
||
// 在后台线程执行垃圾回收,但直接等待完成避免警告
|
||
await Task.Run(() =>
|
||
{
|
||
GC.Collect();
|
||
GC.WaitForPendingFinalizers();
|
||
});
|
||
}
|
||
|
||
// 只使用文件路径作为键,忽略事件类型,这样同一文件的创建和修改事件会被视为同一事件
|
||
var key = e.FullPath;
|
||
|
||
// 对于创建事件,如果之前有相同文件的其他事件,则跳过
|
||
if (e.ChangeType == WatcherChangeTypes.Changed &&
|
||
recentlyProcessed.TryGetValue(key, out var _))
|
||
{
|
||
// 如果是修改事件且文件最近被处理过,则跳过
|
||
continue;
|
||
}
|
||
|
||
// 更新或添加文件处理时间,使用原子操作
|
||
recentlyProcessed[key] = DateTime.UtcNow;
|
||
|
||
string eventType = e.ChangeType switch
|
||
{
|
||
WatcherChangeTypes.Created => "创建",
|
||
WatcherChangeTypes.Deleted => "删除",
|
||
WatcherChangeTypes.Changed => "修改",
|
||
WatcherChangeTypes.Renamed => "重命名",
|
||
_ => "未知"
|
||
};
|
||
|
||
string fileName = e.Name ?? Path.GetFileName(e.FullPath);
|
||
string directoryName = Path.GetDirectoryName(e.FullPath) ?? string.Empty;
|
||
|
||
Log.Information("{EventType}: {FileName}", eventType, fileName);
|
||
|
||
if (_config.Logging.LogFileEventDetails)
|
||
{
|
||
Log.Debug(" 路径: {Path}", directoryName);
|
||
|
||
var extension = Path.GetExtension(e.FullPath);
|
||
Log.Debug(" 类型: {Extension}", extension);
|
||
|
||
if (e is RenamedEventArgs renamedEvent)
|
||
{
|
||
Log.Debug(" 原名称: {OldName}", Path.GetFileName(renamedEvent.OldFullPath));
|
||
}
|
||
}
|
||
|
||
// 延迟可配置
|
||
await Task.Delay(_config.Performance.ProcessingDelayMs, cancellationToken);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// 预期的取消异常,正常退出
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "处理事件时发生错误: {Message}", ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
// 确保资源释放
|
||
GC.Collect();
|
||
GC.WaitForPendingFinalizers();
|
||
}
|
||
}
|
||
|
||
// 健康监控任务,监视文件监控器的状态并在需要时重启它
|
||
private static async Task StartWatcherHealthMonitorAsync(
|
||
string path, ChannelWriter<FileSystemEventArgs> writer, CancellationToken cancellationToken)
|
||
{
|
||
Log.Information("已启动监控器健康检查,间隔: {Seconds}秒", _config.Robustness.WatcherHealthCheckIntervalSeconds);
|
||
|
||
try
|
||
{
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
// 等待指定的健康检查间隔
|
||
await Task.Delay(TimeSpan.FromSeconds(_config.Robustness.WatcherHealthCheckIntervalSeconds),
|
||
cancellationToken);
|
||
|
||
// 如果监控器不健康且重启次数未超过上限,尝试重启
|
||
if (_activeWatcher != null &&
|
||
!FileWatcherUtils.IsWatcherHealthy(_activeWatcher, _lastEventTime, _config.Robustness) &&
|
||
_watcherRestartAttempts < _config.Robustness.MaxRestartAttempts)
|
||
{
|
||
// 停止当前的监控器
|
||
Log.Warning("检测到监控器异常,准备重启...");
|
||
|
||
try
|
||
{
|
||
_activeWatcher.EnableRaisingEvents = false;
|
||
_activeWatcher.Dispose();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "停止异常监控器时出错: {Message}", ex.Message);
|
||
}
|
||
|
||
_watcherRestartAttempts++;
|
||
Log.Warning("正在重启监控器,尝试次数: {Current}/{Max}",
|
||
_watcherRestartAttempts, _config.Robustness.MaxRestartAttempts);
|
||
|
||
// 等待指定的重启延迟时间
|
||
await Task.Delay(TimeSpan.FromSeconds(_config.Robustness.RestartDelaySeconds),
|
||
cancellationToken);
|
||
|
||
// 创建新的监控器
|
||
try
|
||
{
|
||
_activeWatcher = CreateFileWatcher(path, writer, cancellationToken);
|
||
_lastEventTime = DateTime.UtcNow; // 重置最后事件时间
|
||
Log.Information("监控器已成功重启");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "重启监控器失败: {Message}", ex.Message);
|
||
}
|
||
}
|
||
else if (_watcherRestartAttempts >= _config.Robustness.MaxRestartAttempts)
|
||
{
|
||
Log.Error("警告: 已达到最大重启次数({Max}),不再尝试重启", _config.Robustness.MaxRestartAttempts);
|
||
Log.Warning("请检查文件系统或手动重启程序");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// 预期的取消异常,正常退出
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "健康监控任务异常: {Message}", ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行数据库工具
|
||
/// </summary>
|
||
private static async Task RunDatabaseUtilityAsync(string[] args)
|
||
{
|
||
Console.WriteLine("正在启动SQLite数据库管理工具...");
|
||
|
||
try
|
||
{
|
||
var dbUtility = new DbUtility();
|
||
await dbUtility.ExecuteAsync(args);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"运行数据库工具时发生错误: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|