You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1192 lines
49 KiB
1192 lines
49 KiB
using Microsoft.AspNetCore.Mvc;
|
|
using LTEMvcApp.Models;
|
|
using LTEMvcApp.Services;
|
|
using Newtonsoft.Json.Linq;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Threading.Tasks;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace LTEMvcApp.Controllers
|
|
{
|
|
/// <summary>
|
|
/// WebSocket控制器 - 提供LTE客户端WebSocket管理API
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class WebSocketController : ControllerBase
|
|
{
|
|
private readonly WebSocketManagerService _webSocketManager;
|
|
private readonly ILogger<WebSocketController> _logger;
|
|
private readonly string _logsDirectory = "ClientMessageLogs";
|
|
|
|
public WebSocketController(WebSocketManagerService webSocketManager, ILogger<WebSocketController> logger)
|
|
{
|
|
_webSocketManager = webSocketManager;
|
|
_logger = logger;
|
|
|
|
// 确保日志目录存在
|
|
if (!Directory.Exists(_logsDirectory))
|
|
{
|
|
Directory.CreateDirectory(_logsDirectory);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取所有客户端状态
|
|
/// </summary>
|
|
/// <returns>客户端状态列表</returns>
|
|
[HttpGet("clients")]
|
|
public ActionResult<Dictionary<string, ClientState>> GetClientStates()
|
|
{
|
|
_logger.LogInformation("获取所有客户端状态");
|
|
var states = _webSocketManager.GetAllClientStates();
|
|
return Ok(states);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取客户端配置
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <returns>客户端配置</returns>
|
|
[HttpGet("clients/{clientName}/config")]
|
|
public ActionResult<ClientConfig?> GetClientConfig(string clientName)
|
|
{
|
|
var config = _webSocketManager.GetClientConfig(clientName);
|
|
if (config == null)
|
|
return NotFound($"客户端 '{clientName}' 不存在");
|
|
|
|
return Ok(config);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取所有客户端配置
|
|
/// </summary>
|
|
/// <returns>客户端配置列表</returns>
|
|
[HttpGet("configs")]
|
|
public ActionResult<List<ClientConfig>> GetAllConfigs()
|
|
{
|
|
var configs = _webSocketManager.GetAllClientConfigs();
|
|
return Ok(configs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 添加客户端配置
|
|
/// </summary>
|
|
/// <param name="config">客户端配置</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("configs")]
|
|
public ActionResult AddClientConfig([FromBody] ClientConfig config)
|
|
{
|
|
if (string.IsNullOrEmpty(config.Name))
|
|
return BadRequest("客户端名称不能为空");
|
|
|
|
var success = _webSocketManager.AddClientConfig(config);
|
|
if (success)
|
|
return Ok(new { message = $"客户端 '{config.Name}' 配置已添加" });
|
|
else
|
|
return BadRequest("添加客户端配置失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 启动客户端
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("clients/{clientName}/start")]
|
|
public ActionResult StartClient(string clientName)
|
|
{
|
|
_logger.LogInformation($"API请求: 启动客户端 {clientName}");
|
|
var success = _webSocketManager.StartClient(clientName);
|
|
if (success)
|
|
{
|
|
_logger.LogInformation($"客户端 {clientName} 启动成功");
|
|
return Ok(new { message = $"客户端 '{clientName}' 已启动" });
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning($"客户端 {clientName} 启动失败");
|
|
return BadRequest($"启动客户端 '{clientName}' 失败");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止客户端
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("clients/{clientName}/stop")]
|
|
public ActionResult StopClient(string clientName)
|
|
{
|
|
var success = _webSocketManager.StopClient(clientName);
|
|
if (success)
|
|
return Ok(new { message = $"客户端 '{clientName}' 已停止" });
|
|
else
|
|
return BadRequest($"停止客户端 '{clientName}' 失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 播放/暂停客户端
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("clients/{clientName}/playpause")]
|
|
public ActionResult PlayPauseClient(string clientName)
|
|
{
|
|
var success = _webSocketManager.PlayPauseClient(clientName);
|
|
if (success)
|
|
return Ok(new { message = $"客户端 '{clientName}' 播放/暂停状态已切换" });
|
|
else
|
|
return BadRequest($"切换客户端 '{clientName}' 播放/暂停状态失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置客户端日志
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("clients/{clientName}/reset-logs")]
|
|
public ActionResult ResetClientLogs(string clientName)
|
|
{
|
|
var success = _webSocketManager.ResetClientLogs(clientName);
|
|
if (success)
|
|
return Ok(new { message = $"客户端 '{clientName}' 日志已重置" });
|
|
else
|
|
return BadRequest($"重置客户端 '{clientName}' 日志失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取客户端日志
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <param name="limit">日志数量限制</param>
|
|
/// <returns>日志列表</returns>
|
|
[HttpGet("clients/{clientName}/logs")]
|
|
public ActionResult<List<LTELog>?> GetClientLogs(string clientName, [FromQuery] int limit = 100)
|
|
{
|
|
var logs = _webSocketManager.GetClientLogs(clientName);
|
|
if (logs == null)
|
|
return NotFound($"客户端 '{clientName}' 不存在或未连接");
|
|
|
|
// 限制返回的日志数量
|
|
var limitedLogs = logs.TakeLast(limit).ToList();
|
|
return Ok(limitedLogs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置客户端日志配置
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <param name="request">请求体</param>
|
|
[HttpPost("clients/{clientName}/logs-config")]
|
|
public ActionResult SetClientLogsConfig(string clientName, [FromBody] ClientLogsConfig request)
|
|
{
|
|
var success = _webSocketManager.SetClientLogsConfig(clientName, request);
|
|
if (success)
|
|
{
|
|
return Ok(new { message = "日志配置已更新" });
|
|
}
|
|
else
|
|
{
|
|
return NotFound(new { message = $"客户端 '{clientName}' 未找到或更新失败" });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 发送消息到客户端
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <param name="message">消息内容</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("clients/{clientName}/send-message")]
|
|
public ActionResult SendMessage(string clientName, [FromBody] JObject message)
|
|
{
|
|
var messageId = _webSocketManager.SendMessageToClient(clientName, message);
|
|
if (messageId >= 0)
|
|
return Ok(new { messageId, message = $"消息已发送到客户端 '{clientName}'" });
|
|
else
|
|
return BadRequest($"发送消息到客户端 '{clientName}' 失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取连接统计信息
|
|
/// </summary>
|
|
/// <returns>统计信息</returns>
|
|
[HttpGet("statistics")]
|
|
public ActionResult<ConnectionStatistics> GetStatistics()
|
|
{
|
|
var stats = _webSocketManager.GetConnectionStatistics();
|
|
return Ok(stats);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 启动所有已配置的客户端
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("start-all")]
|
|
public ActionResult StartAllClients()
|
|
{
|
|
_webSocketManager.StartAllConfiguredClients();
|
|
return Ok(new { message = "所有已配置的客户端已启动" });
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止所有客户端
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("stop-all")]
|
|
public ActionResult StopAllClients()
|
|
{
|
|
_webSocketManager.StopAllClients();
|
|
return Ok(new { message = "所有客户端已停止" });
|
|
}
|
|
|
|
/// <summary>
|
|
/// 移除客户端配置
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpDelete("configs/{clientName}")]
|
|
public ActionResult RemoveClientConfig(string clientName)
|
|
{
|
|
var success = _webSocketManager.RemoveClientConfig(clientName);
|
|
if (success)
|
|
return Ok(new { message = $"客户端 '{clientName}' 配置已移除" });
|
|
else
|
|
return BadRequest($"移除客户端 '{clientName}' 配置失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 使用 Server-Sent Events (SSE) 实时推送客户端消息
|
|
/// </summary>
|
|
/// <param name="clientName">客户端名称</param>
|
|
[HttpGet("clients/{clientName}/messages/stream")]
|
|
public async Task StreamClientMessages(string clientName)
|
|
{
|
|
try
|
|
{
|
|
Response.ContentType = "text/event-stream";
|
|
Response.Headers.Append("Cache-Control", "no-cache");
|
|
Response.Headers.Append("Connection", "keep-alive");
|
|
Response.Headers.Append("Access-Control-Allow-Origin", "*");
|
|
|
|
var client = _webSocketManager.GetClientInstance(clientName);
|
|
if (client == null)
|
|
{
|
|
// 发送一个错误事件然后关闭
|
|
await SendSseEvent("error", new { message = "客户端未连接或不存在", clientName });
|
|
return;
|
|
}
|
|
|
|
// 发送一个连接成功事件
|
|
await SendSseEvent("open", new {
|
|
message = "成功连接到服务器事件流",
|
|
clientName,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(HttpContext.RequestAborted);
|
|
|
|
int lastSentCount = 0;
|
|
int lastReceivedCount = 0;
|
|
var cancellationToken = HttpContext.RequestAborted;
|
|
|
|
// 创建日志文件路径
|
|
var sentLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_sent_messages.log");
|
|
var receivedLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_received_messages.log");
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
bool hasNewMessages = false;
|
|
|
|
// 检查并高效地发送新的"已发送"消息
|
|
var currentSentCount = client.SentMessagesCount;
|
|
if (currentSentCount > lastSentCount)
|
|
{
|
|
var sentMessages = client.SentMessages?.ToList() ?? new List<string>();
|
|
if (sentMessages.Count > lastSentCount)
|
|
{
|
|
var newMessages = sentMessages.Skip(lastSentCount).ToList();
|
|
if (newMessages.Any())
|
|
{
|
|
// 记录发送的消息到日志文件
|
|
await LogMessagesToFile(sentLogFilePath, newMessages, "SENT", clientName);
|
|
|
|
await SendSseEvent("update", new {
|
|
type = "sent",
|
|
messages = newMessages,
|
|
totalCount = currentSentCount,
|
|
newCount = newMessages.Count
|
|
});
|
|
lastSentCount = currentSentCount;
|
|
hasNewMessages = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 检查并高效地发送新的"已接收"消息
|
|
var currentReceivedCount = client.ReceivedMessagesCount;
|
|
if (currentReceivedCount > lastReceivedCount)
|
|
{
|
|
var receivedMessages = client.ReceivedMessages?.ToList() ?? new List<string>();
|
|
if (receivedMessages.Count > lastReceivedCount)
|
|
{
|
|
var newMessages = receivedMessages.Skip(lastReceivedCount).ToList();
|
|
if (newMessages.Any())
|
|
{
|
|
// 记录接收的消息到日志文件
|
|
await LogMessagesToFile(receivedLogFilePath, newMessages, "RECEIVED", clientName);
|
|
|
|
await SendSseEvent("update", new {
|
|
type = "received",
|
|
messages = newMessages,
|
|
totalCount = currentReceivedCount,
|
|
newCount = newMessages.Count
|
|
});
|
|
lastReceivedCount = currentReceivedCount;
|
|
hasNewMessages = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasNewMessages)
|
|
{
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
|
|
await Task.Delay(250, cancellationToken); // 每250毫秒检查一次新消息
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 正常的取消操作,退出循环
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "StreamClientMessages 循环中发生错误,客户端: {ClientName}", clientName);
|
|
await SendSseEvent("error", new {
|
|
message = "处理消息流时发生错误",
|
|
error = ex.Message,
|
|
clientName,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
|
|
// 等待一段时间后继续,避免频繁错误
|
|
await Task.Delay(1000, cancellationToken);
|
|
}
|
|
}
|
|
|
|
// 发送断开连接事件
|
|
await SendSseEvent("disconnected", new {
|
|
message = "客户端消息流连接已断开",
|
|
clientName,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("StreamClientMessages 连接被客户端取消,客户端: {ClientName}", clientName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "StreamClientMessages 方法执行时发生未处理的异常,客户端: {ClientName}", clientName);
|
|
try
|
|
{
|
|
await SendSseEvent("fatal_error", new {
|
|
message = "服务器内部错误",
|
|
error = ex.Message,
|
|
clientName,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync();
|
|
}
|
|
catch
|
|
{
|
|
// 忽略发送错误事件时的异常
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将消息记录到日志文件
|
|
/// </summary>
|
|
/// <param name="logFilePath">日志文件路径</param>
|
|
/// <param name="messages">消息列表</param>
|
|
/// <param name="messageType">消息类型(SENT/RECEIVED)</param>
|
|
/// <param name="clientName">客户端名称</param>
|
|
private async Task LogMessagesToFile(string logFilePath, List<string> messages, string messageType, string clientName)
|
|
{
|
|
try
|
|
{
|
|
var logBuilder = new StringBuilder();
|
|
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
|
|
|
foreach (var message in messages)
|
|
{
|
|
logBuilder.AppendLine($"[{timestamp}] [{clientName}] [{messageType}] {message}");
|
|
logBuilder.AppendLine(new string('-', 80)); // 分隔线
|
|
}
|
|
|
|
await System.IO.File.AppendAllTextAsync(logFilePath, logBuilder.ToString(), Encoding.UTF8);
|
|
_logger.LogDebug("已记录 {Count} 条 {MessageType} 消息到文件: {FilePath}", messages.Count, messageType, logFilePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "记录消息到文件时发生错误: {FilePath}", logFilePath);
|
|
}
|
|
}
|
|
|
|
private async Task SendSseEvent(string eventName, object data)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(eventName))
|
|
{
|
|
_logger.LogWarning("尝试发送空事件名称的SSE事件");
|
|
return;
|
|
}
|
|
|
|
if (data == null)
|
|
{
|
|
_logger.LogWarning("尝试发送空数据的SSE事件: {EventName}", eventName);
|
|
return;
|
|
}
|
|
|
|
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data);
|
|
var eventData = $"event: {eventName}\ndata: {json}\n\n";
|
|
|
|
_logger.LogDebug("SendSseEvent: 发送事件 {EventName}, 数据长度: {DataLength}, 事件数据长度: {EventDataLength}",
|
|
eventName, json.Length, eventData.Length);
|
|
|
|
await Response.WriteAsync(eventData);
|
|
|
|
_logger.LogDebug("SendSseEvent: 事件 {EventName} 发送成功", eventName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName);
|
|
// 不重新抛出异常,避免影响整个流
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取测试客户端配置
|
|
/// </summary>
|
|
/// <returns>测试客户端配置</returns>
|
|
[HttpGet("test-client-config")]
|
|
public ActionResult<ClientConfig> GetTestClientConfig()
|
|
{
|
|
var testConfig = _webSocketManager.GetTestClientConfig();
|
|
return Ok(testConfig);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置测试客户端配置
|
|
/// </summary>
|
|
/// <param name="config">测试客户端配置</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("test-client-config")]
|
|
public ActionResult SetTestClientConfig([FromBody] ClientConfig config)
|
|
{
|
|
if (string.IsNullOrEmpty(config.Name))
|
|
return BadRequest("客户端名称不能为空");
|
|
|
|
var success = _webSocketManager.SetTestClientConfig(config);
|
|
if (success)
|
|
return Ok(new { message = "测试客户端配置已更新" });
|
|
else
|
|
return BadRequest("更新测试客户端配置失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 启动测试客户端
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("test-client/start")]
|
|
public ActionResult StartTestClient()
|
|
{
|
|
var success = _webSocketManager.StartTestClient();
|
|
if (success)
|
|
return Ok(new { message = "测试客户端已启动" });
|
|
else
|
|
return BadRequest("启动测试客户端失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止测试客户端
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("test-client/stop")]
|
|
public ActionResult StopTestClient()
|
|
{
|
|
_logger.LogInformation("API 请求: 停止测试客户端");
|
|
var success = _webSocketManager.StopTestClient();
|
|
if (success)
|
|
return Ok(new { message = "测试客户端停止成功" });
|
|
else
|
|
return BadRequest("停止测试客户端失败");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取日志缓存统计信息
|
|
/// </summary>
|
|
/// <returns>统计信息</returns>
|
|
[HttpGet("logs/stats")]
|
|
public ActionResult<object> GetLogCacheStats()
|
|
{
|
|
try
|
|
{
|
|
var stats = new
|
|
{
|
|
totalLogs = _webSocketManager.GetLogCacheCount(),
|
|
cacheSize = 10000, // LogCacheSize
|
|
timestamp = DateTime.UtcNow
|
|
};
|
|
return Ok(stats);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "获取日志缓存统计信息时发生错误");
|
|
return StatusCode(500, new { message = "获取统计信息失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空全局日志缓存
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("logs/clear")]
|
|
public ActionResult ClearLogCache()
|
|
{
|
|
try
|
|
{
|
|
_webSocketManager.ClearLogCache();
|
|
return Ok(new { message = "日志缓存已清空" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "清空日志缓存时发生错误");
|
|
return StatusCode(500, new { message = "清空日志缓存失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置全局日志缓存
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("logs/reset")]
|
|
public ActionResult ResetLogCache()
|
|
{
|
|
try
|
|
{
|
|
_webSocketManager.ResetLogCache();
|
|
return Ok(new { message = "日志缓存已重置" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "重置日志缓存时发生错误");
|
|
return StatusCode(500, new { message = "重置日志缓存失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 添加测试日志数据
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("logs/add-test-data")]
|
|
public ActionResult AddTestLogData()
|
|
{
|
|
try
|
|
{
|
|
var testLogs = new List<LTELog>
|
|
{
|
|
|
|
};
|
|
|
|
// 手动添加到日志缓存
|
|
_webSocketManager.AddLogsToCache(testLogs);
|
|
|
|
return Ok(new { message = $"已添加 {testLogs.Count} 条测试日志" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "添加测试日志数据时发生错误");
|
|
return StatusCode(500, new { message = "添加测试日志失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 使用 Server-Sent Events (SSE) 实时推送全局日志
|
|
/// </summary>
|
|
[HttpGet("logs/stream")]
|
|
public async Task StreamLogs(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
Response.ContentType = "text/event-stream";
|
|
Response.Headers.Append("Cache-Control", "no-cache");
|
|
Response.Headers.Append("Connection", "keep-alive");
|
|
Response.Headers.Append("Access-Control-Allow-Origin", "*");
|
|
|
|
// 发送连接成功事件
|
|
await SendSseEvent("connected", new { message = "日志流连接已建立", timestamp = DateTime.UtcNow });
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
|
|
int lastLogCount = 0;
|
|
var lastLogs = new List<LTELog>();
|
|
var lastLogHash = string.Empty; // 添加日志内容哈希值用于比较
|
|
|
|
// 首先,一次性推送所有已缓存的日志
|
|
try
|
|
{
|
|
var initialLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>();
|
|
_logger.LogInformation("StreamLogs: 获取到初始日志 {Count} 条", initialLogs.Count);
|
|
|
|
if (initialLogs.Any())
|
|
{
|
|
_logger.LogInformation("StreamLogs: 发送历史日志事件,日志数量: {Count}", initialLogs.Count);
|
|
await SendSseEvent("history", new { logs = initialLogs, totalCount = initialLogs.Count });
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
lastLogCount = initialLogs.Count;
|
|
lastLogs = initialLogs.ToList(); // 保存副本用于比较
|
|
lastLogHash = CalculateLogsHash(initialLogs); // 计算初始日志哈希值
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("StreamLogs: 没有历史日志数据");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "获取初始日志时发生错误");
|
|
await SendSseEvent("error", new { message = "获取初始日志失败", error = ex.Message });
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var currentLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>();
|
|
var currentLogHash = CalculateLogsHash(currentLogs);
|
|
|
|
_logger.LogDebug("StreamLogs: 当前日志数量: {CurrentCount}, 上次日志数量: {LastCount}, 哈希值: {CurrentHash}",
|
|
currentLogs.Count, lastLogCount, currentLogHash);
|
|
|
|
// 检查是否有新日志(通过数量和内容哈希双重检查)
|
|
bool hasNewLogs = false;
|
|
List<LTELog> newLogs = new List<LTELog>();
|
|
|
|
if (currentLogs.Count > lastLogCount)
|
|
{
|
|
// 数量增加,计算新增的日志
|
|
newLogs = currentLogs.Skip(lastLogCount).ToList();
|
|
hasNewLogs = newLogs.Any();
|
|
_logger.LogDebug("StreamLogs: 检测到数量增加,新增 {NewCount} 条日志", newLogs.Count);
|
|
}
|
|
else if (currentLogs.Count == lastLogCount && currentLogHash != lastLogHash)
|
|
{
|
|
// 数量相同但内容变化,可能是日志被替换或更新
|
|
// 比较每个日志项,找出变化的日志
|
|
newLogs = GetChangedLogs(lastLogs, currentLogs);
|
|
hasNewLogs = newLogs.Any();
|
|
_logger.LogDebug("StreamLogs: 检测到内容变化,变化 {ChangedCount} 条日志", newLogs.Count);
|
|
}
|
|
else if (currentLogs.Count < lastLogCount)
|
|
{
|
|
// 日志被清空或重置的情况
|
|
_logger.LogInformation("检测到日志缓存被重置,重新同步");
|
|
await SendSseEvent("reset", new {
|
|
message = "日志缓存已重置",
|
|
totalCount = currentLogs.Count
|
|
});
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
|
|
lastLogCount = currentLogs.Count;
|
|
lastLogs = currentLogs.ToList();
|
|
lastLogHash = currentLogHash;
|
|
continue; // 跳过本次循环,等待下次检查
|
|
}
|
|
else
|
|
{
|
|
// 数量和内容都没有变化
|
|
_logger.LogDebug("StreamLogs: 没有检测到变化,当前数量: {CurrentCount}, 上次数量: {LastCount}",
|
|
currentLogs.Count, lastLogCount);
|
|
}
|
|
|
|
if (hasNewLogs && newLogs.Any())
|
|
{
|
|
_logger.LogInformation("StreamLogs: 发送新日志事件,新增日志数量: {NewCount}, 总日志数量: {TotalCount}", newLogs.Count, currentLogs.Count);
|
|
_logger.LogDebug("StreamLogs: 新日志详情 - 第一条: {FirstLog}, 最后一条: {LastLog}",
|
|
newLogs.FirstOrDefault()?.Message,
|
|
newLogs.LastOrDefault()?.Message);
|
|
|
|
var eventData = new {
|
|
logs = newLogs,
|
|
totalCount = currentLogs.Count,
|
|
newCount = newLogs.Count
|
|
};
|
|
|
|
_logger.LogDebug("StreamLogs: 准备发送事件数据: {EventData}",
|
|
Newtonsoft.Json.JsonConvert.SerializeObject(eventData));
|
|
|
|
await SendSseEvent("new_logs", eventData);
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
|
|
_logger.LogInformation("StreamLogs: 新日志事件发送完成");
|
|
|
|
// 更新索引和缓存
|
|
lastLogCount = currentLogs.Count;
|
|
lastLogs = currentLogs.ToList();
|
|
lastLogHash = currentLogHash;
|
|
}
|
|
|
|
await Task.Delay(250, cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 正常的取消操作,退出循环
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "StreamLogs 循环中发生错误");
|
|
await SendSseEvent("error", new {
|
|
message = "处理日志流时发生错误",
|
|
error = ex.Message,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
|
|
// 等待一段时间后继续,避免频繁错误
|
|
await Task.Delay(1000, cancellationToken);
|
|
}
|
|
}
|
|
|
|
// 发送断开连接事件
|
|
await SendSseEvent("disconnected", new {
|
|
message = "日志流连接已断开",
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("StreamLogs 连接被客户端取消");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "StreamLogs 方法执行时发生未处理的异常");
|
|
try
|
|
{
|
|
await SendSseEvent("fatal_error", new {
|
|
message = "服务器内部错误",
|
|
error = ex.Message,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync();
|
|
}
|
|
catch
|
|
{
|
|
// 忽略发送错误事件时的异常
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算日志列表的哈希值,用于检测内容变化
|
|
/// </summary>
|
|
/// <param name="logs">日志列表</param>
|
|
/// <returns>哈希值字符串</returns>
|
|
private string CalculateLogsHash(List<LTELog> logs)
|
|
{
|
|
if (logs == null || !logs.Any())
|
|
return string.Empty;
|
|
|
|
try
|
|
{
|
|
// 使用日志的关键字段计算哈希值
|
|
var hashInput = string.Join("|", logs.Select(log =>
|
|
$"{log.Timestamp}_{log.Layer}_{log.Message}"));
|
|
|
|
using (var sha256 = System.Security.Cryptography.SHA256.Create())
|
|
{
|
|
var bytes = System.Text.Encoding.UTF8.GetBytes(hashInput);
|
|
var hash = sha256.ComputeHash(bytes);
|
|
return Convert.ToBase64String(hash);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "计算日志哈希值时发生错误");
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取变化的日志项
|
|
/// </summary>
|
|
/// <param name="oldLogs">旧日志列表</param>
|
|
/// <param name="newLogs">新日志列表</param>
|
|
/// <returns>变化的日志列表</returns>
|
|
private List<LTELog> GetChangedLogs(List<LTELog> oldLogs, List<LTELog> newLogs)
|
|
{
|
|
var changedLogs = new List<LTELog>();
|
|
|
|
if (oldLogs.Count != newLogs.Count)
|
|
{
|
|
// 数量不同,返回所有新日志
|
|
return newLogs;
|
|
}
|
|
|
|
// 比较每个日志项
|
|
for (int i = 0; i < newLogs.Count; i++)
|
|
{
|
|
if (i < oldLogs.Count)
|
|
{
|
|
var oldLog = oldLogs[i];
|
|
var newLog = newLogs[i];
|
|
|
|
// 比较关键字段
|
|
if (oldLog.Timestamp != newLog.Timestamp ||
|
|
oldLog.Layer != newLog.Layer ||
|
|
oldLog.Message != newLog.Message)
|
|
{
|
|
changedLogs.Add(newLog);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 新增的日志
|
|
changedLogs.Add(newLogs[i]);
|
|
}
|
|
}
|
|
|
|
return changedLogs;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取客户端消息日志文件列表
|
|
/// </summary>
|
|
/// <returns>日志文件列表</returns>
|
|
[HttpGet("clients/message-logs")]
|
|
public ActionResult<object> GetClientMessageLogFiles()
|
|
{
|
|
try
|
|
{
|
|
if (!Directory.Exists(_logsDirectory))
|
|
{
|
|
return Ok(new { files = new List<object>(), message = "日志目录不存在" });
|
|
}
|
|
|
|
var logFiles = Directory.GetFiles(_logsDirectory, "*.log")
|
|
.Select(filePath => new
|
|
{
|
|
fileName = Path.GetFileName(filePath),
|
|
filePath = filePath,
|
|
size = new System.IO.FileInfo(filePath).Length,
|
|
lastModified = System.IO.File.GetLastWriteTime(filePath),
|
|
clientName = Path.GetFileNameWithoutExtension(filePath).Replace("_sent_messages", "").Replace("_received_messages", ""),
|
|
type = filePath.Contains("_sent_messages") ? "发送消息" : "接收消息"
|
|
})
|
|
.OrderByDescending(f => f.lastModified)
|
|
.ToList();
|
|
|
|
return Ok(new { files = logFiles, totalCount = logFiles.Count });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "获取客户端消息日志文件列表时发生错误");
|
|
return StatusCode(500, new { message = "获取日志文件列表失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取客户端消息日志文件内容
|
|
/// </summary>
|
|
/// <param name="fileName">文件名</param>
|
|
/// <param name="lines">返回的行数(默认100行)</param>
|
|
/// <returns>日志文件内容</returns>
|
|
[HttpGet("clients/message-logs/{fileName}")]
|
|
public ActionResult<object> GetClientMessageLogContent(string fileName, [FromQuery] int lines = 100)
|
|
{
|
|
try
|
|
{
|
|
var filePath = Path.Combine(_logsDirectory, fileName);
|
|
if (!System.IO.File.Exists(filePath))
|
|
{
|
|
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" });
|
|
}
|
|
|
|
var fileInfo = new System.IO.FileInfo(filePath);
|
|
var allLines = System.IO.File.ReadAllLines(filePath, Encoding.UTF8);
|
|
var lastLines = allLines.TakeLast(lines).ToList();
|
|
|
|
return Ok(new
|
|
{
|
|
fileName = fileName,
|
|
filePath = filePath,
|
|
totalLines = allLines.Length,
|
|
returnedLines = lastLines.Count,
|
|
fileSize = fileInfo.Length,
|
|
lastModified = fileInfo.LastWriteTime,
|
|
content = lastLines
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "获取客户端消息日志文件内容时发生错误: {FileName}", fileName);
|
|
return StatusCode(500, new { message = "获取日志文件内容失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空客户端消息日志文件
|
|
/// </summary>
|
|
/// <param name="fileName">文件名(可选,如果不提供则清空所有日志文件)</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpDelete("clients/message-logs")]
|
|
public ActionResult ClearClientMessageLogs([FromQuery] string? fileName = null)
|
|
{
|
|
try
|
|
{
|
|
if (!Directory.Exists(_logsDirectory))
|
|
{
|
|
return Ok(new { message = "日志目录不存在,无需清空" });
|
|
}
|
|
|
|
int clearedCount = 0;
|
|
if (string.IsNullOrEmpty(fileName))
|
|
{
|
|
// 清空所有日志文件
|
|
var logFiles = Directory.GetFiles(_logsDirectory, "*.log");
|
|
foreach (var filePath in logFiles)
|
|
{
|
|
System.IO.File.WriteAllText(filePath, string.Empty);
|
|
clearedCount++;
|
|
}
|
|
_logger.LogInformation("已清空 {Count} 个客户端消息日志文件", clearedCount);
|
|
return Ok(new { message = $"已清空 {clearedCount} 个客户端消息日志文件" });
|
|
}
|
|
else
|
|
{
|
|
// 清空指定日志文件
|
|
var filePath = Path.Combine(_logsDirectory, fileName);
|
|
if (!System.IO.File.Exists(filePath))
|
|
{
|
|
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" });
|
|
}
|
|
|
|
System.IO.File.WriteAllText(filePath, string.Empty);
|
|
_logger.LogInformation("已清空客户端消息日志文件: {FileName}", fileName);
|
|
return Ok(new { message = $"已清空客户端消息日志文件 '{fileName}'" });
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "清空客户端消息日志文件时发生错误");
|
|
return StatusCode(500, new { message = "清空日志文件失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 删除客户端消息日志文件
|
|
/// </summary>
|
|
/// <param name="fileName">文件名(可选,如果不提供则删除所有日志文件)</param>
|
|
/// <returns>操作结果</returns>
|
|
[HttpDelete("clients/message-logs/delete")]
|
|
public ActionResult DeleteClientMessageLogs([FromQuery] string? fileName = null)
|
|
{
|
|
try
|
|
{
|
|
if (!Directory.Exists(_logsDirectory))
|
|
{
|
|
return Ok(new { message = "日志目录不存在,无需删除" });
|
|
}
|
|
|
|
int deletedCount = 0;
|
|
if (string.IsNullOrEmpty(fileName))
|
|
{
|
|
// 删除所有日志文件
|
|
var logFiles = Directory.GetFiles(_logsDirectory, "*.log");
|
|
foreach (var filePath in logFiles)
|
|
{
|
|
System.IO.File.Delete(filePath);
|
|
deletedCount++;
|
|
}
|
|
_logger.LogInformation("已删除 {Count} 个客户端消息日志文件", deletedCount);
|
|
return Ok(new { message = $"已删除 {deletedCount} 个客户端消息日志文件" });
|
|
}
|
|
else
|
|
{
|
|
// 删除指定日志文件
|
|
var filePath = Path.Combine(_logsDirectory, fileName);
|
|
if (!System.IO.File.Exists(filePath))
|
|
{
|
|
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" });
|
|
}
|
|
|
|
System.IO.File.Delete(filePath);
|
|
_logger.LogInformation("已删除客户端消息日志文件: {FileName}", fileName);
|
|
return Ok(new { message = $"已删除客户端消息日志文件 '{fileName}'" });
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "删除客户端消息日志文件时发生错误");
|
|
return StatusCode(500, new { message = "删除日志文件失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取日志缓存详细状态(调试用)
|
|
/// </summary>
|
|
/// <returns>日志缓存详细状态</returns>
|
|
[HttpGet("logs/debug")]
|
|
public ActionResult<object> GetLogCacheDebugInfo()
|
|
{
|
|
try
|
|
{
|
|
var logs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>();
|
|
var logCount = _webSocketManager.GetLogCacheCount();
|
|
var cacheStatus = _webSocketManager.GetLogCacheStatus();
|
|
|
|
var debugInfo = new
|
|
{
|
|
totalLogs = logCount,
|
|
actualLogsCount = logs.Count,
|
|
cacheSize = 10000, // LogCacheSize
|
|
timestamp = DateTime.UtcNow,
|
|
sampleLogs = logs.TakeLast(5).Select(log => new
|
|
{
|
|
timestamp = log.Timestamp,
|
|
layer = log.Layer,
|
|
message = log.Message?.Substring(0, Math.Min(100, log.Message?.Length ?? 0)) + "..."
|
|
}).ToList(),
|
|
logHash = CalculateLogsHash(logs),
|
|
cacheStatus = cacheStatus
|
|
};
|
|
|
|
return Ok(debugInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "获取日志缓存调试信息时发生错误");
|
|
return StatusCode(500, new { message = "获取调试信息失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取SSE连接状态(调试用)
|
|
/// </summary>
|
|
/// <returns>连接状态信息</returns>
|
|
[HttpGet("logs/connection-status")]
|
|
public ActionResult<object> GetSseConnectionStatus()
|
|
{
|
|
try
|
|
{
|
|
var status = new
|
|
{
|
|
timestamp = DateTime.UtcNow,
|
|
requestHeaders = Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()),
|
|
userAgent = Request.Headers["User-Agent"].ToString(),
|
|
accept = Request.Headers["Accept"].ToString(),
|
|
cacheControl = Request.Headers["Cache-Control"].ToString(),
|
|
connection = Request.Headers["Connection"].ToString(),
|
|
isHttps = Request.IsHttps,
|
|
host = Request.Host.ToString(),
|
|
path = Request.Path.ToString(),
|
|
queryString = Request.QueryString.ToString(),
|
|
method = Request.Method,
|
|
contentType = Request.ContentType,
|
|
contentLength = Request.ContentLength
|
|
};
|
|
|
|
return Ok(status);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "获取SSE连接状态时发生错误");
|
|
return StatusCode(500, new { message = "获取连接状态失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 测试SSE连接(调试用)
|
|
/// </summary>
|
|
/// <returns>测试结果</returns>
|
|
[HttpGet("logs/test-connection")]
|
|
public ActionResult<object> TestSseConnection()
|
|
{
|
|
try
|
|
{
|
|
var testResult = new
|
|
{
|
|
message = "SSE连接测试成功",
|
|
timestamp = DateTime.UtcNow,
|
|
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
|
logCacheCount = _webSocketManager.GetLogCacheCount(),
|
|
testData = new { test = true, message = "这是一个测试消息" }
|
|
};
|
|
|
|
return Ok(testResult);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "测试SSE连接时发生错误");
|
|
return StatusCode(500, new { message = "测试连接失败", error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 强制推送测试日志(调试用)
|
|
/// </summary>
|
|
/// <returns>操作结果</returns>
|
|
[HttpPost("logs/force-push-test")]
|
|
public ActionResult ForcePushTestLogs()
|
|
{
|
|
try
|
|
{
|
|
var testLogs = new List<LTELog>
|
|
{
|
|
};
|
|
|
|
// 手动添加到日志缓存
|
|
_webSocketManager.AddLogsToCache(testLogs);
|
|
|
|
_logger.LogInformation("强制推送测试日志: {Message}", testLogs[0].Message);
|
|
|
|
return Ok(new {
|
|
message = $"已强制推送测试日志: {testLogs[0].Message}",
|
|
timestamp = testLogs[0].Timestamp
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "强制推送测试日志时发生错误");
|
|
return StatusCode(500, new { message = "强制推送失败", error = ex.Message });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 日志配置请求
|
|
/// </summary>
|
|
public class LogsConfigRequest
|
|
{
|
|
/// <summary>
|
|
/// 日志配置
|
|
/// </summary>
|
|
public Dictionary<string, object> Config { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 是否保存配置
|
|
/// </summary>
|
|
public bool Save { get; set; } = false;
|
|
}
|
|
}
|