Files
squad-rain-ops-mini/Helpers/SquadGameLog/SquadLogHelper.cs

403 lines
14 KiB
C#
Raw Permalink 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 Microsoft.Extensions.Caching.Memory;
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.Caching;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using RainOpsMini;
namespace RainOpsMini.Helpers
{
class SquadLogHelper
{
/// <summary>
/// 本地文件模式时日志路径
/// </summary>
public static string filePath = "";
/// <summary>
/// 需要提取的日志白名单
/// </summary>
public static List<string> RegularMatching = "Chat,UChannel::Close:,has created Squad,LogSquad: PostLogin: NewPlayer,Wound(),Die(),TakeDamage(),LogSquad: Player:,LogSquad: USQGameState: Server Tick Rate,the match with,has revived,ADMIN COMMAND: Change layer,ADMIN COMMAND: Set next layer,Match State Changed from InProgress to WaitingPostMatch,LogTextFormatter".Split(',').ToList();
/// <summary>
/// ftp的文件地址
/// </summary>
public static string ftpUrl = "";
/// <summary>
/// 数据清洗实例
/// </summary>
public static SquadLogCleaning squadLogCleaning = new SquadLogCleaning();
/// <summary>
/// 启动日志监听服务
/// </summary>
/// <param name="path">日志文件路径</param>
public void Run(string path)
{
// 日志路径地址
filePath = path;
// 如果是默认配置路径则查找当前文件夹或上层文件夹(上层文件夹主要为了兼容之前的版本)
if (string.IsNullOrEmpty(filePath) || filePath == "/SquadGame.log")
{
// 获取当前执行路径
string currentDirectory = Directory.GetCurrentDirectory();
// 文件名
string fileName = "SquadGame.log";
// 检查当前目录是否存在该文件
string currentFilePath = Path.Combine(currentDirectory, fileName);
if (File.Exists(currentFilePath))
{
filePath = currentFilePath;
}
else
{
// 如果当前目录没有文件,检查上一层目录
string parentDirectory = Directory.GetParent(currentDirectory)?.FullName;
if (parentDirectory != null)
{
string parentFilePath = Path.Combine(parentDirectory, fileName);
filePath = parentFilePath;
}
}
}
// 等待文件创建
WaitForFileCreation(filePath);
// 首次清洗文件
#region
// 检查本局数据
CheckLatestMatchEndingTime(filePath);
#endregion
// 初始化文件监视器
InitializeFileWatcher();
}
/// <summary>
/// 等待文件创建
/// </summary>
/// <param name="path">文件路径</param>
private static void WaitForFileCreation(string path)
{
while (true)
{
// 日志存在则跳出
if (File.Exists(path))
{
break;
}
// 每秒检查一次
Thread.Sleep(1000);
}
}
/// <summary>
/// 初始化文件监视器
/// </summary>
private static void InitializeFileWatcher()
{
new Thread(() =>
{
// 创建 FileTimestampAndSizeWatcher 实例,监听指定路径的文件
var fileWatcher = new FileTimestampAndSizeWatcher(filePath);
// 绑定文件更新事件
fileWatcher.OnFileUpdated += message =>
{
SendLog(message);
};
// 开始监听文件变化
fileWatcher.Start();
}).Start();
}
/// <summary>
/// 查找最近的对局结束时间,并清洗旧日志
/// </summary>
/// <param name="filePath">日志文件路径</param>
private static void CheckLatestMatchEndingTime(string filePath)
{
// 标记开始清洗时间
DateTime dateTime1 = DateTime.Now;
// 等待 RCON 连接
RconClient? client = null;
int retryCount = 0;
// 等待RCON连接最多等待10秒避免阻塞太久
while (retryCount < 10)
{
client = RainOpsMini.Program.Client;
if (client != null && client.IsConnected)
{
break;
}
Thread.Sleep(1000);
retryCount++;
}
DateTime roundStartTime = DateTime.MinValue;
if (client != null && client.IsConnected)
{
try
{
// 获取服务器信息
string response = client.SendCommandAsync("showserverinfo").GetAwaiter().GetResult();
// 提取 PLAYTIME_I
Match matchPlayTime = Regex.Match(response, @"PLAYTIME_I: (\d+)");
if (matchPlayTime.Success)
{
int playTimeI = int.Parse(matchPlayTime.Groups[1].Value);
// 游戏开始时间要减去准备时间+60的冗余
int val = playTimeI + 300 + 60;
// 根据RCON中的对局运行时间推算日志开始时间戳
roundStartTime = DateTime.Now.AddSeconds(-val);
RainOpsMini.Helpers.RainOpsLog.Log($"[调试] RCON 信息 - 游戏时长: {playTimeI}, 计算出的回合开始时间: {roundStartTime:yyyy-MM-dd HH:mm:ss}");
}
else
{
RainOpsMini.Helpers.RainOpsLog.Log($"[调试] 从响应中匹配 PLAYTIME_I 失败: {response}");
}
}
catch (Exception ex)
{
RainOpsMini.Helpers.RainOpsLog.Log("获取服务器信息失败: " + ex.Message);
}
}
else
{
RainOpsMini.Helpers.RainOpsLog.Log("RCON 未连接,跳过基于游戏时长的日志过滤。");
}
int log_idx = 0;
int matched_count = 0;
int filtered_count = 0;
if (!File.Exists(filePath))
{
RainOpsMini.Helpers.RainOpsLog.Log($"[错误] 日志文件未找到: {filePath}");
return;
}
RainOpsMini.Helpers.RainOpsLog.Log($"[调试] 正在读取日志文件: {filePath}");
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (StreamReader sr = new StreamReader(fs))
{
string line;
while ((line = sr.ReadLine()) != null)
{
// 提取日志时间、线程ID和其他信息
Match match = Regex.Match(line, @"\[(\d{4}\.\d{2}\.\d{2})-(\d{2})\.(\d{2})\.(\d{2}):(\d{3})\]");
if (match.Success)
{
matched_count++;
string datePart = match.Groups[1].Value;
int hour = int.Parse(match.Groups[2].Value);
int minute = int.Parse(match.Groups[3].Value);
int second = int.Parse(match.Groups[4].Value);
int millisecond = int.Parse(match.Groups[5].Value);
// 解析为 DateTime 并+8小时
DateTime dt = DateTime.Parse(datePart).AddHours(hour).AddMinutes(minute).AddSeconds(second).AddHours(8);
// Debug first few lines
if (matched_count <= 5)
{
RainOpsMini.Helpers.RainOpsLog.Log($"[调试] 日志时间: {dt:yyyy-MM-dd HH:mm:ss}, 回合开始时间: {roundStartTime:yyyy-MM-dd HH:mm:ss}, 保留: {dt >= roundStartTime}");
}
// 非本局数据直接过滤
if (roundStartTime != DateTime.MinValue && dt < roundStartTime)
{
filtered_count++;
continue;
}
// 对局结算的日志不能进行清洗防止错误的打乱阵营、不清洗管理员指令防止黄字太多
//if (!line.Contains("the match with") && !line.Contains("ADMIN COMMAND"))
//{
squadLogCleaning.PushLog(line, true);
//}
log_idx++;
// 每间隔10000行打印一次内存情况
if (log_idx % 10000 == 0)
{
Process currentProcess = Process.GetCurrentProcess();
long memorySize = currentProcess.PrivateMemorySize64; // 以字节为单位
double memorySizeMB = memorySize / 1024.0 / 1024.0; // 转换为 MB
RainOpsMini.Helpers.RainOpsLog.Log($"[调试] 已处理 {log_idx} 条日志。内存使用: {memorySizeMB:F2} MB");
}
}
}
}
RainOpsMini.Helpers.RainOpsLog.Log($"[调试] 日志处理完成。总匹配行数: {matched_count}, 过滤(旧数据): {filtered_count}, 保留: {log_idx}");
DateTime dateTime2 = DateTime.Now;
double Seconds = dateTime2.Subtract(dateTime1).TotalSeconds;
}
/// <summary>
/// 获取文件长度
/// </summary>
/// <param name="path">文件路径</param>
/// <returns>文件长度字节如果出错返回0</returns>
private static long GetFileLength(string path)
{
try
{
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
return fs.Length;
}
}
catch (Exception ex)
{
RainOpsMini.Helpers.RainOpsLog.Log($"获取文件长度时出现错误: {ex.Message}");
return 0;
}
}
/// <summary>
/// 内存缓存MemoryCache 实例
/// </summary>
public static IMemoryCache cache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new MemoryCacheOptions());
/// <summary>
/// 创建一个缓存项策略,设置过期时间为 120 分钟
/// </summary>
public static MemoryCacheEntryOptions policy = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(120)
};
/// <summary>
/// 发送日志数据
/// </summary>
/// <param name="newContent">新的日志内容</param>
public static void SendLog(string newContent)
{
if (IsRelevantLog(newContent))
{
// 空日志跳过
if (string.IsNullOrEmpty(newContent))
{
return;
}
string cacheKey = GetMD5Hash(newContent);
// 检查缓存是否存在
if (cache.TryGetValue(cacheKey, out _))
{
return;
}
// 写入缓存
cache.Set(cacheKey, 1, policy);
// 日志本地解析
squadLogCleaning.PushLog(newContent);
}
}
/// <summary>
/// 检查日志内容是否包含白名单字段
/// </summary>
/// <param name="content">日志内容</param>
/// <returns>如果包含白名单字段返回true否则返回false</returns>
private static bool IsRelevantLog(string content)
{
foreach (var item in RegularMatching)
{
if (!string.IsNullOrEmpty(item) && content.Contains(item))
{
return true;
}
}
return false;
}
/// <summary>
/// 生成唯一ID (基于MAC地址)
/// </summary>
/// <returns>唯一ID字符串</returns>
public static string GenerateUniqueId()
{
NetworkInterface networkInterface = NetworkInterface.GetAllNetworkInterfaces()[0];
byte[] macBytes = networkInterface.GetPhysicalAddress().GetAddressBytes();
using (MD5 md5 = MD5.Create())
{
byte[] hashBytes = md5.ComputeHash(macBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
}
/// <summary>
/// 获取字符串的MD5哈希值
/// </summary>
/// <param name="input">输入字符串</param>
/// <returns>MD5哈希字符串</returns>
public static string GetMD5Hash(string input)
{
// 创建 MD5 实例
using (MD5 md5 = MD5.Create())
{
// 将字符串转换为字节数组
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
// 计算哈希值
byte[] hashBytes = md5.ComputeHash(inputBytes);
// 将字节数组转换为十六进制字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("x2")); // "x2" 表示将字节格式化为两位十六进制
}
return sb.ToString();
}
}
}
}