mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-09 14:45:58 +08:00
403 lines
14 KiB
C#
403 lines
14 KiB
C#
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();
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|