Compare commits

..

3 Commits

Author SHA1 Message Date
Diego
dbfc9a5bb4 更新授权显示 2025-05-30 11:21:45 +08:00
Diego
1b758aa41a 10.6.37 2025-05-30 10:15:58 +08:00
Diego
43bdc70899 优化文本日志读取 2025-05-30 10:02:51 +08:00
17 changed files with 167 additions and 141 deletions

View File

@@ -12,10 +12,16 @@
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using ThingsGateway.Admin.Application;
using ThingsGateway.Admin.Razor;
using ThingsGateway.Extension;
namespace ThingsGateway.AdminServer; namespace ThingsGateway.AdminServer;

View File

@@ -13,6 +13,8 @@ using Microsoft.Extensions.Localization;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ThingsGateway.Admin.Application;
namespace ThingsGateway.AdminServer; namespace ThingsGateway.AdminServer;
public partial class AccessDenied public partial class AccessDenied

View File

@@ -9,6 +9,10 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
using BootstrapBlazor.Components;
using Mapster;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@@ -16,6 +20,11 @@ using Microsoft.Extensions.Options;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ThingsGateway.Admin.Application;
using ThingsGateway.DataEncryption;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.Razor;
namespace ThingsGateway.AdminServer; namespace ThingsGateway.AdminServer;
public partial class Login public partial class Login

View File

@@ -9,12 +9,18 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ThingsGateway.Admin.Application;
using ThingsGateway.Admin.Razor;
using ThingsGateway.Razor;
namespace ThingsGateway.AdminServer; namespace ThingsGateway.AdminServer;
public partial class MainLayout : IDisposable public partial class MainLayout : IDisposable

View File

@@ -13,6 +13,8 @@ using Microsoft.AspNetCore.ResponseCompression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.AdminServer; namespace ThingsGateway.AdminServer;
public class Program public class Program

View File

@@ -18,11 +18,18 @@ using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Unicode; using System.Text.Unicode;
using ThingsGateway.Admin.Application;
using ThingsGateway.Admin.Razor;
using ThingsGateway.Extension;
using ThingsGateway.NewLife.Caching;
namespace ThingsGateway.AdminServer; namespace ThingsGateway.AdminServer;
[AppStartup(-99999)] [AppStartup(-99999)]

View File

@@ -1,9 +1,9 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<PluginVersion>10.6.35</PluginVersion> <PluginVersion>10.7.0</PluginVersion>
<ProPluginVersion>10.6.35</ProPluginVersion> <ProPluginVersion>10.7.0</ProPluginVersion>
<AuthenticationVersion>2.1.8</AuthenticationVersion> <AuthenticationVersion>2.2.0</AuthenticationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -28,11 +28,12 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
set set
{ {
_heartbeat = value; _heartbeat = value;
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value)); if (!_heartbeat.IsNullOrEmpty())
HeartbeatByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(value));
} }
} }
private string _heartbeat; private string _heartbeat;
private ArraySegment<byte> HeartbeatByte; private ArraySegment<byte> HeartbeatByte = new();
/// <inheritdoc/> /// <inheritdoc/>
public async Task OnTcpReceiving(ITcpSession client, ByteBlockEventArgs e) public async Task OnTcpReceiving(ITcpSession client, ByteBlockEventArgs e)

View File

@@ -47,7 +47,7 @@ public static class PluginUtil
Action<IPluginManager> action = a => { }; Action<IPluginManager> action = a => { };
action += GetTcpServicePlugin(channelOptions); action += GetTcpServicePlugin(channelOptions);
if (!channelOptions.Heartbeat.IsNullOrWhiteSpace()) //if (!channelOptions.Heartbeat.IsNullOrWhiteSpace())
{ {
action += a => action += a =>
{ {

View File

@@ -8,17 +8,13 @@
// QQ群605534569 // QQ群605534569
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
using System.Buffers;
using System.Text; using System.Text;
using ThingsGateway.NewLife.Caching; using ThingsGateway.NewLife.Caching;
namespace ThingsGateway.Foundation; namespace ThingsGateway.Foundation;
public class LogDataCache
{
public List<LogData> LogDatas { get; set; }
public long Length { get; set; }
}
/// <summary> /// <summary>
/// 日志数据 /// 日志数据
/// </summary> /// </summary>
@@ -47,8 +43,19 @@ public class LogData
/// <summary>日志文本文件倒序读取</summary> /// <summary>日志文本文件倒序读取</summary>
public class LogDataCache
{
public List<LogData> LogDatas { get; set; }
public long Length { get; set; }
}
/// <summary>高性能日志文件读取器(支持倒序读取)</summary>
public class TextFileReader public class TextFileReader
{ {
private static readonly MemoryCache _cache = new() { Expire = 30 };
private static readonly MemoryCache _fileLocks = new();
private static readonly ArrayPool<byte> _bytePool = ArrayPool<byte>.Shared;
/// <summary> /// <summary>
/// 获取指定目录下所有文件信息 /// 获取指定目录下所有文件信息
/// </summary> /// </summary>
@@ -86,159 +93,167 @@ public class TextFileReader
return result; return result;
} }
static MemoryCache _cache = new() { Expire = 30 };
public static OperResult<List<LogData>> LastLog(string file, int lineCount = 200) public static OperResult<List<LogData>> LastLog(string file, int lineCount = 200)
{ {
lock (_cache) if (!File.Exists(file))
{ return new OperResult<List<LogData>>("The file path is invalid");
OperResult<List<LogData>> result = new(); // 初始化结果对象 _fileLocks.SetExpire(file, TimeSpan.FromSeconds(30));
var fileLock = _fileLocks.GetOrAdd(file, _ => new object());
lock (fileLock)
{
try try
{ {
if (!File.Exists(file)) // 检查文件是否存在 var fileInfo = new FileInfo(file);
var length = fileInfo.Length;
var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLog)}_{file})";
if (_cache.TryGetValue<LogDataCache>(cacheKey, out var cachedData))
{ {
result.OperCode = 999; if (cachedData != null && cachedData.Length == length)
result.ErrorMessage = "The file path is invalid";
return result;
}
List<string> txt = new(); // 存储读取的文本内容
long ps = 0; // 保存起始位置
var key = $"{nameof(TextFileReader)}_{nameof(LastLog)}_{file})";
long length = 0;
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
length = fs.Length;
var dataCache = _cache.Get<LogDataCache>(key);
if (dataCache != null && dataCache.Length == length)
{ {
result.Content = dataCache.LogDatas; return new OperResult<List<LogData>>() { Content = cachedData.LogDatas };
result.OperCode = 0; // 操作状态设为成功
return result; // 返回解析结果
} }
else
if (ps <= 0) // 如果起始位置小于等于0将起始位置设置为文件长度
ps = length - 1;
// 循环读取指定行数的文本内容
for (int i = 0; i < lineCount; i++)
{ {
ps = InverseReadRow(fs, ps, out var value); // 使用逆序读取 _cache.Remove(cacheKey);
txt.Add(value);
if (ps <= 0) // 如果已经读取到文件开头则跳出循环
break;
} }
} }
// 使用单次 LINQ 操作进行过滤和解析 using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.SequentialScan);
result.Content = txt var result = ReadLogsInverse(fs, lineCount, fileInfo.Length);
.Select(a => ParseCSV(a))
.Where(data => data.Count >= 3)
.Select(data =>
{
var log = new LogData
{
LogTime = data[0].Trim(),
LogLevel = Enum.TryParse(data[1].Trim(), out LogLevel level) ? level : LogLevel.Info,
Message = data[2].Trim(),
ExceptionString = data.Count > 3 ? data[3].Trim() : null
};
return log;
})
.ToList();
result.OperCode = 0; // 操作状态设为成功 _cache.Set(cacheKey, new LogDataCache
var data = _cache.Set<LogDataCache>(key, new LogDataCache() { Length = length, LogDatas = result.Content }); {
LogDatas = result,
Length = fileInfo.Length,
});
return result; // 返回解析结果 return new OperResult<List<LogData>>() { Content = result };
} }
catch (Exception ex) // 捕获异常 catch (Exception ex)
{ {
result = new(ex); // 创建包含异常信息的结果对象 return new OperResult<List<LogData>>(ex);
return result; // 返回异常结果
} }
} }
} }
private static List<LogData> ReadLogsInverse(FileStream fs, int lineCount, long length)
{
length = fs.Length;
long ps = 0; // 保存起始位置
List<string> txt = new(); // 存储读取的文本内容
if (ps <= 0) // 如果起始位置小于等于0将起始位置设置为文件长度
ps = length - 1;
// 循环读取指定行数的文本内容
for (int i = 0; i < lineCount; i++)
{
ps = InverseReadRow(fs, ps, out var value); // 使用逆序读取
txt.Add(value);
if (ps <= 0) // 如果已经读取到文件开头则跳出循环
break;
}
// 使用单次 LINQ 操作进行过滤和解析
var result = txt
.Select(a => ParseCSV(a))
.Where(data => data.Count >= 3)
.Select(data =>
{
var log = new LogData
{
LogTime = data[0].Trim(),
LogLevel = Enum.TryParse(data[1].Trim(), out LogLevel level) ? level : LogLevel.Info,
Message = data[2].Trim(),
ExceptionString = data.Count > 3 ? data[3].Trim() : null
};
return log;
})
.ToList();
return result; // 返回解析结果
}
private static long InverseReadRow(FileStream fs, long position, out string value, int maxRead = 102400) private static long InverseReadRow(FileStream fs, long position, out string value, int maxRead = 102400)
{ {
byte n = 0xD; // 换行符 byte n = 0xD;
byte a = 0xA; // 回车符 byte a = 0xA;
value = string.Empty; value = string.Empty;
if (fs.Length == 0) return 0; // 若文件长度为0则直接返回0作为新的位置
if (fs.Length == 0) return 0;
var newPos = position; var newPos = position;
List<byte> buffer = new List<byte>(maxRead); // 缓存读取的数据 byte[] buffer = _bytePool.Rent(maxRead); // 从池中租借字节数组
int index = 0;
try try
{ {
var readLength = 0; while (true)
while (true) // 循环读取一行数据TextFileLogger.Separator行判定
{ {
readLength++;
if (newPos <= 0) if (newPos <= 0)
newPos = 0; newPos = 0;
fs.Position = newPos; fs.Position = newPos;
int byteRead = fs.ReadByte(); int byteRead = fs.ReadByte();
if (byteRead == -1) break; // 到达文件开头时跳出循环 if (byteRead == -1) break;
buffer.Add((byte)byteRead); if (index >= maxRead)
if (byteRead == n || byteRead == a)//判断当前字符是换行符 // TextFileLogger.Separator
{
if (MatchSeparator(buffer))
{
// 去掉匹配的指定字符串
buffer.RemoveRange(buffer.Count - TextFileLogger.SeparatorBytes.Length, TextFileLogger.SeparatorBytes.Length);
break;
}
}
if (buffer.Count > maxRead) // 超过最大字节数限制时丢弃数据
{ {
newPos = -1; newPos = -1;
return newPos; return newPos;
} }
buffer[index++] = (byte)byteRead;
if (byteRead == n || byteRead == a)
{
if (MatchSeparator(buffer, index))
{
index -= TextFileLogger.SeparatorBytes.Length;
break;
}
}
newPos--; newPos--;
if (newPos <= -1) if (newPos <= -1)
break; break;
} }
if (buffer.Count >= 10) if (index >= 10)
{ {
buffer.Reverse(); Array.Reverse(buffer, 0, index); // 倒序
value = Encoding.UTF8.GetString(buffer.ToArray()); // 转换为字符串 value = Encoding.UTF8.GetString(buffer, 0, index);
} }
return newPos; // 返回新的读取位置 return newPos;
} }
finally finally
{ {
_bytePool.Return(buffer); // 归还数组
} }
} }
private static bool MatchSeparator(List<byte> arr) private static bool MatchSeparator(byte[] arr, int length)
{ {
if (arr.Count < TextFileLogger.SeparatorBytes.Length) if (length < TextFileLogger.SeparatorBytes.Length)
{
return false; return false;
}
var pos = arr.Count - 1; int pos = length - 1;
for (int i = 0; i < TextFileLogger.SeparatorBytes.Length; i++) for (int i = 0; i < TextFileLogger.SeparatorBytes.Length; i++)
{ {
if (arr[pos] != TextFileLogger.SeparatorBytes[i]) if (arr[pos] != TextFileLogger.SeparatorBytes[i])
{
return false; return false;
}
pos--; pos--;
} }
return true; return true;
} }
private static List<string> ParseCSV(string data) private static List<string> ParseCSV(string data)
{ {
List<string> items = new List<string>(); List<string> items = new List<string>();

View File

@@ -75,7 +75,6 @@ public class SmartTriggerScheduler
return; return;
} }
// 有新的触发,继续下一轮循环(再执行一次)
} }
} }
} }

View File

@@ -352,7 +352,12 @@ public abstract class DriverBase : DisposableObject, IDriver
public string GetAuthString() public string GetAuthString()
{ {
return PluginServiceUtil.IsEducation(GetType()) ? ThingsGateway.Authentication.ProAuthentication.TryGetAuthorizeInfo(out _) ? Localizer["Authorized"] : Localizer["Unauthorized"] : string.Empty; if (PluginServiceUtil.IsEducation(GetType()))
{
ThingsGateway.Authentication.ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
return authorizeInfo.Auth ? Localizer["Authorized"] : Localizer["Unauthorized"];
}
return string.Empty;
} }
/// <summary> /// <summary>

View File

@@ -10,8 +10,6 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ThingsGateway.Authentication;
namespace ThingsGateway.Gateway.Razor; namespace ThingsGateway.Gateway.Razor;
/// <inheritdoc/> /// <inheritdoc/>
@@ -27,39 +25,5 @@ public partial class GatewayAbout
[NotNull] [NotNull]
private IOptions<WebsiteOptions>? WebsiteOption { get; set; } private IOptions<WebsiteOptions>? WebsiteOption { get; set; }
private string Password { get; set; }
private AuthorizeInfo AuthorizeInfo { get; set; }
[Inject]
ToastService ToastService { get; set; }
protected override void OnParametersSet()
{
ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
AuthorizeInfo = authorizeInfo;
base.OnParametersSet();
}
private async Task Register()
{
var result = ProAuthentication.TryAuthorize(Password, out var authorizeInfo);
if (result)
{
AuthorizeInfo = authorizeInfo;
await ToastService.Default();
}
else
await ToastService.Default(false);
Password = string.Empty;
await InvokeAsync(StateHasChanged);
}
private async Task Unregister()
{
ProAuthentication.UnAuthorize();
var result = ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
AuthorizeInfo = authorizeInfo;
await InvokeAsync(StateHasChanged);
}
} }

View File

@@ -35,6 +35,15 @@ public partial class VariableRuntimeInfo : IDisposable
[Parameter] [Parameter]
public IEnumerable<VariableRuntime>? Items { get; set; } = Enumerable.Empty<VariableRuntime>(); public IEnumerable<VariableRuntime>? Items { get; set; } = Enumerable.Empty<VariableRuntime>();
private IEnumerable<VariableRuntime>? _previousItemsRef;
protected override async Task OnParametersSetAsync()
{
if (!ReferenceEquals(_previousItemsRef, Items))
{
_previousItemsRef = Items;
await Refresh(null);
}
}
public void Dispose() public void Dispose()
{ {
@@ -47,7 +56,7 @@ public partial class VariableRuntimeInfo : IDisposable
{ {
VariableRuntimeDispatchService.Subscribe(Refresh); VariableRuntimeDispatchService.Subscribe(Refresh);
scheduler = new SmartTriggerScheduler(Notify, TimeSpan.FromMilliseconds(3000)); scheduler = new SmartTriggerScheduler(Notify, TimeSpan.FromMilliseconds(1000));
_ = RunTimerAsync(); _ = RunTimerAsync();
base.OnInitialized(); base.OnInitialized();

View File

@@ -40,7 +40,7 @@
</label> </label>
<label class="form-control"> <label class="form-control">
@(AuthorizeInfo != null ? Localizer["Authorized"] : Localizer["Unauthorized"]) @(AuthorizeInfo?.Auth==true ? Localizer["Authorized"] : Localizer["Unauthorized"])
</label> </label>
</div> </div>
</div> </div>
@@ -50,6 +50,7 @@
@if (AuthorizeInfo != null) @if (AuthorizeInfo != null)
{ {
<div class="row g-3 form-inline"> <div class="row g-3 form-inline">
<div class="col-12 col-sm-12"> <div class="col-12 col-sm-12">
<label class="form-label"> <label class="form-label">
@@ -70,7 +71,7 @@
</label> </label>
<label class="form-control"> <label class="form-control">
@AuthorizeInfo?.ExpireTime @AuthorizeInfo?.RealExpireTime
</label> </label>
</div> </div>

View File

@@ -55,7 +55,7 @@ public partial class Authentication
private async Task Unregister() private async Task Unregister()
{ {
ProAuthentication.UnAuthorize(); ProAuthentication.UnAuthorize();
var result = ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo); _ = ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
AuthorizeInfo = authorizeInfo; AuthorizeInfo = authorizeInfo;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);

View File

@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>10.6.35</Version> <Version>10.7.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>