Compare commits

..

9 Commits

Author SHA1 Message Date
Diego
d7df6fc605 10.10.12 2025-08-07 11:24:57 +08:00
2248356998 qq.com
eb4bb2fd48 10.10.11 2025-08-07 10:19:28 +08:00
2248356998 qq.com
faa9858974 10.10.11 2025-08-07 10:18:22 +08:00
2248356998 qq.com
1b3d2dda49 QuestDbRestAPI 2025-08-07 10:11:37 +08:00
Diego
a8a9453611 !70 fix: questdb restapi多实例 2025-08-07 01:58:32 +00:00
2248356998 qq.com
e84f42ce14 10.10.10 2025-08-06 21:42:46 +08:00
2248356998 qq.com
6f814cf6b8 更新questdb restapi启用字段 2025-08-06 21:42:23 +08:00
2248356998 qq.com
e36432e4e9 10.10.9 2025-08-06 19:33:30 +08:00
Diego
ebd71e807b !69 更新依赖 2025-08-06 11:25:31 +00:00
227 changed files with 1986 additions and 4922 deletions

View File

@@ -123,6 +123,15 @@ public static class QueryPageOptionsExtensions
};
var items = datas.GetData(option, out var totalCount, where);
ret.TotalCount = totalCount;
if (totalCount > 0)
{
if (!items.Any() && option.PageIndex != 1)
{
option.PageIndex = 1;
items = datas.GetData(option, out totalCount, where);
}
}
ret.Items = items.ToList();
return ret;
}

View File

@@ -27,16 +27,17 @@
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertDatas">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public int BulkCopy<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public int BulkCopy<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
int result = 0;
// 使用分页方式处理大数据量插入
db.Utilities.PageEach(insertDatas, pageSize, pageItems =>
{
// 同步调用批量插入API并累加结果
result += questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).GetAwaiter().GetResult();
result += questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).GetAwaiter().GetResult();
});
return result;
}
@@ -46,16 +47,17 @@
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertDatas">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public async Task<int> BulkCopyAsync<T>(IEnumerable<T> insertDatas, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
int result = 0;
// 异步分页处理大数据量插入
await db.Utilities.PageEachAsync(insertDatas, pageSize, async pageItems =>
{
// 异步调用批量插入API并累加结果
result += await questDbRestAPI.BulkCopyAsync(pageItems, dateFormat).ConfigureAwait(false);
result += await questDbRestAPI.BulkCopyAsync(pageItems, tableName, dateFormat).ConfigureAwait(false);
}).ConfigureAwait(false);
return result;
}

View File

@@ -7,16 +7,12 @@ namespace ThingsGateway.SqlSugar
/// <summary>
/// 绑定RestAPI需要的信息
/// </summary>
public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string httpPort, ref string username, ref string password)
public static void SetRestApiInfo(DbConnectionStringBuilder builder, ref string host, ref string username, ref string password)
{
if (builder.TryGetValue("Host", out object hostValue))
{
host = Convert.ToString(hostValue);
}
if (builder.TryGetValue("HttpPort", out object httpPortValue))
{
httpPort = Convert.ToString(httpPortValue);
}
if (builder.TryGetValue("Username", out object usernameValue))
{
username = Convert.ToString(usernameValue);

View File

@@ -28,16 +28,16 @@ namespace ThingsGateway.SqlSugar
/// 初始化 QuestDbRestAPI 实例
/// </summary>
/// <param name="db">SqlSugar 数据库客户端</param>
public QuestDbRestAPI(ISqlSugarClient db)
/// <param name="httpPort">restApi端口</param>
public QuestDbRestAPI(ISqlSugarClient db, int httpPort = 9000)
{
var builder = new DbConnectionStringBuilder();
builder.ConnectionString = db.CurrentConnectionConfig.ConnectionString;
this.db = db;
string httpPort = String.Empty;
string host = String.Empty;
string username = String.Empty;
string password = String.Empty;
QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref httpPort, ref username, ref password);
QuestDbRestAPHelper.SetRestApiInfo(builder, ref host, ref username, ref password);
BindHost(host, httpPort, username, password);
}
@@ -51,9 +51,14 @@ namespace ThingsGateway.SqlSugar
// HTTP GET 请求执行SQL
var result = string.Empty;
var url = $"{this.url}/exec?query={HttpUtility.UrlEncode(sql)}";
var request = new HttpRequestMessage(HttpMethod.Get, url);
if (!string.IsNullOrWhiteSpace(authorization))
client.DefaultRequestHeaders.Add("Authorization", authorization);
var httpResponseMessage = await client.GetAsync(url).ConfigureAwait(false);
{
request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
}
using var httpResponseMessage = await client.SendAsync(request).ConfigureAwait(false);
result = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
return result;
}
@@ -68,34 +73,34 @@ namespace ThingsGateway.SqlSugar
return ExecuteCommandAsync(sql).GetAwaiter().GetResult();
}
/// <summary>
/// 异步批量插入单条数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertData">要插入的数据</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>影响的行数</returns>
public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
if (db.CurrentConnectionConfig.MoreSettings == null)
db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings();
db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true;
var sql = db.InsertableT(insertData).ToSqlString();
var result = await ExecuteCommandAsync(sql).ConfigureAwait(false);
return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
}
///// <summary>
///// 异步批量插入单条数据
///// </summary>
///// <typeparam name="T">数据类型</typeparam>
///// <param name="insertData">要插入的数据</param>
///// <param name="dateFormat">日期格式字符串</param>
///// <returns>影响的行数</returns>
//public async Task<int> BulkCopyAsync<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
//{
// if (db.CurrentConnectionConfig.MoreSettings == null)
// db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings();
// db.CurrentConnectionConfig.MoreSettings.DisableNvarchar = true;
// var sql = db.InsertableT(insertData).ToSqlString();
// var result = await ExecuteCommandAsync(sql).ConfigureAwait(false);
// return result.Contains("OK", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
//}
/// <summary>
/// 同步批量插入单条数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertData">要插入的数据</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>影响的行数</returns>
public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult();
}
///// <summary>
///// 同步批量插入单条数据
///// </summary>
///// <typeparam name="T">数据类型</typeparam>
///// <param name="insertData">要插入的数据</param>
///// <param name="dateFormat">日期格式字符串</param>
///// <returns>影响的行数</returns>
//public int BulkCopy<T>(T insertData, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
//{
// return BulkCopyAsync(insertData, dateFormat).GetAwaiter().GetResult();
//}
/// <summary>
/// 创建分页批量插入器
@@ -115,9 +120,10 @@ namespace ThingsGateway.SqlSugar
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertList">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public async Task<int> BulkCopyAsync<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public async Task<int> BulkCopyAsync<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
var result = 0;
var fileName = $"{Guid.NewGuid()}.csv";
@@ -127,12 +133,12 @@ namespace ThingsGateway.SqlSugar
// 准备多部分表单数据
var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
var list = new List<Hashtable>();
var name = db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
tableName ??= db.EntityMaintenance.GetEntityInfo<T>().DbTableName;
// 获取或创建列信息缓存
var key = "QuestDbBulkCopy" + typeof(T).FullName + typeof(T).GetHashCode();
var columns = ReflectionInoCacheService.Instance.GetOrCreate(key, () =>
db.CopyNew().DbMaintenance.GetColumnInfosByTableName(name));
db.CopyNew().DbMaintenance.GetColumnInfosByTableName(tableName));
// 构建schema信息
columns.ForEach(d =>
@@ -170,8 +176,8 @@ namespace ThingsGateway.SqlSugar
// 准备HTTP请求内容
using var httpContent = new MultipartFormDataContent(boundary);
using var fileStream = File.OpenRead(filePath);
if (!string.IsNullOrWhiteSpace(this.authorization))
client.DefaultRequestHeaders.Add("Authorization", this.authorization);
//if (!string.IsNullOrWhiteSpace(this.authorization))
// client.DefaultRequestHeaders.Add("Authorization", this.authorization);
httpContent.Add(new StringContent(schema), "schema");
var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
@@ -183,8 +189,8 @@ namespace ThingsGateway.SqlSugar
"multipart/form-data; boundary=" + boundary);
// 发送请求并处理响应
var httpResponseMessage =
await Post(client, name, httpContent).ConfigureAwait(false);
using var httpResponseMessage =
await Post(client, tableName, httpContent).ConfigureAwait(false);
var readAsStringAsync = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
var splitByLine = QuestDbRestAPHelper.SplitByLine(readAsStringAsync);
@@ -266,11 +272,12 @@ namespace ThingsGateway.SqlSugar
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="insertList">要插入的数据列表</param>
/// <param name="tableName">表名称</param>
/// <param name="dateFormat">日期格式字符串</param>
/// <returns>插入的记录数</returns>
public int BulkCopy<T>(List<T> insertList, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
public int BulkCopy<T>(List<T> insertList, string tableName = null, string dateFormat = "yyyy/M/d H:mm:ss") where T : class, new()
{
return BulkCopyAsync(insertList, dateFormat).GetAwaiter().GetResult();
return BulkCopyAsync(insertList, tableName, dateFormat).GetAwaiter().GetResult();
}
/// <summary>
@@ -280,7 +287,7 @@ namespace ThingsGateway.SqlSugar
/// <param name="httpPort">HTTP端口</param>
/// <param name="username">用户名</param>
/// <param name="password">密码</param>
private void BindHost(string host, string httpPort, string username, string password)
private void BindHost(string host, int httpPort, string username, string password)
{
url = host;
if (url.EndsWith('/'))

View File

@@ -2,9 +2,9 @@
{
public static class QuestDbSqlSugarClientExtensions
{
public static QuestDbRestAPI RestApi(this ISqlSugarClient db)
public static QuestDbRestAPI RestApi(this ISqlSugarClient db, int httpPort = 9000)
{
return new QuestDbRestAPI(db);
return new QuestDbRestAPI(db, httpPort);
}
}
}

View File

@@ -8,6 +8,6 @@
V Get<V>(string key);
IEnumerable<string> GetAllKey<V>();
void Remove<V>(string key);
V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue);
V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600);
}
}

View File

@@ -31,7 +31,7 @@ namespace ThingsGateway.SqlSugar
return ReflectionInoCore<V>.GetInstance().GetAllKey();
}
public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = int.MaxValue)
public V GetOrCreate<V>(string cacheKey, Func<V> create, int cacheDurationInSeconds = 3600)
{
return ReflectionInoCore<V>.GetInstance().GetOrCreate(cacheKey, create);
}
@@ -43,10 +43,13 @@ namespace ThingsGateway.SqlSugar
}
public class ReflectionInoCore<V>
{
private MemoryCache InstanceCache => new MemoryCache() { Expire = 60 };
private MemoryCache InstanceCache = new MemoryCache() { Expire = 180 };
private static ReflectionInoCore<V> _instance = null;
private static readonly object _instanceLock = new object();
private ReflectionInoCore() { }
private ReflectionInoCore()
{
}
public V this[string key]
{
@@ -107,10 +110,10 @@ namespace ThingsGateway.SqlSugar
return this.InstanceCache.Keys;
}
public V GetOrCreate(string cacheKey, Func<V> create)
public V GetOrCreate(string cacheKey, Func<V> create, int expire = 3600)
{
return InstanceCache.GetOrAdd<V>(cacheKey, (a) =>
create());
create(), expire);
}
}
public static class ReflectionInoHelper

View File

@@ -447,6 +447,28 @@ namespace ThingsGateway.SqlSugar
}
public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
{
if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
string cacheKey = "QuestDB.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
cacheKey = GetCacheKey(cacheKey);
if (isCache)
{
return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
{
return GetColInfo(tableName);
});
}
else
{
return GetColInfo(tableName);
}
}
private List<DbColumnInfo> GetColInfo(string tableName)
{
var sql = String.Format(GetColumnInfosByTableNameSql, tableName);
List<DbColumnInfo> result = new List<DbColumnInfo>();

View File

@@ -717,8 +717,32 @@ namespace ThingsGateway.SqlSugar
/// <returns>列信息列表</returns>
public override List<DbColumnInfo> GetColumnInfosByTableName(string tableName, bool isCache = true)
{
var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
if (string.IsNullOrEmpty(tableName)) return new List<DbColumnInfo>();
string cacheKey = "TDengine.GetColumnInfosByTableName." + this.SqlBuilder.GetNoTranslationColumnName(tableName).ToLower() + this.Context.CurrentConnectionConfig.ConfigId;
cacheKey = GetCacheKey(cacheKey);
if (isCache)
{
return this.Context.Utilities.GetReflectionInoCacheInstance().GetOrCreate(cacheKey, () =>
{
return GetColInfo(tableName);
});
}
else
{
return GetColInfo(tableName);
}
}
private List<DbColumnInfo> GetColInfo(string tableName)
{
List<DbColumnInfo> result = new List<DbColumnInfo>();
var sql = $"select * from {this.SqlBuilder.GetTranslationColumnName(tableName)} where 1=2 ";
DataTable dt = null;
try
{

View File

@@ -1,9 +1,9 @@
<Project>
<PropertyGroup>
<PluginVersion>10.10.4</PluginVersion>
<ProPluginVersion>10.10.4</ProPluginVersion>
<DefaultVersion>10.10.7</DefaultVersion>
<PluginVersion>10.10.9</PluginVersion>
<ProPluginVersion>10.10.9</ProPluginVersion>
<DefaultVersion>10.10.12</DefaultVersion>
<AuthenticationVersion>10.10.1</AuthenticationVersion>
<SourceGeneratorVersion>10.10.1</SourceGeneratorVersion>
<NET8Version>8.0.19</NET8Version>

View File

@@ -77,7 +77,8 @@ public partial class LogConsole : IDisposable
[Inject]
private ToastService ToastService { get; set; }
[Inject]
ITextFileReadService TextFileReadService { get; set; }
public void Dispose()
{
Disposed = true;
@@ -94,7 +95,7 @@ public partial class LogConsole : IDisposable
if (LogPath != null)
{
var files = TextFileReader.GetFiles(LogPath);
var files = await TextFileReadService.GetLogFiles(LogPath);
if (!files.IsSuccess)
{
Messages = new List<LogMessage>();
@@ -105,7 +106,7 @@ public partial class LogConsole : IDisposable
await Task.Run(async () =>
{
Stopwatch sw = Stopwatch.StartNew();
var result = TextFileReader.LastLog(files.Content.FirstOrDefault());
var result = await TextFileReadService.LastLogData(files.Content.FirstOrDefault());
if (result.IsSuccess)
{
Messages = result.Content.Where(a => a.LogLevel >= LogLevel).Select(a => new LogMessage((int)a.LogLevel, $"{a.LogTime} - {a.Message}{(a.ExceptionString.IsNullOrWhiteSpace() ? null : $"{Environment.NewLine}{a.ExceptionString}")}")).ToList();
@@ -143,7 +144,7 @@ public partial class LogConsole : IDisposable
{
if (LogPath != null)
{
var files = TextFileReader.GetFiles(LogPath);
var files = await TextFileReadService.GetLogFiles(LogPath);
if (files.IsSuccess)
{
foreach (var item in files.Content)

View File

@@ -26,7 +26,7 @@ public class PlatformService : IPlatformService
public async Task OnLogExport(string logPath)
{
var files = TextFileReader.GetFiles(logPath);
var files = TextFileReader.GetLogFiles(logPath);
if (!files.IsSuccess)
{
return;

View File

@@ -33,5 +33,6 @@ public class Startup : AppStartup
}
services.AddScoped<IPlatformService, PlatformService>();
services.AddSingleton<ITextFileReadService, TextFileReadService>();
}
}

View File

@@ -59,7 +59,7 @@ public static class TextFileReader
/// </summary>
/// <param name="directoryPath">目录路径</param>
/// <returns>包含文件信息的列表</returns>
public static OperResult<List<string>> GetFiles(string directoryPath)
public static OperResult<List<string>> GetLogFiles(string directoryPath)
{
OperResult<List<string>> result = new(); // 初始化结果对象
// 检查目录是否存在
@@ -91,7 +91,7 @@ public static class TextFileReader
return result;
}
public static OperResult<List<LogData>> LastLog(string file, int lineCount = 200)
public static OperResult<List<LogData>> LastLogData(string file, int lineCount = 200)
{
if (!File.Exists(file))
return new OperResult<List<LogData>>("The file path is invalid");
@@ -104,7 +104,7 @@ public static class TextFileReader
{
var fileInfo = new FileInfo(file);
var length = fileInfo.Length;
var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLog)}_{file})";
var cacheKey = $"{nameof(TextFileReader)}_{nameof(LastLogData)}_{file})";
if (_cache.TryGetValue<LogDataCache>(cacheKey, out var cachedData))
{
if (cachedData != null && cachedData.Length == length)

View File

@@ -0,0 +1,24 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Foundation;
public interface ITextFileReadService
{
/// <summary>
/// 获取指定目录下所有文件信息
/// </summary>
/// <param name="directoryPath">目录路径</param>
/// <returns>包含文件信息的列表</returns>
public Task<OperResult<List<string>>> GetLogFiles(string directoryPath);
public Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200);
}

View File

@@ -8,13 +8,13 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Riok.Mapperly.Abstractions;
using TouchSocket.Dmtp;
namespace ThingsGateway.Upgrade;
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
public static partial class UpgradeMapper
namespace ThingsGateway.Foundation;
public class TextFileReadService : ITextFileReadService
{
public static partial List<TcpSessionClientDto> AdaptListTcpSessionClientDto(this List<TcpDmtpSessionClient> src);
public Task<OperResult<List<string>>> GetLogFiles(string directoryPath) => Task.FromResult(TextFileReader.GetLogFiles(directoryPath));
public Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200) => Task.FromResult(TextFileReader.LastLogData(file, lineCount));
}

View File

@@ -1,43 +0,0 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RouteAttribute = Microsoft.AspNetCore.Mvc.RouteAttribute;
namespace ThingsGateway.Management;
[ApiDescriptionSettings("ThingsGateway.OpenApi", Order = 200)]
[Route("openApi/autoUpdate")]
[RolePermission]
[RequestAudit]
[ApiController]
[Authorize(AuthenticationSchemes = "Bearer")]
public class AutoUpdateController : ControllerBase
{
private IUpdateZipFileHostedService _updateZipFileService;
public AutoUpdateController(IUpdateZipFileHostedService updateZipFileService)
{
_updateZipFileService = updateZipFileService;
}
/// <summary>
/// 检查更新
/// </summary>
/// <returns></returns>
[HttpPost("update")]
public async Task Update()
{
var data = await _updateZipFileService.GetList().ConfigureAwait(false);
if (data.Count != 0)
await _updateZipFileService.Update(data.OrderByDescending(a => a.Version).FirstOrDefault()).ConfigureAwait(false);
}
}

View File

@@ -145,7 +145,7 @@ public class RuntimeInfoController : ControllerBase, IRpcServer
public SqlSugarPagedList<PluginInfo> GetPluginInfos([FromQuery][TouchSocket.WebApi.FromBody] PluginInfoPageInput input)
{
//指定关键词搜索为插件FullName
return GlobalData.PluginService.GetList().WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
return (GlobalData.PluginService.GetPluginListSync()).WhereIF(!input.Name.IsNullOrWhiteSpace(), a => a.Name == input.Name)
.ToPagedList(input);
}
}

View File

@@ -17,8 +17,10 @@ namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 后台日志表
///</summary>
#if !Management
[SugarTable("backend_log", TableDescription = "后台日志表")]
[Tenant(SqlSugarConst.DB_Log)]
#endif
public class BackendLog : PrimaryIdEntity
{
/// <summary>

View File

@@ -22,9 +22,11 @@ namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 通道表
/// </summary>
#if !Management
[SugarTable("channel", TableDescription = "通道表")]
[Tenant(SqlSugarConst.DB_Custom)]
[SugarIndex("unique_channel_name", nameof(Channel.Name), OrderByType.Asc, true)]
#endif
public class Channel : ChannelOptionsBase, IPrimaryIdEntity, IBaseDataEntity, IBaseEntity
{
/// <summary>

View File

@@ -22,9 +22,11 @@ namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 设备表
/// </summary>
#if !Management
[SugarTable("device", TableDescription = "设备表")]
[Tenant(SqlSugarConst.DB_Custom)]
[SugarIndex("unique_device_name", nameof(Device.Name), OrderByType.Asc, true)]
#endif
public class Device : BaseDataEntity, IValidatableObject
{
public override string ToString()

View File

@@ -15,8 +15,10 @@ namespace ThingsGateway.Gateway.Application;
/// <summary>
/// Rpc写入日志
///</summary>
#if !Management
[SugarTable("rpc_log", TableDescription = "RPC操作日志")]
[Tenant(SqlSugarConst.DB_Log)]
#endif
public class RpcLog : PrimaryIdEntity
{
/// <summary>

View File

@@ -21,10 +21,12 @@ namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 设备变量表
/// </summary>
#if !Management
[SugarTable("variable", TableDescription = "设备变量表")]
[Tenant(SqlSugarConst.DB_Custom)]
[SugarIndex("index_device", nameof(Variable.DeviceId), OrderByType.Asc)]
[SugarIndex("unique_deviceid_variable_name", nameof(Variable.Name), OrderByType.Asc, nameof(Variable.DeviceId), OrderByType.Asc, true)]
#endif
public class Variable : BaseDataEntity, IValidatableObject
{
/// <summary>
@@ -48,6 +50,7 @@ public class Variable : BaseDataEntity, IValidatableObject
private long deviceId;
private int? arrayLength;
private int alarmDelay;
private int alarmLevel;
private ProtectTypeEnum protectType = ProtectTypeEnum.ReadWrite;
private DataTypeEnum dataType = DataTypeEnum.Int16;
@@ -274,6 +277,15 @@ public class Variable : BaseDataEntity, IValidatableObject
public Dictionary<long, Dictionary<string, string>>? VariablePropertys { get => variablePropertys; set => variablePropertys = value; }
#region
/// <summary>
/// 报警等级
/// </summary>
[SugarColumn(ColumnDescription = "报警等级")]
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
public int AlarmLevel { get => alarmLevel; set => alarmLevel = value; }
/// <summary>
/// 报警延时
/// </summary>

View File

@@ -39,4 +39,9 @@ public enum EventTypeEnum
/// 准备恢复
/// </summary>
PrepareFinish,
/// <summary>
/// 报警确认并恢复
/// </summary>
ConfirmAndFinish,
}

View File

@@ -8,7 +8,6 @@
// QQ群605534569
// ------------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
namespace Microsoft.Extensions.DependencyInjection;
@@ -19,30 +18,19 @@ namespace Microsoft.Extensions.DependencyInjection;
[ThingsGateway.DependencyInjection.SuppressSniffer]
public static class ServiceCollectionHostedServiceExtensions
{
/// <summary>
/// Add an <see cref="IHostedService"/> registration for the given type.
/// </summary>
/// <typeparam name="THostedService">An <see cref="IHostedService"/> to register.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to register with.</param>
/// <returns>The original <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddGatewayHostedService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services)
where THostedService : class, IHostedService
{
services.AddSingleton<THostedService>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>(seriveProvider => seriveProvider.GetService<THostedService>()));
return services;
}
/// <summary>
/// Add an <see cref="IHostedService"/> registration for the given type.
/// </summary>
public static IServiceCollection AddGatewayHostedService<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services)
where TService : class, IHostedService
where TService : class
where THostedService : class, IHostedService, TService
{
services.AddSingleton(typeof(TService), typeof(THostedService));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, TService>(seriveProvider => seriveProvider.GetService<TService>()));
services.AddSingleton<THostedService>();
services.AddHostedService<THostedService>(a => a.GetService<THostedService>());
services.AddSingleton<TService>(a => a.GetService<THostedService>());
return services;
}
}

View File

@@ -1,6 +1,24 @@
{
"ThingsGateway.Management.Application._Imports": {
"Restart": "Restart",
"Upgrade": "Upgrade"
},
"ThingsGateway.Management.Application.SaveUpdateZipFile": {
"DownTemplate": "Download Template",
"SaveUpdateZipFile": "Upload Version Package"
},
"ThingsGateway.Management.Application.UpdateZipFile": {
"AppName": "AppName",
"Architecture": "Architecture",
"DotNetVersion": "DotNetVersion",
"FilePath": "FilePath",
"FileSize": "FileSize",
"MinimumCompatibleVersion": "MinimumCompatibleVersion",
"OSPlatform": "OSPlatform",
"Version": "Version"
},
"ThingsGateway.Gateway.Application.DefaultDiagram": {
"ThingsGateway.Gateway.Application.INode": {
"Actuator": "Actuator",
"AlarmChangedTriggerNode": "AlarmStateTrigger",
@@ -53,11 +71,7 @@
},
"ThingsGateway.Management.AutoUpdateController": {
"AutoUpdateController": "AutoUpdate",
"Update": "Update"
},
"ThingsGateway.Management.RedundancyHostedService": {
"ThingsGateway.Gateway.Application.RedundancyHostedService": {
"ErrorSynchronizingData": "Synchronize data to standby site error",
"RedundancyDisable": "Redundant gateway site not enabled",
"RedundancyDup": "Redundant station settings duplicated",
@@ -67,10 +81,10 @@
"SwitchNormalState": "Local machine (primary site) will switch to normal state",
"SwitchSlaveState": "Master site has recovered, local machine (standby) will switch to standby state"
},
"ThingsGateway.Management.RedundancyOptions": {
"ThingsGateway.Gateway.Application.RedundancyOptions": {
"Confirm": "Confirm switching to redundant state",
"Enable": "Enable Dual-Machine Redundancy",
"ForcedSync": "Forced Synchronous",
"RedundancyForcedSync": "Forced Synchronous",
"ForcedSyncWarning": "Forcing synchronization will generate database configuration information.Are you sure you want to continue?",
"HeartbeatInterval": "Heartbeat Interval",
"IsMaster": "IsMaster",
@@ -86,13 +100,13 @@
"SyncInterval": "Data Synchronization Interval",
"VerifyToken": "Verification Token"
},
"ThingsGateway.Management.RedundancyService": {
"ThingsGateway.Gateway.Application.RedundancyService": {
"EditRedundancyOption": "EditRedundancyOption"
},
"ThingsGateway.Management.UpdateZipFileHostedService": {
"ThingsGateway.Gateway.Application.UpdateZipFileService": {
"Update": "New version detected"
},
"ThingsGateway.Upgrade.UpdateZipFile": {
"ThingsGateway.Gateway.Application.UpdateZipFile": {
"AppName": "AppName",
"Architecture": "Architecture",
"DotNetVersion": "DotNetVersion",
@@ -106,6 +120,7 @@
"ThingsGateway.Gateway.Application.AlarmVariable": {
"AlarmCode": "AlarmCode",
"AlarmDelay": "AlarmDelay",
"AlarmLevel": "AlarmLevel",
"AlarmEnable": "AlarmEnable",
"AlarmLimit": "AlarmLimit",
"AlarmText": "AlarmText",
@@ -437,6 +452,7 @@
"ThingsGateway.Gateway.Application.Variable": {
"AddressOrOtherMethodNotNull": "Variable address or special method cannot be empty at the same time",
"AlarmDelay": "AlarmDelay",
"AlarmLevel": "AlarmLevel",
"ArrayLength": "ArrayLength",
"BoolCloseAlarmEnable": "BoolCloseAlarmEnable",
"BoolCloseAlarmText": "BoolCloseAlarmText",

View File

@@ -1,6 +1,24 @@
{
"ThingsGateway.Management.Application._Imports": {
"Restart": "重启",
"Upgrade": "更新"
},
"ThingsGateway.Management.Application.SaveUpdateZipFile": {
"DownTemplate": "下载模板",
"SaveUpdateZipFile": "上传版本包"
},
"ThingsGateway.Management.Application.UpdateZipFile": {
"AppName": "名称",
"Architecture": "架构",
"DotNetVersion": ".net版本",
"FilePath": "文件路径",
"FileSize": "文件大小",
"MinimumCompatibleVersion": "最小兼容版本",
"OSPlatform": "系统版本",
"Version": "版本"
},
"ThingsGateway.Gateway.Application.DefaultDiagram": {
"ThingsGateway.Gateway.Application.INode": {
"Actuator": "执行",
"AlarmChangedTriggerNode": "报警状态触发器",
@@ -51,11 +69,8 @@
"RulesId": "名称"
},
"ThingsGateway.Management.AutoUpdateController": {
"AutoUpdateController": "程序更新",
"Update": "更新"
},
"ThingsGateway.Management.RedundancyHostedService": {
"ThingsGateway.Gateway.Application.RedundancyHostedService": {
"ErrorSynchronizingData": "同步数据到从站错误",
"RedundancyDisable": "不启用网关冗余站点",
"RedundancyDup": "主备站设置重复",
@@ -65,10 +80,10 @@
"SwitchNormalState": "本机(主站)将切换到正常状态",
"SwitchSlaveState": "主站已恢复,本机(从站)将切换到备用状态"
},
"ThingsGateway.Management.RedundancyOptions": {
"ThingsGateway.Gateway.Application.RedundancyOptions": {
"Confirm": "确认切换冗余状态",
"Enable": "启用双机冗余",
"ForcedSync": "强制同步",
"RedundancyForcedSync": "强制同步",
"ForcedSyncWarning": "强制同步会生成数据库配置信息,是否继续?",
"HeartbeatInterval": "心跳间隔",
"IsMaster": "是否为主站",
@@ -84,13 +99,13 @@
"SyncInterval": "数据同步间隔",
"VerifyToken": "Token"
},
"ThingsGateway.Management.RedundancyService": {
"ThingsGateway.Gateway.Application.RedundancyService": {
"EditRedundancyOption": "修改网关冗余配置"
},
"ThingsGateway.Management.UpdateZipFileHostedService": {
"ThingsGateway.Gateway.Application.UpdateZipFileService": {
"Update": "检测到新版本"
},
"ThingsGateway.Upgrade.UpdateZipFile": {
"ThingsGateway.Gateway.Application.UpdateZipFile": {
"AppName": "名称",
"Architecture": "架构",
"DotNetVersion": ".net版本",
@@ -105,6 +120,7 @@
"ThingsGateway.Gateway.Application.AlarmVariable": {
"AlarmCode": "报警值",
"AlarmDelay": "报警延时",
"AlarmLevel": "报警等级",
"AlarmEnable": "报警使能",
"AlarmLimit": "报警限值",
"AlarmText": "报警文本",
@@ -438,6 +454,7 @@
"ThingsGateway.Gateway.Application.Variable": {
"AddressOrOtherMethodNotNull": " 变量地址或特殊方法不能同时为空 ",
"AlarmDelay": "报警延时",
"AlarmLevel": "报警等级",
"ArrayLength": "数组长度",
"BoolCloseAlarmEnable": "布尔关报警使能",
"BoolCloseAlarmText": "布尔关报警文本",

View File

@@ -10,8 +10,6 @@
using Riok.Mapperly.Abstractions;
using ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
public static partial class GatewayMapper

View File

@@ -63,6 +63,11 @@ public class AlarmVariable : PrimaryIdEntity, IDBHistoryAlarm
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
public DataTypeEnum DataType { get; set; }
/// <inheritdoc cref="Variable.AlarmLevel"/>
[SugarColumn(ColumnDescription = "报警等级", IsNullable = false)]
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]
public int AlarmLevel { get; set; }
/// <inheritdoc cref="VariableRuntime.AlarmCode"/>
[SugarColumn(ColumnDescription = "报警值", IsNullable = false)]
[AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)]

View File

@@ -117,7 +117,7 @@ public class ChannelRuntime : Channel, IChannelOptions, IDisposable
public void Init()
{
// 通过插件名称获取插件信息
PluginInfo = GlobalData.PluginService.GetList().FirstOrDefault(A => A.FullName == PluginName);
PluginInfo = GlobalData.PluginService.GetPluginListSync().FirstOrDefault(A => A.FullName == PluginName);
GlobalData.IdChannels.TryRemove(Id, out _);
GlobalData.Channels.TryRemove(Name, out _);

View File

@@ -27,7 +27,7 @@ public class PluginInfo
/// 插件文件名称.插件类型名称
/// </summary>
[AutoGenerateColumn(Ignore = true)]
public string FullName => PluginServiceUtil.GetFullName(FileName, Name);
public string FullName => PluginInfoUtil.GetFullName(FileName, Name);
/// <summary>
/// 插件文件名称
@@ -70,8 +70,5 @@ public class PluginInfo
/// </summary>
[IgnoreExcel]
[SugarColumn(IsIgnore = true)]
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
[AutoGenerateColumn(Ignore = true)]
public string Directory { get; set; }
}

View File

@@ -46,6 +46,8 @@ public partial class VariableRuntime : Variable, IVariable, IDisposable
private object _value;
private object lastSetValue;
private object rawValue;
internal object AlarmLockObject = new();
internal bool AlarmConfirm;
private DeviceRuntime? deviceRuntime;
private IVariableSource? variableSource;
private VariableMethod? variableMethod;

View File

@@ -8,7 +8,7 @@
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 系统配置种子数据

View File

@@ -218,7 +218,7 @@ internal sealed class AlarmTask : IDisposable
if (alarmEnum == null)
{
// 如果仍未获取到报警类型,则触发需恢复报警事件(如果存在)
AlarmChange(item, null, text, EventTypeEnum.Finish, alarmEnum, delay);
AlarmChange(item, null, text, true, alarmEnum, delay);
}
else
{
@@ -233,14 +233,14 @@ internal sealed class AlarmTask : IDisposable
if (result)
{
// 如果表达式结果为true则触发报警事件
AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
AlarmChange(item, limit, text, false, alarmEnum, delay);
}
}
}
else
{
// 如果不存在报警约束表达式,则直接触发报警事件
AlarmChange(item, limit, text, EventTypeEnum.Alarm, alarmEnum, delay);
AlarmChange(item, limit, text, false, alarmEnum, delay);
}
}
}
@@ -251,193 +251,227 @@ internal sealed class AlarmTask : IDisposable
/// <param name="item">要处理的变量</param>
/// <param name="limit">报警限制值</param>
/// <param name="text">报警文本</param>
/// <param name="eventEnum">报警事件类型枚举</param>
/// <param name="finish">是否恢复</param>
/// <param name="alarmEnum">报警类型枚举</param>
/// <param name="delay">报警延时</param>
private static void AlarmChange(VariableRuntime item, object limit, string text, EventTypeEnum eventEnum, AlarmTypeEnum? alarmEnum, int delay)
private static void AlarmChange(VariableRuntime item, object limit, string text, bool finish, AlarmTypeEnum? alarmEnum, int delay)
{
bool changed = false;
if (eventEnum == EventTypeEnum.Finish)
lock (item.AlarmLockObject)
{
// 如果是需恢复报警事件
// 如果实时报警列表中不存在该变量,则直接返回
if (!GlobalData.RealAlarmIdVariables.ContainsKey(item.Id))
bool changed = false;
if (finish)
{
return;
}
}
else if (eventEnum == EventTypeEnum.Alarm)
{
// 如果是触发报警事件
// 在实时报警列表中查找该变量
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var variable))
{
// 如果变量已经处于相同的报警类型,则直接返回
if (item.AlarmType == alarmEnum)
// 如果是需恢复报警事件
// 如果实时报警列表中不存在该变量,则直接返回
if (!GlobalData.RealAlarmIdVariables.ContainsKey(item.Id))
{
return;
}
}
// 更新变量的报警信息和事件时间
if (eventEnum == EventTypeEnum.Alarm)
{
var now = DateTime.Now;
//添加报警延时策略
if (delay > 0)
{
if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.PrepareAlarm)
{
item.EventType = EventTypeEnum.PrepareAlarm;//准备报警
item.PrepareAlarmEventTime = now;
}
else
{
if (item.EventType == EventTypeEnum.PrepareAlarm)
{
if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
{
//超过延时时间,触发报警
item.EventType = EventTypeEnum.Alarm;
item.AlarmTime = now;
item.EventTime = now;
item.AlarmType = alarmEnum;
item.AlarmLimit = limit.ToString();
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
else if (item.EventType == EventTypeEnum.Alarm && item.AlarmType != alarmEnum)
{
//报警类型改变,重新计时
if (item.PrepareAlarmEventTime == null)
item.PrepareAlarmEventTime = now;
if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
{
//超过延时时间,触发报警
item.EventType = EventTypeEnum.Alarm;
item.AlarmTime = now;
item.EventTime = now;
item.AlarmType = alarmEnum;
item.AlarmLimit = limit.ToString();
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
else
{
return;
}
}
}
else
{
if (item.EventType != EventTypeEnum.Confirm)
item.AlarmConfirm = false;
// 如果是触发报警事件
item.EventType = eventEnum;
item.AlarmTime = now;
item.EventTime = now;
item.AlarmType = alarmEnum;
item.AlarmLimit = limit.ToString();
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
else if (eventEnum == EventTypeEnum.Finish)
{
var now = DateTime.Now;
//添加报警延时策略
if (delay > 0)
{
if (item.EventType != EventTypeEnum.Finish && item.EventType != EventTypeEnum.PrepareFinish)
// 在实时报警列表中查找该变量
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var variable))
{
item.EventType = EventTypeEnum.PrepareFinish;//准备报警
item.PrepareFinishEventTime = now;
// 如果变量已经处于相同的报警类型,则直接返回
if (item.AlarmType == alarmEnum)
return;
}
else
}
// 更新变量的报警信息和事件时间
if (!finish)
{
var now = DateTime.Now;
//添加报警延时策略
if (delay > 0)
{
if (item.EventType == EventTypeEnum.PrepareFinish)
if (item.EventType != EventTypeEnum.Alarm && item.EventType != EventTypeEnum.PrepareAlarm)
{
if ((now - item.PrepareFinishEventTime!.Value).TotalMilliseconds > delay)
item.EventType = EventTypeEnum.PrepareAlarm;//准备报警
item.PrepareAlarmEventTime = now;
}
else
{
if (item.EventType == EventTypeEnum.PrepareAlarm)
{
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
{
item.AlarmType = oldAlarm.AlarmType;
item.EventType = eventEnum;
item.AlarmLimit = oldAlarm.AlarmLimit;
item.AlarmCode = oldAlarm.AlarmCode;
item.RecoveryCode = item.Value.ToString();
item.AlarmText = oldAlarm.AlarmText;
item.EventTime = DateTime.Now;
item.PrepareFinishEventTime = null;
//超过延时时间,触发报警
item.EventType = EventTypeEnum.Alarm;
item.AlarmTime = now;
item.EventTime = now;
item.AlarmType = alarmEnum;
item.AlarmLimit = limit.ToString();
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
else if (item.EventType == EventTypeEnum.Alarm && item.AlarmType != alarmEnum)
{
//报警类型改变,重新计时
if (item.PrepareAlarmEventTime == null)
item.PrepareAlarmEventTime = now;
if ((now - item.PrepareAlarmEventTime!.Value).TotalMilliseconds > delay)
{
//超过延时时间,触发报警
item.EventType = EventTypeEnum.Alarm;
item.AlarmTime = now;
item.EventTime = now;
item.AlarmType = alarmEnum;
item.AlarmLimit = limit.ToString();
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
else
{
return;
}
}
else
{
return;
}
}
else
{
// 如果是触发报警事件
item.EventType = EventTypeEnum.Alarm;
item.AlarmTime = now;
item.EventTime = now;
item.AlarmType = alarmEnum;
item.AlarmLimit = limit.ToString();
item.AlarmCode = item.Value.ToString();
item.RecoveryCode = string.Empty;
item.AlarmText = text;
item.PrepareAlarmEventTime = null;
changed = true;
}
}
else
{
// 如果是需恢复报警事件
// 获取旧的报警信息
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
var now = DateTime.Now;
//添加报警延时策略
if (delay > 0)
{
item.AlarmType = oldAlarm.AlarmType;
item.EventType = eventEnum;
item.AlarmLimit = oldAlarm.AlarmLimit;
item.AlarmCode = oldAlarm.AlarmCode;
item.RecoveryCode = item.Value.ToString();
item.AlarmText = oldAlarm.AlarmText;
item.EventTime = DateTime.Now;
item.PrepareFinishEventTime = null;
changed = true;
if (item.EventType != EventTypeEnum.Finish && item.EventType != EventTypeEnum.PrepareFinish)
{
item.EventType = EventTypeEnum.PrepareFinish;
item.PrepareFinishEventTime = now;
}
else
{
if (item.EventType == EventTypeEnum.PrepareFinish)
{
if ((now - item.PrepareFinishEventTime!.Value).TotalMilliseconds > delay)
{
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
{
item.AlarmType = oldAlarm.AlarmType;
item.AlarmLimit = oldAlarm.AlarmLimit;
item.AlarmCode = oldAlarm.AlarmCode;
item.RecoveryCode = item.Value.ToString();
item.AlarmText = oldAlarm.AlarmText;
if (item.EventType != EventTypeEnum.Finish)
{
item.EventTime = now;
}
item.EventType = EventTypeEnum.Finish;
item.PrepareFinishEventTime = null;
changed = true;
}
}
}
else
{
return;
}
}
}
else
{
// 如果是需恢复报警事件
// 获取旧的报警信息
if (GlobalData.RealAlarmIdVariables.TryGetValue(item.Id, out var oldAlarm))
{
item.AlarmType = oldAlarm.AlarmType;
item.AlarmLimit = oldAlarm.AlarmLimit;
item.AlarmCode = oldAlarm.AlarmCode;
item.RecoveryCode = item.Value.ToString();
item.AlarmText = oldAlarm.AlarmText;
if (item.EventType != EventTypeEnum.Finish)
{
item.EventTime = now;
}
item.EventType = EventTypeEnum.Finish;
item.PrepareFinishEventTime = null;
changed = true;
}
}
}
}
// 触发报警变化事件
if (changed)
{
if (item.EventType == EventTypeEnum.Alarm)
// 触发报警变化事件
if (changed)
{
// 如果是触发报警事件
//lock (GlobalData. RealAlarmVariables)
if (item.EventType == EventTypeEnum.Alarm)
{
// 从实时报警列表中移除旧的报警信息,并添加新的报警信息
GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
// 如果是触发报警事件
//lock (GlobalData. RealAlarmVariables)
{
// 从实时报警列表中移除旧的报警信息,并添加新的报警信息
GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
}
}
else if (item.EventType == EventTypeEnum.Finish)
{
// 如果是需恢复报警事件,则从实时报警列表中移除该变量
if (item.AlarmConfirm)
{
GlobalData.RealAlarmIdVariables.TryRemove(item.Id, out _);
item.EventType = EventTypeEnum.ConfirmAndFinish;
}
else
{
GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
}
}
GlobalData.AlarmChange(item.AdaptAlarmVariable());
}
else if (item.EventType == EventTypeEnum.Finish)
{
// 如果是需恢复报警事件,则从实时报警列表中移除该变量
GlobalData.RealAlarmIdVariables.TryRemove(item.Id, out _);
//GlobalData.RealAlarmIdVariables.AddOrUpdate(item.Id, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
}
GlobalData.AlarmChange(item.AdaptAlarmVariable());
}
}
public void ConfirmAlarm(long variableId)
{
// 如果是确认报警事件
if (GlobalData.AlarmEnableIdVariables.TryGetValue(variableId, out var variableRuntime))
if (GlobalData.AlarmEnableIdVariables.TryGetValue(variableId, out var item))
{
variableRuntime.EventType = EventTypeEnum.Confirm;
variableRuntime.EventTime = DateTime.Now;
GlobalData.RealAlarmIdVariables.AddOrUpdate(variableId, a => variableRuntime.AdaptAlarmVariable(), (a, b) => variableRuntime.AdaptAlarmVariable());
GlobalData.AlarmChange(variableRuntime.AdaptAlarmVariable());
lock (item.AlarmLockObject)
{
item.AlarmConfirm = true;
item.EventTime = DateTime.Now;
if (item.EventType == EventTypeEnum.Finish)
{
item.EventType = EventTypeEnum.ConfirmAndFinish;
GlobalData.RealAlarmIdVariables.TryRemove(variableId, out _);
}
else
{
item.EventType = EventTypeEnum.Confirm;
GlobalData.RealAlarmIdVariables.AddOrUpdate(variableId, a => item.AdaptAlarmVariable(), (a, b) => item.AdaptAlarmVariable());
}
GlobalData.AlarmChange(item.AdaptAlarmVariable());
}
}
}

View File

@@ -8,11 +8,9 @@
// QQ群605534569
// ------------------------------------------------------------------------------
using Microsoft.Extensions.Hosting;
namespace ThingsGateway.Gateway.Application;
public interface IAlarmHostedService : IHostedService
public interface IAlarmHostedService
{
/// <summary>
/// 确认报警

View File

@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using TouchSocket.Dmtp.Rpc;
namespace ThingsGateway.Gateway.Application
{
public interface IRealAlarmService
{
[DmtpRpc]
Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariables();
}
}

View File

@@ -8,20 +8,16 @@
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.DB;
namespace ThingsGateway.Upgrade;
namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 关系表种子数据
/// 设备采集报警后台服务
/// </summary>
public class SysRelationSeedData : ISqlSugarEntitySeedData<SysRelation>
internal sealed class RealAlarmService : IRealAlarmService
{
/// <inheritdoc/>
public IEnumerable<SysRelation> SeedData()
public Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariables()
{
var data = SeedDataUtil.GetSeedData<SysRelation>(PathExtensions.CombinePathWithOs("SeedData", "Upgrade", "seed_upgrade_relation.json"));
var assembly = GetType().Assembly;
return SeedDataUtil.GetSeedDataByJson<SysRelation>(SeedDataUtil.GetManifestResourceStream(assembly, "SeedData.Upgrade.seed_upgrade_relation.json")).Concat(data);
return GlobalData.GetCurrentUserRealAlarmVariables();
}
}

View File

@@ -260,7 +260,7 @@ internal sealed class ChannelService : BaseService<Channel>, IChannelService
HashSet<long>? channel = null;
if (exportFilter.PluginType != null)
{
var pluginInfo = GlobalData.PluginService.GetList(exportFilter.PluginType).Select(a => a.FullName).ToHashSet();
var pluginInfo = GlobalData.PluginService.GetPluginListSync(exportFilter.PluginType).Select(a => a.FullName).ToHashSet();
channel = GlobalData.IdChannels.Where(a => pluginInfo.Contains(a.Value.PluginName)).Select(a => a.Value.Id).ToHashSet();
}
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);

View File

@@ -216,7 +216,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
}
if (exportFilter.PluginType != null)
{
var pluginInfo = GlobalData.PluginService.GetList(exportFilter.PluginType).Select(a => a.FullName).ToHashSet();
var pluginInfo = GlobalData.PluginService.GetPluginListSync(exportFilter.PluginType).Select(a => a.FullName).ToHashSet();
channel = (GlobalData.IdChannels).Where(a => pluginInfo.Contains(a.Value.PluginName)).Select(a => a.Value.Id).ToHashSet();
}
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
@@ -238,7 +238,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
}
if (exportFilter.PluginType != null)
{
var pluginInfo = GlobalData.PluginService.GetList(exportFilter.PluginType).Select(a => a.FullName).ToHashSet();
var pluginInfo = GlobalData.PluginService.GetPluginListSync(exportFilter.PluginType).Select(a => a.FullName).ToHashSet();
channel = (GlobalData.IdChannels).Where(a => pluginInfo.Contains(a.Value.PluginName)).Select(a => a.Value.Id).ToHashSet();
}
var dataScope = await GlobalData.SysUserService.GetCurrentUserDataScopeAsync().ConfigureAwait(false);
@@ -417,7 +417,7 @@ internal sealed class DeviceService : BaseService<Device>, IDeviceService
ImportPreviewOutput<Device> deviceImportPreview = new();
// 获取所有驱动程序,并将驱动程序名称作为键构建字典
var driverPluginNameDict = _pluginService.GetList().DistinctBy(a => a.Name).ToDictionary(a => a.Name);
var driverPluginNameDict = _pluginService.GetPluginListSync().DistinctBy(a => a.Name).ToDictionary(a => a.Name);
ConcurrentDictionary<string, (Type, Dictionary<string, PropertyInfo>, Dictionary<string, PropertyInfo>)> propertysDict = new();
foreach (var sheetName in sheetNames)
{

View File

@@ -50,7 +50,7 @@ HashSet<string> pluginSheetNames,
foreach (var plugin in pluginSheetNames)
{
var filtered = FilterPluginDevices(data, plugin, channelDicts);
var filtResult = PluginServiceUtil.GetFileNameAndTypeName(plugin);
var filtResult = PluginInfoUtil.GetFileNameAndTypeName(plugin);
var pluginSheets = GetPluginSheets(filtered, propertysDict, plugin);
result.Add(filtResult.TypeName, pluginSheets);
}
@@ -75,7 +75,7 @@ string? channelName = null)
foreach (var plugin in pluginSheetNames)
{
var filtered = FilterPluginDevices(data2, plugin, channelDicts);
var filtResult = PluginServiceUtil.GetFileNameAndTypeName(plugin);
var filtResult = PluginInfoUtil.GetFileNameAndTypeName(plugin);
var pluginSheets = GetPluginSheets(filtered, propertysDict, plugin);
result.Add(filtResult.TypeName, pluginSheets);
}
@@ -288,10 +288,10 @@ string? channelName)
ImportPreviewOutput<Device> deviceImportPreview = new();
// 获取所有驱动程序,并将驱动程序的完整名称作为键构建字典
var driverPluginFullNameDict = GlobalData.PluginService.GetList().ToDictionary(a => a.FullName);
var driverPluginFullNameDict = GlobalData.PluginService.GetPluginListSync().ToDictionary(a => a.FullName);
// 获取所有驱动程序,并将驱动程序名称作为键构建字典
var driverPluginNameDict = GlobalData.PluginService.GetList().DistinctBy(a => a.Name).ToDictionary(a => a.Name);
var driverPluginNameDict = GlobalData.PluginService.GetPluginListSync().DistinctBy(a => a.Name).ToDictionary(a => a.Name);
ConcurrentDictionary<string, (Type, Dictionary<string, PropertyInfo>, Dictionary<string, PropertyInfo>)> propertysDict = new();
var sheetNames = uSheetDatas.sheets.Keys.ToList();

View File

@@ -21,7 +21,7 @@ internal sealed class BackendLogService : BaseService<BackendLog>, IBackendLogSe
/// <summary>
/// 最新十条
/// </summary>
public async Task<List<BackendLog>> GetNewLog()
public async Task<List<BackendLog>> GetNewBackendLog()
{
using var db = GetDB();
var data = await db.Queryable<BackendLog>().OrderByDescending(a => a.LogTime).Take(10).ToListAsync().ConfigureAwait(false);
@@ -32,7 +32,7 @@ internal sealed class BackendLogService : BaseService<BackendLog>, IBackendLogSe
/// 表格查询
/// </summary>
/// <param name="option">查询条件</param>
public Task<QueryData<BackendLog>> PageAsync(QueryPageOptions option)
public Task<QueryData<BackendLog>> BackendLogPageAsync(QueryPageOptions option)
{
return QueryAsync(option);
}
@@ -58,7 +58,7 @@ internal sealed class BackendLogService : BaseService<BackendLog>, IBackendLogSe
/// </summary>
/// <param name="day">天</param>
/// <returns>统计信息</returns>
public async Task<List<BackendLogDayStatisticsOutput>> StatisticsByDayAsync(int day)
public async Task<List<BackendLogDayStatisticsOutput>> BackendLogStatisticsByDayAsync(int day)
{
using var db = GetDB();
//取最近七天

View File

@@ -10,13 +10,9 @@
using BootstrapBlazor.Components;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
namespace ThingsGateway.Gateway.Application;
[GeneratorRpcProxy(GeneratorFlag = GeneratorFlag.ExtensionAsync)]
public interface IBackendLogService : IRpcServer
public interface IBackendLogService
{
/// <summary>
/// 删除 BackendLog 表中的所有记录
@@ -24,29 +20,25 @@ public interface IBackendLogService : IRpcServer
/// <remarks>
/// 调用此方法会删除 BackendLog 表中的所有记录。
/// </remarks>
[DmtpRpc]
Task DeleteBackendLogAsync();
/// <summary>
/// 获取最新的十条 BackendLog 记录
/// </summary>
/// <returns>最新的十条记录</returns>
[DmtpRpc]
Task<List<BackendLog>> GetNewLog();
Task<List<BackendLog>> GetNewBackendLog();
/// <summary>
/// 分页查询 BackendLog 数据
/// </summary>
/// <param name="option">查询选项</param>
/// <returns>查询到的数据</returns>
[DmtpRpc]
Task<QueryData<BackendLog>> PageAsync(QueryPageOptions option);
Task<QueryData<BackendLog>> BackendLogPageAsync(QueryPageOptions option);
/// <summary>
/// 获取最近一段时间内每天的后端日志统计信息
/// </summary>
/// <param name="day">要统计的天数</param>
/// <returns>按天统计的后端日志信息列表</returns>
[DmtpRpc]
Task<List<BackendLogDayStatisticsOutput>> StatisticsByDayAsync(int day);
Task<List<BackendLogDayStatisticsOutput>> BackendLogStatisticsByDayAsync(int day);
}

View File

@@ -26,19 +26,19 @@ public interface IRpcLogService
/// 获取最新的十条 RpcLog 记录
/// </summary>
/// <returns>最新的十条记录</returns>
Task<List<RpcLog>> GetNewLog();
Task<List<RpcLog>> GetNewRpcLog();
/// <summary>
/// 分页查询 RpcLog 数据
/// </summary>
/// <param name="option">查询选项</param>
/// <returns>查询到的数据</returns>
Task<QueryData<RpcLog>> PageAsync(QueryPageOptions option);
Task<QueryData<RpcLog>> RpcLogPageAsync(QueryPageOptions option);
/// <summary>
/// 按天统计 RpcLog 数据
/// </summary>
/// <param name="day">统计的天数</param>
/// <returns>按天统计的结果列表</returns>
Task<List<RpcLogDayStatisticsOutput>> StatisticsByDayAsync(int day);
Task<List<RpcLogDayStatisticsOutput>> RpcLogStatisticsByDayAsync(int day);
}

View File

@@ -21,7 +21,7 @@ internal sealed class RpcLogService : BaseService<RpcLog>, IRpcLogService
/// <summary>
/// 最新十条
/// </summary>
public async Task<List<RpcLog>> GetNewLog()
public async Task<List<RpcLog>> GetNewRpcLog()
{
using var db = GetDB();
var data = await db.Queryable<RpcLog>().OrderByDescending(a => a.LogTime).Take(10).ToListAsync().ConfigureAwait(false);
@@ -32,7 +32,7 @@ internal sealed class RpcLogService : BaseService<RpcLog>, IRpcLogService
/// 表格查询
/// </summary>
/// <param name="option">查询条件</param>
public Task<QueryData<RpcLog>> PageAsync(QueryPageOptions option)
public Task<QueryData<RpcLog>> RpcLogPageAsync(QueryPageOptions option)
{
return QueryAsync(option);
}
@@ -51,7 +51,7 @@ internal sealed class RpcLogService : BaseService<RpcLog>, IRpcLogService
#endregion
public async Task<List<RpcLogDayStatisticsOutput>> StatisticsByDayAsync(int day)
public async Task<List<RpcLogDayStatisticsOutput>> RpcLogStatisticsByDayAsync(int day)
{
using var db = GetDB();
//取最近七天

View File

@@ -8,12 +8,11 @@
// QQ群605534569
// ------------------------------------------------------------------------------
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ThingsGateway.Gateway.Application;
public interface IGatewayMonitorHostedService : IHostedService
public interface IGatewayMonitorHostedService
{
public ILogger Logger { get; }
}

View File

@@ -0,0 +1,37 @@
// ------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
// ------------------------------------------------------------------------------
using ThingsGateway.Authentication;
namespace ThingsGateway.Gateway.Application;
internal sealed class AuthenticationService : IAuthenticationService
{
public Task<string> UUID() => Task.FromResult(ProAuthentication.UUID);
public Task<AuthorizeInfo> TryGetAuthorizeInfo()
{
ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo);
return Task.FromResult(authorizeInfo);
}
public Task<AuthorizeInfo> TryAuthorize(string password)
{
ProAuthentication.TryAuthorize(password, out var authorizeInfo);
return Task.FromResult(authorizeInfo);
}
public Task UnAuthorize()
{
ProAuthentication.UnAuthorize();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,21 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.Authentication;
namespace ThingsGateway.Gateway.Application;
public interface IAuthenticationService
{
Task<string> UUID();
Task<AuthorizeInfo> TryAuthorize(string password);
Task<AuthorizeInfo> TryGetAuthorizeInfo();
Task UnAuthorize();
}

View File

@@ -0,0 +1,26 @@
// ------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
// ------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
internal sealed class ChannelEnableService : IChannelEnableService
{
/// <summary>
/// 采集通道是否可用
/// </summary>
public Task<bool> StartCollectChannelEnable() => Task.FromResult(GlobalData.StartCollectChannelEnable);
/// <summary>
/// 业务通道是否可用
/// </summary>
public Task<bool> StartBusinessChannelEnable() => Task.FromResult(GlobalData.StartBusinessChannelEnable);
}

View File

@@ -0,0 +1,24 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
public interface IChannelEnableService
{
/// <summary>
/// 采集通道是否可用
/// </summary>
public Task<bool> StartCollectChannelEnable();
/// <summary>
/// 业务通道是否可用
/// </summary>
public Task<bool> StartBusinessChannelEnable();
}

View File

@@ -0,0 +1,157 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
using ThingsGateway.Authentication;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
namespace ThingsGateway.Gateway.Application;
#if Management
[GeneratorRpcProxy(GeneratorFlag = GeneratorFlag.ExtensionAsync)]
#endif
public interface IManagementRpcServer : IRpcServer
{
[DmtpRpc]
Task<Dictionary<string, Dictionary<string, OperResult<object>>>> Rpc(ICallContext callContext, Dictionary<string, Dictionary<string, string>> deviceDatas);
[DmtpRpc]
Task DeleteBackendLogAsync();
[DmtpRpc]
Task<List<BackendLog>> GetNewBackendLog();
[DmtpRpc]
Task<QueryData<BackendLog>> BackendLogPageAsync(QueryPageOptions option);
[DmtpRpc]
Task<List<BackendLogDayStatisticsOutput>> BackendLogStatisticsByDayAsync(int day);
/// <summary>
/// 删除 RpcLog 表中的所有记录
/// </summary>
/// <remarks>
/// 调用此方法会删除 RpcLog 表中的所有记录。
/// </remarks>
[DmtpRpc]
Task DeleteRpcLogAsync();
/// <summary>
/// 获取最新的十条 RpcLog 记录
/// </summary>
/// <returns>最新的十条记录</returns>
[DmtpRpc]
Task<List<RpcLog>> GetNewRpcLog();
/// <summary>
/// 分页查询 RpcLog 数据
/// </summary>
/// <param name="option">查询选项</param>
/// <returns>查询到的数据</returns>
[DmtpRpc]
Task<QueryData<RpcLog>> RpcLogPageAsync(QueryPageOptions option);
/// <summary>
/// 按天统计 RpcLog 数据
/// </summary>
/// <param name="day">统计的天数</param>
/// <returns>按天统计的结果列表</returns>
[DmtpRpc]
Task<List<RpcLogDayStatisticsOutput>> RpcLogStatisticsByDayAsync(int day);
[DmtpRpc]
Task RestartServer();
[DmtpRpc]
Task<string> UUID();
[DmtpRpc]
Task<AuthorizeInfo> TryAuthorize(string password);
[DmtpRpc]
Task<AuthorizeInfo> TryGetAuthorizeInfo();
[DmtpRpc]
Task UnAuthorize();
[DmtpRpc]
Task<bool> StartBusinessChannelEnable();
[DmtpRpc]
Task<bool> StartCollectChannelEnable();
[DmtpRpc]
Task StartRedundancyTaskAsync();
[DmtpRpc]
Task StopRedundancyTaskAsync();
[DmtpRpc]
Task RedundancyForcedSync();
[DmtpRpc]
public Task<TouchSocket.Core.LogLevel> RedundancyLogLevel();
[DmtpRpc]
public Task SetRedundancyLogLevel(TouchSocket.Core.LogLevel logLevel);
[DmtpRpc]
public Task<string> RedundancyLogPath();
/// <summary>
/// 修改冗余设置
/// </summary>
/// <param name="input"></param>
[DmtpRpc]
Task EditRedundancyOptionAsync(RedundancyOptions input);
/// <summary>
/// 获取冗余设置
/// </summary>
[DmtpRpc]
Task<RedundancyOptions> GetRedundancyAsync();
[DmtpRpc]
Task<OperResult<List<string>>> GetLogFiles(string directoryPath);
[DmtpRpc]
Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200);
/// <summary>
/// 根据插件类型获取信息
/// </summary>
/// <param name="pluginType"></param>
/// <returns></returns>
[DmtpRpc]
Task<List<PluginInfo>> GetPluginListAsync(PluginTypeEnum? pluginType = null);
/// <summary>
/// 分页显示插件
/// </summary>
[DmtpRpc]
public Task<QueryData<PluginInfo>> PluginPage(QueryPageOptions options, PluginTypeEnum? pluginTypeEnum = null);
/// <summary>
/// 重载插件
/// </summary>
[DmtpRpc]
Task ReloadPlugin();
/// <summary>
/// 添加插件
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
[DmtpRpc]
Task SavePluginByPath(PluginAddPathInput plugin);
[DmtpRpc]
Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariables();
}

View File

@@ -11,21 +11,21 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
internal sealed class RemoteManagementHostedService : BackgroundService
internal sealed class ManagementHostedService : BackgroundService
{
public RemoteManagementHostedService(ILoggerFactory loggerFactory)
public ManagementHostedService(ILoggerFactory loggerFactory)
{
var clientManagementOptions = App.GetOptions<RemoteClientManagementOptions>();
var serverManagementOptions = App.GetOptions<RemoteServerManagementOptions>();
RemoteClientManagementTask = new RemoteManagementTask(loggerFactory.CreateLogger(nameof(RemoteClientManagementTask)), clientManagementOptions);
RemoteServerManagementTask = new RemoteManagementTask(loggerFactory.CreateLogger(nameof(RemoteServerManagementTask)), serverManagementOptions);
RemoteClientManagementTask = new ManagementTask(loggerFactory.CreateLogger(nameof(RemoteClientManagementTask)), clientManagementOptions);
RemoteServerManagementTask = new ManagementTask(loggerFactory.CreateLogger(nameof(RemoteServerManagementTask)), serverManagementOptions);
}
private RemoteManagementTask RemoteClientManagementTask;
private RemoteManagementTask RemoteServerManagementTask;
private ManagementTask RemoteClientManagementTask;
private ManagementTask RemoteServerManagementTask;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{

View File

@@ -12,8 +12,8 @@ using System.ComponentModel.DataAnnotations;
using ThingsGateway.ConfigurableOptions;
namespace ThingsGateway.Management;
public class RemoteManagementOptions
namespace ThingsGateway.Gateway.Application;
public class ManagementOptions
{
public bool Enable { get; set; }
@@ -33,11 +33,11 @@ public class RemoteManagementOptions
}
public class RemoteClientManagementOptions : RemoteManagementOptions, IConfigurableOptions
public class RemoteClientManagementOptions : ManagementOptions, IConfigurableOptions
{
public override bool IsServer => false;
}
public class RemoteServerManagementOptions : RemoteManagementOptions, IConfigurableOptions
public class RemoteServerManagementOptions : ManagementOptions, IConfigurableOptions
{
public override bool IsServer => true;
}

View File

@@ -0,0 +1,102 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
using ThingsGateway.Authentication;
using TouchSocket.Core;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
using TouchSocket.Sockets;
namespace ThingsGateway.Gateway.Application;
public partial class ManagementRpcServer : IRpcServer, IManagementRpcServer, IBackendLogService, IRpcLogService, IRestartService, IAuthenticationService, IChannelEnableService, IRedundancyHostedService, IRedundancyService, ITextFileReadService, IPluginPageService, IRealAlarmService
{
[DmtpRpc]
public Task DeleteBackendLogAsync() => App.GetService<IBackendLogService>().DeleteBackendLogAsync();
[DmtpRpc]
public Task<List<BackendLogDayStatisticsOutput>> BackendLogStatisticsByDayAsync(int day) => App.GetService<IBackendLogService>().BackendLogStatisticsByDayAsync(day);
[DmtpRpc]
public Task<List<BackendLog>> GetNewBackendLog() => App.GetService<IBackendLogService>().GetNewBackendLog();
[DmtpRpc]
public Task<QueryData<BackendLog>> BackendLogPageAsync(QueryPageOptions option) => App.GetService<IBackendLogService>().BackendLogPageAsync(option);
[DmtpRpc]
public async Task<Dictionary<string, Dictionary<string, OperResult<object>>>> Rpc(ICallContext callContext, Dictionary<string, Dictionary<string, string>> deviceDatas)
{
var data = await GlobalData.RpcService.InvokeDeviceMethodAsync($"Management[{(callContext.Caller is ITcpSession tcpSession ? tcpSession.GetIPPort() : string.Empty)}]", deviceDatas, callContext.Token).ConfigureAwait(false);
return data.ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => b.Value.GetOperResult()));
}
public Task DeleteRpcLogAsync() => App.GetService<IRpcLogService>().DeleteRpcLogAsync();
public Task<List<RpcLog>> GetNewRpcLog() => App.GetService<IRpcLogService>().GetNewRpcLog();
public Task<QueryData<RpcLog>> RpcLogPageAsync(QueryPageOptions option) => App.GetService<IRpcLogService>().RpcLogPageAsync(option);
public Task<List<RpcLogDayStatisticsOutput>> RpcLogStatisticsByDayAsync(int day) => App.GetService<IRpcLogService>().RpcLogStatisticsByDayAsync(day);
public Task RestartServer() => App.GetService<IRestartService>().RestartServer();
public Task<string> UUID() => App.GetService<IAuthenticationService>().UUID();
public Task<AuthorizeInfo> TryAuthorize(string password) => App.GetService<IAuthenticationService>().TryAuthorize(password);
public Task<AuthorizeInfo> TryGetAuthorizeInfo() => App.GetService<IAuthenticationService>().TryGetAuthorizeInfo();
public Task UnAuthorize() => App.GetService<IAuthenticationService>().UnAuthorize();
public Task<bool> StartBusinessChannelEnable() => App.GetService<IChannelEnableService>().StartBusinessChannelEnable();
public Task<bool> StartCollectChannelEnable() => App.GetService<IChannelEnableService>().StartCollectChannelEnable();
public Task StartRedundancyTaskAsync() => App.GetService<IRedundancyHostedService>().StartRedundancyTaskAsync();
public Task StopRedundancyTaskAsync() => App.GetService<IRedundancyHostedService>().StopRedundancyTaskAsync();
public Task RedundancyForcedSync() => App.GetService<IRedundancyHostedService>().RedundancyForcedSync();
public Task EditRedundancyOptionAsync(RedundancyOptions input) => App.GetService<IRedundancyService>().EditRedundancyOptionAsync(input);
public Task<RedundancyOptions> GetRedundancyAsync() => App.GetService<IRedundancyService>().GetRedundancyAsync();
public Task<LogLevel> RedundancyLogLevel() => App.GetService<IRedundancyHostedService>().RedundancyLogLevel();
public Task SetRedundancyLogLevel(LogLevel logLevel) => App.GetService<IRedundancyHostedService>().SetRedundancyLogLevel(logLevel);
public Task<string> RedundancyLogPath() => App.GetService<IRedundancyHostedService>().RedundancyLogPath();
public Task<OperResult<List<string>>> GetLogFiles(string directoryPath) => App.GetService<ITextFileReadService>().GetLogFiles(directoryPath);
public Task<OperResult<List<LogData>>> LastLogData(string file, int lineCount = 200) => App.GetService<ITextFileReadService>().LastLogData(file, lineCount);
public Task<List<PluginInfo>> GetPluginListAsync(PluginTypeEnum? pluginType = null) => App.GetService<IPluginPageService>().GetPluginListAsync(pluginType);
public Task<QueryData<PluginInfo>> PluginPage(QueryPageOptions options, PluginTypeEnum? pluginTypeEnum = null) => App.GetService<IPluginPageService>().PluginPage(options, pluginTypeEnum);
public Task ReloadPlugin() => App.GetService<IPluginPageService>().ReloadPlugin();
public Task SavePluginByPath(PluginAddPathInput plugin) => App.GetService<IPluginPageService>().SavePluginByPath(plugin);
public Task<IEnumerable<AlarmVariable>> GetCurrentUserRealAlarmVariables() => App.GetService<IRealAlarmService>().GetCurrentUserRealAlarmVariables();
}

View File

@@ -8,26 +8,26 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ThingsGateway.Gateway.Application;
using TouchSocket.Core;
using TouchSocket.Dmtp;
using TouchSocket.Dmtp.FileTransfer;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
using TouchSocket.Sockets;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
public partial class RemoteManagementTask : AsyncDisposableObject
public partial class ManagementTask : AsyncDisposableObject
{
internal const string LogPath = $"Logs/{nameof(RemoteManagementTask)}";
internal const string LogPath = $"Logs/{nameof(ManagementTask)}";
private ILog LogMessage;
private ILogger _logger;
private TextFileLogger TextLogger;
public RemoteManagementTask(ILogger logger, RemoteManagementOptions remoteManagementOptions)
public ManagementTask(ILogger logger, ManagementOptions managementOptions)
{
_logger = logger;
TextLogger = TextFileLogger.GetMultipleFileLogger(LogPath);
@@ -37,7 +37,7 @@ public partial class RemoteManagementTask : AsyncDisposableObject
log?.AddLogger(TextLogger);
LogMessage = log;
_remoteManagementOptions = remoteManagementOptions;
_managementOptions = managementOptions;
}
@@ -48,9 +48,9 @@ public partial class RemoteManagementTask : AsyncDisposableObject
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_remoteManagementOptions.Enable) return;
if (!_managementOptions.Enable) return;
if (_remoteManagementOptions.IsServer)
if (_managementOptions.IsServer)
{
_tcpDmtpService ??= await GetTcpDmtpService().ConfigureAwait(false);
}
@@ -78,33 +78,52 @@ public partial class RemoteManagementTask : AsyncDisposableObject
private TcpDmtpClient? _tcpDmtpClient;
private TcpDmtpService? _tcpDmtpService;
private RemoteManagementOptions _remoteManagementOptions;
private ManagementOptions _managementOptions;
private async Task<TcpDmtpClient> GetTcpDmtpClient()
{
var tcpDmtpClient = new TcpDmtpClient();
var config = new TouchSocketConfig()
.SetRemoteIPHost(_remoteManagementOptions.ServerUri)
.SetRemoteIPHost(_managementOptions.ServerUri)
.SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
.SetDmtpOption(new DmtpOption() { VerifyToken = _remoteManagementOptions.VerifyToken })
.SetDmtpOption(new DmtpOption() { VerifyToken = _managementOptions.VerifyToken })
.ConfigureContainer(a =>
{
a.AddDmtpRouteService();//添加路由策略
a.AddLogger(LogMessage);
a.AddRpcStore(store => store.RegisterServer<RemoteManagementRpcServer>());
a.AddRpcStore(store =>
{
store.RegisterServer<IManagementRpcServer>(new ManagementRpcServer());
store.RegisterServer<IUpgradeRpcServer>(new UpgradeRpcServer());
});
})
.ConfigurePlugins(a =>
{
a.UseTcpSessionCheckClear();
a.UseDmtpRpc();
a.UseDmtpRpc().ConfigureDefaultSerializationSelector(b =>
{
b.UseSystemTextJson(json =>
{
});
});
a.UseDmtpFileTransfer();//必须添加文件传输插件
a.Add<FilePlugin>();
a.UseDmtpHeartbeat()//使用Dmtp心跳
.SetTick(TimeSpan.FromMilliseconds(_remoteManagementOptions.HeartbeatInterval))
.SetTick(TimeSpan.FromMilliseconds(_managementOptions.HeartbeatInterval))
.SetMaxFailCount(3);
a.AddDmtpHandshakedPlugin(async () =>
{
try
{
await tcpDmtpClient.ResetIdAsync($"{_remoteManagementOptions.Name}:{GlobalData.HardwareJob.HardwareInfo.UUID}").ConfigureAwait(false);
await tcpDmtpClient.ResetIdAsync($"{_managementOptions.Name}:{GlobalData.HardwareJob.HardwareInfo.UUID}").ConfigureAwait(false);
}
catch (Exception)
{
@@ -121,20 +140,37 @@ public partial class RemoteManagementTask : AsyncDisposableObject
{
var tcpDmtpService = new TcpDmtpService();
var config = new TouchSocketConfig()
.SetListenIPHosts(_remoteManagementOptions.ServerUri)
.SetListenIPHosts(_managementOptions.ServerUri)
.SetAdapterOption(new AdapterOption() { MaxPackageSize = 1024 * 1024 * 1024 })
.SetDmtpOption(new DmtpOption() { VerifyToken = _remoteManagementOptions.VerifyToken })
.SetDmtpOption(new DmtpOption() { VerifyToken = _managementOptions.VerifyToken })
.ConfigureContainer(a =>
{
a.AddDmtpRouteService();//添加路由策略
a.AddLogger(LogMessage);
a.AddRpcStore(store => store.RegisterServer<RemoteManagementRpcServer>());
a.AddRpcStore(store =>
{
store.RegisterServer<IManagementRpcServer>(new ManagementRpcServer());
store.RegisterServer<IUpgradeRpcServer>(new UpgradeRpcServer());
});
})
.ConfigurePlugins(a =>
{
a.UseTcpSessionCheckClear();
a.UseDmtpRpc();
a.UseDmtpRpc().ConfigureDefaultSerializationSelector(b =>
{
b.UseSystemTextJson(json =>
{
});
});
a.UseDmtpFileTransfer();//必须添加文件传输插件
a.Add<FilePlugin>();
a.UseDmtpHeartbeat()//使用Dmtp心跳
.SetTick(TimeSpan.FromMilliseconds(_remoteManagementOptions.HeartbeatInterval))
.SetTick(TimeSpan.FromMilliseconds(_managementOptions.HeartbeatInterval))
.SetMaxFailCount(3);
});
@@ -144,7 +180,7 @@ public partial class RemoteManagementTask : AsyncDisposableObject
private async Task EnsureChannelOpenAsync(CancellationToken cancellationToken)
{
if (_remoteManagementOptions.IsServer)
if (_managementOptions.IsServer)
{
if (_tcpDmtpService.ServerState != ServerState.Running)
{
@@ -176,6 +212,18 @@ public partial class RemoteManagementTask : AsyncDisposableObject
_tcpDmtpService.SafeDispose();
_tcpDmtpService = null;
}
TextLogger?.Dispose();
await base.DisposeAsync(disposing).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace ThingsGateway.Gateway.Application;
public static class FileConst
{
public const string FilePathKey = "FilePath";
public static string UpgradePath = Path.Combine(AppContext.BaseDirectory, "Upgrade.zip");
public static string UpgradeBackupPath = Path.Combine(AppContext.BaseDirectory, "..", "Backup.zip");
public static string UpgradeBackupDirPath = Path.Combine(AppContext.BaseDirectory, "..", "Backup");
public const string UpdateZipFileServerDir = "UpdateZipFile";
}

View File

@@ -2,7 +2,7 @@
using TouchSocket.Dmtp;
using TouchSocket.Dmtp.FileTransfer;
namespace ThingsGateway.Upgrade;
namespace ThingsGateway.Gateway.Application;
internal sealed class FilePlugin : PluginBase, IDmtpFileTransferringPlugin, IDmtpFileTransferredPlugin, IDmtpRoutingPlugin
{
@@ -67,7 +67,7 @@ internal sealed class FilePlugin : PluginBase, IDmtpFileTransferringPlugin, IDmt
public async Task OnDmtpRouting(IDmtpActorObject client, PackageRouterEventArgs e)
{
e.IsPermitOperation = true;//允许路由
m_logger.Info($"路由类型:{e.RouterType}");
m_logger.Debug($"路由类型:{e.RouterType}");
await e.InvokeNext().ConfigureAwait(false);
}
}

View File

@@ -2,7 +2,7 @@
using System.Runtime.InteropServices;
namespace ThingsGateway.Upgrade;
namespace ThingsGateway.Gateway.Application;
public class UpdateZipFile
{

View File

@@ -0,0 +1,108 @@
using TouchSocket.Core;
using TouchSocket.Dmtp;
using TouchSocket.Dmtp.FileTransfer;
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
public static class FileServerHelpers
{
/// <summary>
/// 传输限速
/// </summary>
public const long MaxSpeed = 1024 * 1024 * 10L;
/// <summary>
/// 客户端从服务器下载文件。
/// </summary>
public static async Task<bool> ClientPullFileFromService(IDmtpActor client, string path, string savePath)
{
Directory.CreateDirectory(savePath.AsFile().DirectoryName);
var metadata = new Metadata();//传递到服务器的元数据
metadata.Add(FileConst.FilePathKey, path);
var fileOperator = new FileOperator//实例化本次传输的控制器,用于获取传输进度、速度、状态等。
{
SavePath = savePath,//客户端本地保存路径
ResourcePath = path,//请求文件的资源路径
Metadata = metadata,//传递到服务器的元数据
Timeout = TimeSpan.FromSeconds(60),//传输超时时长
TryCount = 10,//当遇到失败时,尝试次数
FileSectionSize = 1024 * 512//分包大小,当网络较差时,应该适当减小该值
};
fileOperator.MaxSpeed = MaxSpeed;//设置最大限速。
//此处的作用相当于Timer定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
if (fileOperator.IsEnd)
{
loop.Dispose();
}
client.Logger.Info($"请求文件:{fileOperator.ResourcePath},进度:{(fileOperator.Progress * 100).ToString("F2")}%,速度:{(fileOperator.Speed() / 1024).ToString("F2")} KB/s");
});
_ = loopAction.RunAsync();
//此方法会阻塞直到传输结束也可以使用PullFileAsync
var result = await client.GetDmtpFileTransferActor().PullFileAsync(fileOperator).ConfigureAwait(false);
if (result.IsSuccess)
client.Logger.Info(result.ToString());
else
client.Logger.Warning(result.ToString());
return result.IsSuccess;
}
/// <summary>
/// 客户端上传文件到服务器。
/// </summary>
public static async Task ClientPushFileFromService(IDmtpActor client, string resourcePath, string serverPath)
{
var metadata = new Metadata();//传递到服务器的元数据
metadata.Add(FileConst.FilePathKey, serverPath);
var fileOperator = new FileOperator//实例化本次传输的控制器,用于获取传输进度、速度、状态等。
{
SavePath = serverPath,//服务器本地保存路径
ResourcePath = resourcePath,//客户端本地即将上传文件的资源路径
Metadata = metadata,//传递到服务器的元数据
Timeout = TimeSpan.FromSeconds(60),//传输超时时长
TryCount = 10,//当遇到失败时,尝试次数
FileSectionSize = 1024 * 512//分包大小,当网络较差时,应该适当减小该值
};
fileOperator.MaxSpeed = MaxSpeed;//设置最大限速。
//此处的作用相当于Timer定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
if (fileOperator.IsEnd)
{
loop.Dispose();
}
client.Logger.Info($"进度:{(fileOperator.Progress * 100).ToString("F2")}%,速度:{(fileOperator.Speed() / 1024).ToString("F2")} KB/s");
});
_ = loopAction.RunAsync();
//此方法会阻塞直到传输结束也可以使用PushFileAsync
var result = await client.GetDmtpFileTransferActor().PushFileAsync(fileOperator).ConfigureAwait(false);
client.Logger.Info(result.ToString());
}
}

View File

@@ -12,13 +12,13 @@
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
#if Management
[GeneratorRpcProxy(GeneratorFlag = GeneratorFlag.ExtensionAsync)]
#endif
public interface IUpgradeRpcServer : IRpcServer
{
[DmtpRpc]
void Restart();
[DmtpRpc]
Task Upgrade();
Task Upgrade(ICallContext callContext, List<UpdateZipFile> updateZipFiles);
}

View File

@@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.NewLife;
using TouchSocket.Dmtp;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
namespace ThingsGateway.Gateway.Application;
public partial class UpgradeRpcServer : IRpcServer, IUpgradeRpcServer
{
[DmtpRpc]
public async Task Upgrade(ICallContext callContext, List<UpdateZipFile> updateZipFiles)
{
if (updateZipFiles?.Count > 0 && callContext.Caller is IDmtpActorObject dmtpActorObject)
await Update(dmtpActorObject.DmtpActor, updateZipFiles.OrderByDescending(a => a.Version).FirstOrDefault()).ConfigureAwait(false);
}
public static async Task Update(IDmtpActor dmtpActor, UpdateZipFile updateZipFile, Func<Task<bool>> check = null)
{
try
{
await UpdateWaitLock.WaitAsync().ConfigureAwait(false);
if (WaitLock.Waited)
{
throw new("Updating, please try again later");
}
try
{
await WaitLock.WaitAsync().ConfigureAwait(false);
RestartServerHelper.DeleteAndBackup();
var result = await FileServerHelpers.ClientPullFileFromService(dmtpActor, updateZipFile.FilePath, FileConst.UpgradePath).ConfigureAwait(false);
if (result)
{
if (check != null)
result = await check.Invoke().ConfigureAwait(false);
if (result)
{
RestartServerHelper.ExtractUpdate();
}
}
}
finally
{
WaitLock.Release();
}
}
finally
{
UpdateWaitLock.Release();
}
}
private static readonly WaitLock WaitLock = new(nameof(ManagementTask));
private static readonly WaitLock UpdateWaitLock = new(nameof(ManagementTask));
}

View File

@@ -15,10 +15,10 @@ using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using ThingsGateway.Gateway.Application;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.NewLife.Threading;
using ThingsGateway.Upgrade;
namespace ThingsGateway;
@@ -29,8 +29,8 @@ public static class RestartServerHelper
//删除不必要的文件
DeleteDelEx();
//删除备份
Delete(FileConst.BackupPath);
Delete(FileConst.BackupDirPath);
Delete(FileConst.UpgradeBackupPath);
Delete(FileConst.UpgradeBackupDirPath);
Delete(FileConst.UpgradePath);
//备份原数据
@@ -234,8 +234,8 @@ public static class RestartServerHelper
{
//备份原数据
var backupDir = new DirectoryInfo(AppContext.BaseDirectory);
backupDir.CopyTo(FileConst.BackupDirPath, allSub: true);
FileConst.BackupDirPath.AsDirectory().Compress(FileConst.BackupPath);
backupDir.CopyTo(FileConst.UpgradeBackupDirPath, allSub: true);
FileConst.UpgradeBackupDirPath.AsDirectory().Compress(FileConst.UpgradeBackupPath);
}
catch
{

View File

@@ -8,9 +8,7 @@
// QQ群605534569
// ------------------------------------------------------------------------------
using ThingsGateway.Gateway.Application;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
public class GatewayRedundantSerivce : IGatewayRedundantSerivce
{

View File

@@ -8,16 +8,15 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Microsoft.Extensions.Hosting;
namespace ThingsGateway.Gateway.Application;
namespace ThingsGateway.Management;
public interface IRedundancyHostedService : IHostedService
public interface IRedundancyHostedService
{
Task StartTaskAsync(CancellationToken cancellationToken);
Task StopTaskAsync();
Task ForcedSync(CancellationToken cancellationToken = default);
Task StartRedundancyTaskAsync();
Task StopRedundancyTaskAsync();
Task RedundancyForcedSync();
public TextFileLogger TextLogger { get; }
public string LogPath { get; }
public Task<TouchSocket.Core.LogLevel> RedundancyLogLevel();
public Task SetRedundancyLogLevel(TouchSocket.Core.LogLevel logLevel);
public Task<string> RedundancyLogPath();
}

View File

@@ -8,7 +8,7 @@
// QQ群605534569
//------------------------------------------------------------------------------
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
public interface IRedundancyService
{

View File

@@ -8,12 +8,10 @@
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.Gateway.Application;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
[GeneratorRpcProxy(GeneratorFlag = GeneratorFlag.ExtensionAsync)]
internal interface IRedundantRpcServer : IRpcServer
@@ -26,13 +24,3 @@ internal interface IRedundantRpcServer : IRpcServer
[DmtpRpc]
void UpData(ICallContext callContext, List<DeviceDataWithValue> deviceDatas);
}
//internal static partial class RedundantRpcServerExtensions
//{
// public static Task<Dictionary<string, Dictionary<string, OperResult<object>>>> RpcAsync<TClient>(this TClient client, System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<string, string>> deviceDatas, IInvokeOption invokeOption = default)
// where TClient : TouchSocket.Rpc.IRpcClient, TouchSocket.Dmtp.Rpc.IDmtpRpcActor
// {
// return client.InvokeTAsync<Dictionary<string, Dictionary<string, OperResult<object>>>>($"{nameof(ThingsGateway.Management)}{nameof(ThingsGateway.Management)}{nameof(ThingsGateway.Management.IRedundantRpcServer)}{nameof(ThingsGateway.Management.IRedundantRpcServer.Rpc)}", invokeOption, deviceDatas);
// }
//}

View File

@@ -10,9 +10,7 @@
using Newtonsoft.Json;
using ThingsGateway.Gateway.Application;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
public class DeviceDataWithValue
{

View File

@@ -10,9 +10,7 @@
using Newtonsoft.Json;
using ThingsGateway.Gateway.Application;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
public class VariableDataWithValue
{

View File

@@ -11,7 +11,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHostedService
{
@@ -24,23 +24,37 @@ internal sealed class RedundancyHostedService : BackgroundService, IRedundancyHo
}
private RedundancyTask RedundancyTask;
public TextFileLogger TextLogger => RedundancyTask.TextLogger;
public string LogPath => RedundancyTask.LogPath;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
await RedundancyTask.StartTaskAsync(stoppingToken).ConfigureAwait(false);
await RedundancyTask.StartRedundancyTaskAsync(stoppingToken).ConfigureAwait(false);
}
public Task StartTaskAsync(CancellationToken cancellationToken) => RedundancyTask.StartTaskAsync(cancellationToken);
public Task StopTaskAsync() => RedundancyTask.StopTaskAsync();
public Task ForcedSync(CancellationToken cancellationToken = default) => RedundancyTask.ForcedSync(cancellationToken);
public Task StartRedundancyTaskAsync() => RedundancyTask.StartRedundancyTaskAsync();
public Task StopRedundancyTaskAsync() => RedundancyTask.StopRedundancyTaskAsync();
public Task RedundancyForcedSync() => RedundancyTask.RedundancyForcedSync();
public override async Task StopAsync(CancellationToken cancellationToken)
{
await RedundancyTask.DisposeAsync().ConfigureAwait(false);
await base.StopAsync(cancellationToken).ConfigureAwait(false);
}
public Task<TouchSocket.Core.LogLevel> RedundancyLogLevel()
{
return Task.FromResult(RedundancyTask.TextLogger.LogLevel);
}
public Task SetRedundancyLogLevel(TouchSocket.Core.LogLevel logLevel)
{
RedundancyTask.TextLogger.LogLevel = logLevel;
return Task.CompletedTask;
}
public Task<string> RedundancyLogPath()
{
return Task.FromResult(RedundancyTask.LogPath);
}
}

View File

@@ -10,7 +10,7 @@
using System.ComponentModel.DataAnnotations;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 冗余配置

View File

@@ -9,7 +9,7 @@
//------------------------------------------------------------------------------
using ThingsGateway.NewLife.Extension;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
internal sealed class RedundancyService : BaseService<SysDict>, IRedundancyService
{

View File

@@ -14,7 +14,6 @@ using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using ThingsGateway.Extension.Generic;
using ThingsGateway.Gateway.Application;
using ThingsGateway.NewLife;
using TouchSocket.Core;
@@ -24,7 +23,7 @@ using TouchSocket.Rpc;
using TouchSocket.Rpc.Generators;
using TouchSocket.Sockets;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
{
@@ -238,9 +237,9 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
return GlobalData.ChannelRuntimeService.RestartChannelAsync(GlobalData.ReadOnlyIdChannels.Values);
}
public async Task StartTaskAsync(CancellationToken cancellationToken)
public async Task StartRedundancyTaskAsync(CancellationToken cancellationToken = default)
{
await StopTaskAsync().ConfigureAwait(false);
await StopRedundancyTaskAsync().ConfigureAwait(false);
RedundancyOptions = (await _redundancyService.GetRedundancyAsync().ConfigureAwait(false)).AdaptRedundancyOptions();
if (RedundancyOptions?.Enable == true)
@@ -278,7 +277,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
scheduledTask.Start();
}
}
public async Task StopTaskAsync()
public async Task StopRedundancyTaskAsync()
{
if (scheduledTask?.Enable == true)
{
@@ -310,7 +309,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
public async ValueTask DisposeAsync()
{
await StopTaskAsync().ConfigureAwait(false);
await StopRedundancyTaskAsync().ConfigureAwait(false);
TextLogger?.TryDispose();
scheduledTask?.SafeDispose();
@@ -336,13 +335,22 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
.ConfigureContainer(a =>
{
a.AddLogger(LogMessage);
a.AddRpcStore(store => store.RegisterServer(typeof(IRedundantRpcServer), new RedundantRpcServer(this)));
a.AddRpcStore(store =>
{
store.RegisterServer<IRedundantRpcServer>(new RedundantRpcServer(this));
});
})
.ConfigurePlugins(a =>
{
a.UseTcpSessionCheckClear();
a.UseDmtpRpc();
a.UseDmtpRpc().ConfigureDefaultSerializationSelector(b =>
{
b.UseSystemTextJson(json =>
{
});
});
a.UseDmtpHeartbeat()//使用Dmtp心跳
.SetTick(TimeSpan.FromMilliseconds(redundancy.HeartbeatInterval))
.SetMaxFailCount(redundancy.MaxErrorCount);
@@ -367,12 +375,21 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
.ConfigureContainer(a =>
{
a.AddLogger(LogMessage);
a.AddRpcStore(store => store.RegisterServer(typeof(IRedundantRpcServer), new RedundantRpcServer(this)));
a.AddRpcStore(store =>
{
store.RegisterServer<IRedundantRpcServer>(new RedundantRpcServer(this));
});
})
.ConfigurePlugins(a =>
{
a.UseTcpSessionCheckClear();
a.UseDmtpRpc();
a.UseDmtpRpc().ConfigureDefaultSerializationSelector(b =>
{
b.UseSystemTextJson(json =>
{
});
});
a.UseDmtpHeartbeat()//使用Dmtp心跳
.SetTick(TimeSpan.FromMilliseconds(redundancy.HeartbeatInterval))
.SetMaxFailCount(redundancy.MaxErrorCount);
@@ -396,10 +413,10 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
#endregion
#region ForcedSync
#region RedundancyForcedSync
WaitLock ForcedSyncWaitLock = new WaitLock(nameof(RedundancyTask));
public async Task ForcedSync(CancellationToken cancellationToken = default)
public async Task RedundancyForcedSync(CancellationToken cancellationToken = default)
{
await ForcedSyncWaitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -420,7 +437,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
if (!online)
{
LogMessage?.LogWarning("ForcedSync data error, no client online");
LogMessage?.LogWarning("RedundancyForcedSync data error, no client online");
return;
}
@@ -439,7 +456,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
catch (OperationCanceledException) { }
catch (Exception ex)
{
LogMessage?.LogWarning(ex, "ForcedSync data error");
LogMessage?.LogWarning(ex, "RedundancyForcedSync data error");
}
}
finally
@@ -488,7 +505,7 @@ internal sealed class RedundancyTask : IRpcDriver, IAsyncDisposable
await client.GetDmtpRpcActor().SyncDataAsync(channelBatch.ToList(), deviceBatch.ToList(), variableBatch, invokeOption).ConfigureAwait(false);
}
LogMessage?.LogTrace($"ForcedSync data success");
LogMessage?.LogTrace($"RedundancyForcedSync data success");
}
#endregion

View File

@@ -8,14 +8,12 @@
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.Gateway.Application;
using TouchSocket.Core;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
using TouchSocket.Sockets;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
internal sealed partial class RedundantRpcServer : SingletonRpcServer, IRedundantRpcServer
{

View File

@@ -1,45 +0,0 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
using ThingsGateway.Gateway.Application;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
using TouchSocket.Sockets;
namespace ThingsGateway.Management;
public partial class RemoteManagementRpcServer : SingletonRpcServer,
IBackendLogService
{
[DmtpRpc]
public Task DeleteBackendLogAsync() => App.GetService<IBackendLogService>().DeleteBackendLogAsync();
[DmtpRpc]
public Task<List<BackendLogDayStatisticsOutput>> StatisticsByDayAsync(int day) => App.GetService<IBackendLogService>().StatisticsByDayAsync(day);
[DmtpRpc]
public Task<List<BackendLog>> GetNewLog() => App.GetService<IBackendLogService>().GetNewLog();
[DmtpRpc]
public Task<QueryData<BackendLog>> PageAsync(QueryPageOptions option) => App.GetService<IBackendLogService>().PageAsync(option);
[DmtpRpc]
public async Task<Dictionary<string, Dictionary<string, OperResult<object>>>> Rpc(ICallContext callContext, Dictionary<string, Dictionary<string, string>> deviceDatas, CancellationToken cancellationToken)
{
var data = await GlobalData.RpcService.InvokeDeviceMethodAsync($"RemoteManagement[{(callContext.Caller is ITcpSession tcpSession ? tcpSession.GetIPPort() : string.Empty)}]", deviceDatas, cancellationToken).ConfigureAwait(false);
return data.ToDictionary(a => a.Key, a => a.Value.ToDictionary(b => b.Key, b => b.Value.GetOperResult()));
}
}

View File

@@ -8,11 +8,9 @@
// QQ群605534569
//------------------------------------------------------------------------------
global using BootstrapBlazor.Components;
namespace ThingsGateway.Gateway.Application;
global using Microsoft.AspNetCore.Components;
global using Microsoft.Extensions.Localization;
global using System.Diagnostics.CodeAnalysis;
global using ThingsGateway.Razor;
public interface IRestartService
{
Task RestartServer();
}

View File

@@ -0,0 +1,23 @@
// ------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
// ------------------------------------------------------------------------------
namespace ThingsGateway.Gateway.Application;
internal sealed class RestartService : IRestartService
{
public Task RestartServer()
{
RestartServerHelper.RestartServer();
return Task.CompletedTask;
}
}

View File

@@ -1,17 +0,0 @@
using Microsoft.Extensions.Hosting;
using ThingsGateway.Upgrade;
using TouchSocket.Dmtp;
namespace ThingsGateway.Management;
public interface IUpdateZipFileHostedService : IHostedService
{
TextFileLogger TextLogger { get; }
string LogPath { get; }
TcpDmtpClient? TcpDmtpClient { get; set; }
Task<List<UpdateZipFile>> GetList();
Task Update(UpdateZipFile updateZipFile, Func<Task<bool>> check = null);
}

View File

@@ -1,331 +0,0 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Reflection;
using System.Runtime.InteropServices;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Log;
using ThingsGateway.Upgrade;
using TouchSocket.Core;
using TouchSocket.Dmtp;
using TouchSocket.Dmtp.FileTransfer;
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
using TouchSocket.Rpc.Generators;
using TouchSocket.Sockets;
namespace ThingsGateway.Management;
internal sealed class UpdateZipFileHostedService : BackgroundService, IUpdateZipFileHostedService
{
public UpdateZipFileHostedService(ILogger<UpdateZipFileHostedService> logger, INoticeService noticeService, IVerificatInfoService verificatInfoService)
{
NoticeService = noticeService;
VerificatInfoService = verificatInfoService;
_logger = logger;
Localizer = App.CreateLocalizerByType(typeof(UpdateZipFileHostedService));
// 创建新的文件日志记录器,并设置日志级别为 Trace
LogPath = "Logs/UpgradeLog";
TextLogger = TextFileLogger.GetMultipleFileLogger(LogPath);
TextLogger.LogLevel = TouchSocket.Core.LogLevel.Trace;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
TcpDmtpClient = await GetTcpDmtpClient().ConfigureAwait(false);
var upgradeServerOptions = App.GetOptions<UpgradeServerOptions>();
bool success = false;
bool first = true;
while (!success)
{
try
{
if (upgradeServerOptions.Enable)
{
await TcpDmtpClient.ConnectAsync().ConfigureAwait(false);
await TcpDmtpClient.ResetIdAsync(upgradeServerOptions.Id).ConfigureAwait(false);
success = true;
}
}
catch (Exception ex)
{
if (first)
XTrace.WriteException(ex);
first = false;
}
finally
{
await Task.Delay(3000, stoppingToken).ConfigureAwait(false);
}
}
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (upgradeServerOptions.Enable && !App.HostEnvironment.IsDevelopment())
{
var data = await GetList().ConfigureAwait(false);
if (data.Count != 0 && (LastVersion == null || data.FirstOrDefault().Version > LastVersion))
{
LastVersion = data.FirstOrDefault().Version;
var verificatInfoIds = VerificatInfoService.GetListByUserId(RoleConst.SuperAdminId);
await NoticeService.NavigationMesage(verificatInfoIds.Where(a => a.Online).SelectMany(a => a.ClientIds), "/gateway/system?tab=1", App.CreateLocalizerByType(typeof(UpdateZipFileHostedService))["Update"]).ConfigureAwait(false);
}
}
error = false;
await Task.Delay(60000, stoppingToken).ConfigureAwait(false);
}
catch (TaskCanceledException) { }
catch (OperationCanceledException) { }
catch (Exception ex)
{
if (!error)
TextLogger.LogWarning(ex);
error = true;
}
}
}
private INoticeService NoticeService { get; set; }
private IVerificatInfoService VerificatInfoService { get; set; }
private Version LastVersion;
private bool error;
private ILogger<UpdateZipFileHostedService> _logger;
private IStringLocalizer Localizer;
private LoggerGroup _log { get; set; }
public TextFileLogger TextLogger { get; }
public string LogPath { get; }
/// <summary>
/// 传输限速
/// </summary>
public const long MaxSpeed = 1024 * 1024 * 10L;
public TcpDmtpClient? TcpDmtpClient { get; set; }
public async Task<List<UpdateZipFile>> GetList()
{
var upgradeServerOptions = App.GetOptions<UpgradeServerOptions>();
if (!upgradeServerOptions.Enable)
throw new Exception("Update service not enabled");
//设置调用配置
var tokenSource = new CancellationTokenSource();//可取消令箭源可用于取消Rpc的调用
var invokeOption = new DmtpInvokeOption()//调用配置
{
FeedbackType = FeedbackType.WaitInvoke,//调用反馈类型
SerializationType = SerializationType.Json,//序列化类型
Timeout = 5000,//调用超时设置
Token = tokenSource.Token//配置可取消令箭
};
var updateZipFiles = await TcpDmtpClient.GetDmtpRpcActor().GetListAsync(new UpdateZipFileInput()
{
Version = Assembly.GetEntryAssembly().GetName().Version,
DotNetVersion = Environment.Version,
OSPlatform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows" :
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "Linux" :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "OSX" : "Unknown",
Architecture = RuntimeInformation.ProcessArchitecture,
AppName = "ThingsGateway"
}, invokeOption).ConfigureAwait(false);
return updateZipFiles.OrderByDescending(a => a.Version).ToList();
}
private readonly WaitLock WaitLock = new(nameof(UpdateZipFileHostedService));
private readonly WaitLock UpdateWaitLock = new(nameof(UpdateZipFileHostedService));
public async Task Update(UpdateZipFile updateZipFile, Func<Task<bool>> check = null)
{
try
{
await UpdateWaitLock.WaitAsync().ConfigureAwait(false);
var upgradeServerOptions = App.GetOptions<UpgradeServerOptions>();
if (!upgradeServerOptions.Enable)
return;
if (WaitLock.Waited)
{
_log.LogWarning("Updating, please try again later");
return;
}
try
{
await WaitLock.WaitAsync().ConfigureAwait(false);
RestartServerHelper.DeleteAndBackup();
var result = await ClientPullFileFromService(TcpDmtpClient, updateZipFile.FilePath).ConfigureAwait(false);
if (result)
{
if (check != null)
result = await check.Invoke().ConfigureAwait(false);
if (result)
{
RestartServerHelper.ExtractUpdate();
}
}
}
finally
{
WaitLock.Release();
}
}
catch (Exception ex)
{
_log.LogWarning(ex);
}
finally
{
UpdateWaitLock.Release();
}
}
/// <summary>
/// 客户端从服务器下载文件。
/// </summary>
private static async Task<bool> ClientPullFileFromService(TcpDmtpClient client, string path)
{
var metadata = new Metadata();//传递到服务器的元数据
metadata.Add(FileConst.FilePathKey, path);
var fileOperator = new FileOperator//实例化本次传输的控制器,用于获取传输进度、速度、状态等。
{
SavePath = FileConst.UpgradePath,//客户端本地保存路径
ResourcePath = path,//请求文件的资源路径
Metadata = metadata,//传递到服务器的元数据
Timeout = TimeSpan.FromSeconds(60),//传输超时时长
TryCount = 10,//当遇到失败时,尝试次数
FileSectionSize = 1024 * 512//分包大小,当网络较差时,应该适当减小该值
};
fileOperator.MaxSpeed = MaxSpeed;//设置最大限速。
//此处的作用相当于Timer定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
if (fileOperator.IsEnd)
{
loop.Dispose();
}
client.Logger.Info($"请求文件:{fileOperator.ResourcePath},进度:{(fileOperator.Progress * 100).ToString("F2")}%,速度:{(fileOperator.Speed() / 1024).ToString("F2")} KB/s");
});
_ = loopAction.RunAsync();
//此方法会阻塞直到传输结束也可以使用PullFileAsync
var result = await client.GetDmtpFileTransferActor().PullFileAsync(fileOperator).ConfigureAwait(false);
if (result.IsSuccess)
client.Logger.Info(result.ToString());
else
client.Logger.Warning(result.ToString());
return result.IsSuccess;
}
/// <summary>
/// 客户端上传文件到服务器。
/// </summary>
private static async Task ClientPushFileFromService(TcpDmtpClient client, string serverPath, string resourcePath)
{
var metadata = new Metadata();//传递到服务器的元数据
metadata.Add(FileConst.FilePathKey, serverPath);
var fileOperator = new FileOperator//实例化本次传输的控制器,用于获取传输进度、速度、状态等。
{
SavePath = serverPath,//服务器本地保存路径
ResourcePath = resourcePath,//客户端本地即将上传文件的资源路径
Metadata = metadata,//传递到服务器的元数据
Timeout = TimeSpan.FromSeconds(60),//传输超时时长
TryCount = 10,//当遇到失败时,尝试次数
FileSectionSize = 1024 * 512//分包大小,当网络较差时,应该适当减小该值
};
fileOperator.MaxSpeed = MaxSpeed;//设置最大限速。
//此处的作用相当于Timer定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
if (fileOperator.IsEnd)
{
loop.Dispose();
}
client.Logger.Info($"进度:{(fileOperator.Progress * 100).ToString("F2")}%,速度:{(fileOperator.Speed() / 1024).ToString("F2")} KB/s");
});
_ = loopAction.RunAsync();
//此方法会阻塞直到传输结束也可以使用PushFileAsync
var result = await client.GetDmtpFileTransferActor().PushFileAsync(fileOperator).ConfigureAwait(false);
client.Logger.Info(result.ToString());
}
/// <summary>
/// 底层错误日志输出
/// </summary>
internal void Log_Out(TouchSocket.Core.LogLevel arg1, object arg2, string arg3, Exception arg4)
{
_logger?.Log_Out(arg1, arg2, arg3, arg4);
}
private async Task<TcpDmtpClient> GetTcpDmtpClient()
{
_log = new LoggerGroup() { LogLevel = TouchSocket.Core.LogLevel.Trace };
_log.AddLogger(new EasyLogger(Log_Out) { LogLevel = TouchSocket.Core.LogLevel.Trace });
_log.AddLogger(TextLogger);
var upgradeServerOptions = App.GetOptions<UpgradeServerOptions>();
var config = new TouchSocketConfig()
.SetRemoteIPHost(new IPHost($"{upgradeServerOptions.UpgradeServerIP}:{upgradeServerOptions.UpgradeServerPort}"))
.SetAdapterOption(new AdapterOption() { MaxPackageSize = 0x20000000 })
.SetDmtpOption(new DmtpOption()
{
VerifyToken = upgradeServerOptions.VerifyToken
})
.ConfigureContainer(a =>
{
a.AddLogger(_log);
var rpcServer = new UpgradeRpcServer();
a.AddRpcStore(store => store.RegisterServer<IUpgradeRpcServer>(rpcServer));
a.AddDmtpRouteService();//添加路由策略
})
.ConfigurePlugins(a =>
{
a.UseTcpSessionCheckClear();
a.UseDmtpRpc();
a.UseDmtpFileTransfer();//必须添加文件传输插件
a.Add<FilePlugin>();
a.UseDmtpHeartbeat()//使用Dmtp心跳
.SetTick(TimeSpan.FromSeconds(5))
.SetMaxFailCount(3);
});
TcpDmtpClient client = new();
await client.SetupAsync(config).ConfigureAwait(false);
return client;
}
public override void Dispose()
{
base.Dispose();
TextLogger.Dispose();
}
}

View File

@@ -1,31 +0,0 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using TouchSocket.Dmtp.Rpc;
using TouchSocket.Rpc;
namespace ThingsGateway.Management;
public partial class UpgradeRpcServer : SingletonRpcServer, IUpgradeRpcServer
{
[DmtpRpc]
public void Restart()
{
RestartServerHelper.RestartServer();
}
[DmtpRpc]
public async Task Upgrade()
{
var _updateZipFileService = App.GetService<IUpdateZipFileHostedService>();
var data = await _updateZipFileService.GetList().ConfigureAwait(false);
if (data.Count != 0)
await _updateZipFileService.Update(data.OrderByDescending(a => a.Version).FirstOrDefault()).ConfigureAwait(false);
}
}

View File

@@ -11,7 +11,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
internal sealed class WebApiHostedService : BackgroundService
{

View File

@@ -12,7 +12,7 @@ using System.ComponentModel.DataAnnotations;
using ThingsGateway.ConfigurableOptions;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
public class WebApiOptions : IConfigurableOptions
{
public bool Enable { get; set; }

View File

@@ -12,15 +12,13 @@ using Microsoft.Extensions.Logging;
using System.Text;
using ThingsGateway.Gateway.Application;
using TouchSocket.Core;
using TouchSocket.Http;
using TouchSocket.Rpc;
using TouchSocket.Sockets;
using TouchSocket.WebApi.Swagger;
namespace ThingsGateway.Management;
namespace ThingsGateway.Gateway.Application;
public partial class WebApiTask : AsyncDisposableObject
{
@@ -143,6 +141,7 @@ public partial class WebApiTask : AsyncDisposableObject
_httpService = null;
}
await base.DisposeAsync(disposing).ConfigureAwait(false);
TextLogger?.Dispose();
}
}
/// <summary>

View File

@@ -30,3 +30,20 @@ public class PluginAddInput
/// </summary>
public List<IBrowserFile> OtherFiles { get; set; }
}
/// <summary>
/// 插件添加DTO
/// </summary>
public class PluginAddPathInput
{
/// <summary>
/// 主程序集
/// </summary>
[Required]
public string MainFilePath { get; set; }
/// <summary>
/// 附属程序集
/// </summary>
public List<string> OtherFilePaths { get; set; } = new();
}

View File

@@ -0,0 +1,52 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 驱动插件服务
/// </summary>
public interface IPluginPageService
{
/// <summary>
/// 根据插件类型获取信息
/// </summary>
/// <param name="pluginType"></param>
/// <returns></returns>
Task<List<PluginInfo>> GetPluginListAsync(PluginTypeEnum? pluginType = null);
/// <summary>
/// 分页显示插件
/// </summary>
public Task<QueryData<PluginInfo>> PluginPage(QueryPageOptions options, PluginTypeEnum? pluginTypeEnum = null);
/// <summary>
/// 重载插件
/// </summary>
Task ReloadPlugin();
///// <summary>
///// 添加插件
///// </summary>
///// <param name="plugin"></param>
///// <returns></returns>
//Task SavePlugin(PluginAddInput plugin);
/// <summary>
/// 添加插件
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
Task SavePluginByPath(PluginAddPathInput plugin);
}

View File

@@ -15,11 +15,20 @@ namespace ThingsGateway.Gateway.Application;
/// <summary>
/// 驱动插件服务
/// </summary>
public interface IPluginService
public interface IPluginService : IPluginPageService
{
Type GetDebugUI(string pluginName);
Type GetAddressUI(string pluginName);
/// <summary>
/// 根据插件类型获取信息
/// </summary>
/// <param name="pluginType"></param>
/// <returns></returns>
List<PluginInfo> GetPluginListSync(PluginTypeEnum? pluginType = null);
/// <summary>
/// 根据插件全名称构建插件实例
/// </summary>
@@ -40,12 +49,7 @@ public interface IPluginService
/// <returns></returns>
(IEnumerable<IEditorItem> EditorItems, object Model, Type PropertyUIType) GetDriverPropertyTypes(string pluginName, IDriver? driver = null);
/// <summary>
/// 根据插件类型获取信息
/// </summary>
/// <param name="pluginType"></param>
/// <returns></returns>
List<PluginInfo> GetList(PluginTypeEnum? pluginType = null);
/// <summary>
/// 获取变量属性
@@ -55,22 +59,6 @@ public interface IPluginService
/// <returns></returns>
(IEnumerable<IEditorItem> EditorItems, object Model, Type VariablePropertyUIType) GetVariablePropertyTypes(string pluginName, BusinessBase? businessBase = null);
/// <summary>
/// 分页显示插件
/// </summary>
public QueryData<PluginInfo> Page(QueryPageOptions options, PluginTypeEnum? pluginTypeEnum = null);
/// <summary>
/// 重载插件
/// </summary>
void Reload();
/// <summary>
/// 添加插件
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
Task SavePlugin(PluginAddInput plugin);
/// <summary>
/// 设置插件动态属性

View File

@@ -0,0 +1,142 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Extension;
namespace ThingsGateway.Gateway.Application;
[ThingsGateway.DependencyInjection.SuppressSniffer]
public static class PluginInfoUtil
{
public const string TempDirName = "TempGatewayPlugins";
private static WaitLock _locker = new(nameof(PluginInfoUtil));
/// <summary>
/// 异步保存驱动程序信息。
/// </summary>
public static async Task SavePlugin(PluginAddInput plugin, IPluginPageService pluginPageService)
{
try
{
// 等待锁可用
await _locker.WaitAsync().ConfigureAwait(false);
var maxFileSize = 100 * 1024 * 1024; // 最大100MB
string tempDir = TempDirName;
// 获取主程序集文件名
var mainFileName = Path.GetFileNameWithoutExtension(plugin.MainFile.Name);
// 构建插件文件夹绝对路径
string fullDir = AppContext.BaseDirectory.CombinePathWithOs(tempDir, mainFileName);
Directory.CreateDirectory(fullDir);
PluginAddPathInput pluginAddPathInput = new();
try
{
// 构建主程序集绝对路径
var fullPath = fullDir.CombinePathWithOs(plugin.MainFile.Name);
// 获取主程序集文件流
using (var stream = plugin.MainFile.OpenReadStream(maxFileSize))
{
FileStream fs = new(fullPath, FileMode.Create);
await stream.CopyToAsync(fs).ConfigureAwait(false);
await fs.SafeDisposeAsync().ConfigureAwait(false);
}
pluginAddPathInput.MainFilePath = fullPath;
foreach (var item in plugin.OtherFiles ?? new())
{
// 获取附属文件流
using (var otherStream = item.OpenReadStream(maxFileSize))
{
var otherFullPath = $"{fullDir.CombinePathWithOs(item.Name)}";
FileStream otherFs = new(otherFullPath, FileMode.Create);
await otherStream.CopyToAsync(otherFs).ConfigureAwait(false);
await otherFs.SafeDisposeAsync().ConfigureAwait(false);
pluginAddPathInput.OtherFilePaths.Add(otherFullPath);
}
}
await pluginPageService.SavePluginByPath(pluginAddPathInput).ConfigureAwait(false);
}
finally
{
if (File.Exists(pluginAddPathInput.MainFilePath))
{
File.Delete(pluginAddPathInput.MainFilePath);
}
foreach (var item in pluginAddPathInput.OtherFilePaths)
{
if (File.Exists(item))
{
File.Delete(item);
}
}
}
}
finally
{
// 释放锁资源
_locker.Release();
}
}
/// <summary>
/// 根据插件FullName获取插件主程序集名称和插件类型名称
/// </summary>
/// <param name="pluginName"></param>
/// <returns></returns>
public static (string FileName, string TypeName) GetFileNameAndTypeName(string pluginName)
{
if (pluginName.IsNullOrWhiteSpace())
return (string.Empty, string.Empty);
// 查找最后一个 '.' 的索引
int lastIndex = pluginName.LastIndexOf('.');
// 如果找到了最后一个 '.',并且它不是最后一个字符
if (lastIndex != -1 && lastIndex < pluginName.Length - 1)
{
// 获取子串直到最后一个 '.'
string part1 = pluginName.Substring(0, lastIndex);
// 获取最后一个 '.' 后面的部分
string part2 = pluginName.Substring(lastIndex + 1);
return (part1, part2);
}
else
{
// 如果没有找到 '.',或者 '.' 是最后一个字符,则返回默认的键和插件名称
return (nameof(ThingsGateway), pluginName);
}
}
/// <summary>
/// 根据插件主程序集名称和插件类型名称获取插件FullName
/// </summary>
/// <param name="fileName"></param>
/// <param name="name"></param>
/// <returns></returns>
public static string GetFullName(string fileName, string name)
{
return string.IsNullOrEmpty(fileName) ? name : $"{fileName}.{name}";
}
}

View File

@@ -35,7 +35,7 @@ internal sealed class PluginService : IPluginService
/// </summary>
public const string DirName = "GatewayPlugins";
private const string CacheKeyGetPluginOutputs = $"ThingsGateway.Gateway.Application.{nameof(PluginService)}{nameof(GetList)}";
private const string CacheKeyGetPluginOutputs = $"ThingsGateway.Gateway.Application.{nameof(PluginService)}{nameof(GetPluginListSync)}";
private const string SaveEx = ".save";
private const string DelEx = ".del";
@@ -69,6 +69,7 @@ internal sealed class PluginService : IPluginService
/// </summary>
private System.Collections.ObjectModel.ReadOnlyDictionary<string, Type> _defaultDriverBaseDict { get; }
/// <summary>
/// 插件FullName/插件Type
/// </summary>
@@ -100,7 +101,7 @@ internal sealed class PluginService : IPluginService
_locker.Wait();
// 解析插件名称,获取文件名和类型名
var filtResult = PluginServiceUtil.GetFileNameAndTypeName(pluginName);
var filtResult = PluginInfoUtil.GetFileNameAndTypeName(pluginName);
// 如果是默认键,则搜索主程序上下文中的类型
if (_defaultDriverBaseDict.TryGetValue(pluginName, out var type))
@@ -258,7 +259,29 @@ internal sealed class PluginService : IPluginService
/// </summary>
/// <param name="pluginType">要筛选的插件类型,可选参数</param>
/// <returns>符合条件的插件列表</returns>
public List<PluginInfo> GetList(PluginTypeEnum? pluginType = null)
public Task<List<PluginInfo>> GetPluginListAsync(PluginTypeEnum? pluginType = null)
{
// 获取完整的插件列表
var pluginList = PrivateGetList();
if (pluginType == null)
{
// 如果未指定插件类型,则返回完整的插件列表
return Task.FromResult(pluginList.ToList());
}
// 筛选出指定类型的插件
var filteredPlugins = pluginList.Where(c => c.PluginType == pluginType).ToList();
return Task.FromResult(filteredPlugins);
}
/// <summary>
/// 获取插件信息的方法,可以根据插件类型筛选插件列表。
/// </summary>
/// <param name="pluginType">要筛选的插件类型,可选参数</param>
/// <returns>符合条件的插件列表</returns>
public List<PluginInfo> GetPluginListSync(PluginTypeEnum? pluginType = null)
{
// 获取完整的插件列表
var pluginList = PrivateGetList();
@@ -274,7 +297,6 @@ internal sealed class PluginService : IPluginService
return filteredPlugins;
}
/// <summary>
/// 获取变量的属性类型
/// </summary>
@@ -308,21 +330,21 @@ internal sealed class PluginService : IPluginService
/// <summary>
/// 分页显示插件
/// </summary>
public QueryData<PluginInfo> Page(QueryPageOptions options, PluginTypeEnum? pluginType = null)
public async Task<QueryData<PluginInfo>> PluginPage(QueryPageOptions options, PluginTypeEnum? pluginType = null)
{
//指定关键词搜索为插件FullName
var query = GetList(pluginType).WhereIf(!options.SearchText.IsNullOrWhiteSpace(), a => a.FullName.Contains(options.SearchText)).GetQueryData(options);
var query = (await GetPluginListAsync(pluginType).ConfigureAwait(false)).WhereIf(!options.SearchText.IsNullOrWhiteSpace(), a => a.FullName.Contains(options.SearchText)).GetQueryData(options);
return query;
}
/// <summary>
/// 移除全部插件
/// </summary>
public void Reload()
public async Task ReloadPlugin()
{
try
{
_locker.Wait();
await _locker.WaitAsync().ConfigureAwait(false);
_driverBaseDict.Clear();
foreach (var item in _assemblyLoadContextDict)
{
@@ -335,77 +357,86 @@ internal sealed class PluginService : IPluginService
{
_locker.Release();
}
}
private WaitLock SaveLock = new(nameof(PluginService));
/// <summary>
/// 异步保存驱动程序信息。
/// </summary>
/// <param name="plugin">要保存的插件信息。</param>
/// <param name="pluginAddPathInput">要保存的插件信息。</param>
[OperDesc("SavePlugin", isRecordPar: false, localizerType: typeof(PluginAddInput))]
public async Task SavePlugin(PluginAddInput plugin)
public async Task SavePluginByPath(PluginAddPathInput pluginAddPathInput)
{
try
{
// 等待锁可用
await _locker.WaitAsync().ConfigureAwait(false);
await SaveLock.WaitAsync().ConfigureAwait(false);
var tempDir = DirName;
// 创建程序集加载上下文
var assemblyLoadContext = new AssemblyLoadContext(CommonUtils.GetSingleId().ToString(), true);
// 存储其他文件的内存流列表
List<(string Name, MemoryStream MemoryStream)> otherFilesStreams = new();
var maxFileSize = 100 * 1024 * 1024; // 最大100MB
// 获取主程序集文件名
var mainFileName = Path.GetFileNameWithoutExtension(plugin.MainFile.Name);
var mainFileName = Path.GetFileNameWithoutExtension(pluginAddPathInput.MainFilePath);
string fullDir = string.Empty;
bool isDefaultDriver = false;
//判定是否上下文程序集
var defaultDriver = _defaultDriverBaseDict.FirstOrDefault(a => Path.GetFileNameWithoutExtension(new FileInfo(a.Value.Assembly.Location).Name) == mainFileName);
if (defaultDriver.Value != null)
{
var filtResult = PluginServiceUtil.GetFileNameAndTypeName(defaultDriver.Key);
var filtResult = PluginInfoUtil.GetFileNameAndTypeName(defaultDriver.Key);
fullDir = Path.GetDirectoryName(filtResult.FileName);
isDefaultDriver = true;
}
else
{
// 构建插件文件夹绝对路径
fullDir = AppContext.BaseDirectory.CombinePathWithOs(DirName, mainFileName);
fullDir = AppContext.BaseDirectory.CombinePathWithOs(tempDir, mainFileName);
isDefaultDriver = false;
}
// 获取主程序集文件流
MemoryStream mainMemoryStream = new MemoryStream();
try
{
// 构建主程序集绝对路径
var fullPath = fullDir.CombinePathWithOs(plugin.MainFile.Name);
var fullPath = fullDir.CombinePathWithOs(Path.GetFileName(pluginAddPathInput.MainFilePath));
// 获取主程序集文件流
using var stream = plugin.MainFile.OpenReadStream(maxFileSize);
MemoryStream mainMemoryStream = new MemoryStream();
await stream.CopyToAsync(mainMemoryStream).ConfigureAwait(false);
using (var stream = File.Open(pluginAddPathInput.MainFilePath, FileMode.Open, FileAccess.Read))
{
await stream.CopyToAsync(mainMemoryStream).ConfigureAwait(false);
}
mainMemoryStream.Seek(0, SeekOrigin.Begin);
#region
// 先加载到内存,如果成功添加后再装载到文件
// 加载主程序集
var assembly = assemblyLoadContext.LoadFromStream(mainMemoryStream);
foreach (var item in plugin.OtherFiles ?? new())
foreach (var item in pluginAddPathInput.OtherFilePaths ?? new())
{
// 获取附属文件流
using var otherStream = item.OpenReadStream(maxFileSize);
MemoryStream memoryStream = new MemoryStream();
await otherStream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin);
otherFilesStreams.Add((item.Name, memoryStream));
try
using (var otherStream = File.Open(pluginAddPathInput.MainFilePath, FileMode.Open, FileAccess.Read))
{
// 尝试加载附属程序集
assemblyLoadContext.LoadFromStream(memoryStream);
}
catch (Exception ex)
{
// 加载失败时记录警告信息
_logger?.LogWarning(ex, string.Format(AppResource.LoadOtherFileFail, item));
MemoryStream memoryStream = new MemoryStream();
await otherStream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Seek(0, SeekOrigin.Begin);
otherFilesStreams.Add((Path.GetFileName(item), memoryStream));
try
{
// 尝试加载附属程序集
assemblyLoadContext.LoadFromStream(memoryStream);
}
catch (Exception ex)
{
// 加载失败时记录警告信息
_logger?.LogWarning(ex, string.Format(AppResource.LoadOtherFileFail, item));
}
}
}
#endregion
@@ -451,6 +482,16 @@ internal sealed class PluginService : IPluginService
}
finally
{
if (mainMemoryStream != null)
await mainMemoryStream.SafeDisposeAsync().ConfigureAwait(false);
otherFilesStreams.ForEach(async a =>
{
if (a.MemoryStream != null)
{
await a.MemoryStream.SafeDisposeAsync().ConfigureAwait(false);
}
});
// 卸载程序集加载上下文并清除缓存
assemblyLoadContext.Unload();
@@ -468,8 +509,19 @@ internal sealed class PluginService : IPluginService
}
finally
{
if (File.Exists(pluginAddPathInput.MainFilePath))
{
File.Delete(pluginAddPathInput.MainFilePath);
}
foreach (var item in pluginAddPathInput.OtherFilePaths)
{
if (File.Exists(item))
{
File.Delete(item);
}
}
// 释放锁资源
_locker.Release();
SaveLock.Release();
}
}
@@ -583,7 +635,7 @@ internal sealed class PluginService : IPluginService
if (_assemblyLoadContextDict.TryGetValue(path, out var assemblyLoadContext))
{
//移除字典
_driverBaseDict.RemoveWhere(a => path == PluginServiceUtil.GetFileNameAndTypeName(a.Key).FileName);
_driverBaseDict.RemoveWhere(a => path == PluginInfoUtil.GetFileNameAndTypeName(a.Key).FileName);
_assemblyLoadContextDict.Remove(path);
//卸载
assemblyLoadContext.AssemblyLoadContext.Unload();
@@ -713,6 +765,7 @@ internal sealed class PluginService : IPluginService
{
var plugins = new List<PluginInfo>();
// 主程序上下文
string tempDir = DirName;
// 遍历程序集上下文默认驱动字典,生成默认驱动插件信息
foreach (var item in _defaultDriverBaseDict)
@@ -740,7 +793,7 @@ internal sealed class PluginService : IPluginService
}
// 获取插件文件夹路径列表
string[] folderPaths = Directory.GetDirectories(AppContext.BaseDirectory.CombinePathWithOs(DirName));
string[] folderPaths = Directory.GetDirectories(AppContext.BaseDirectory.CombinePathWithOs(tempDir));
// 遍历插件文件夹
foreach (string folderPath in folderPaths)
@@ -769,7 +822,7 @@ internal sealed class PluginService : IPluginService
{
//添加到字典
_driverBaseDict.TryAdd($"{driverMainName}.{type.Name}", type);
_logger?.LogInformation(string.Format(AppResource.LoadTypeSuccess, PluginServiceUtil.GetFullName(driverMainName, type.Name)));
_logger?.LogInformation(string.Format(AppResource.LoadTypeSuccess, PluginInfoUtil.GetFullName(driverMainName, type.Name)));
}
var plugin = new PluginInfo()
{
@@ -789,10 +842,15 @@ internal sealed class PluginService : IPluginService
catch (Exception ex)
{
// 记录加载插件失败的日志
_logger?.LogWarning(ex, string.Format(AppResource.LoadPluginFail, Path.GetRelativePath(AppContext.BaseDirectory.CombinePathWithOs(DirName), folderPath)));
_logger?.LogWarning(ex, string.Format(AppResource.LoadPluginFail, Path.GetRelativePath(AppContext.BaseDirectory.CombinePathWithOs(tempDir), folderPath)));
}
}
return plugins.DistinctBy(a => a.FullName).OrderBy(a => a.EducationPlugin);
}
}
//public Task SavePlugin(PluginAddInput plugin)
//{
// return PluginInfoUtil.SavePlugin(plugin);
//}
}

View File

@@ -67,44 +67,6 @@ public static class PluginServiceUtil
return cols;
}
/// <summary>
/// 根据插件FullName获取插件主程序集名称和插件类型名称
/// </summary>
/// <param name="pluginName"></param>
/// <returns></returns>
public static (string FileName, string TypeName) GetFileNameAndTypeName(string pluginName)
{
if (pluginName.IsNullOrWhiteSpace())
return (string.Empty, string.Empty);
// 查找最后一个 '.' 的索引
int lastIndex = pluginName.LastIndexOf('.');
// 如果找到了最后一个 '.',并且它不是最后一个字符
if (lastIndex != -1 && lastIndex < pluginName.Length - 1)
{
// 获取子串直到最后一个 '.'
string part1 = pluginName.Substring(0, lastIndex);
// 获取最后一个 '.' 后面的部分
string part2 = pluginName.Substring(lastIndex + 1);
return (part1, part2);
}
else
{
// 如果没有找到 '.',或者 '.' 是最后一个字符,则返回默认的键和插件名称
return (nameof(ThingsGateway), pluginName);
}
}
/// <summary>
/// 根据插件主程序集名称和插件类型名称获取插件FullName
/// </summary>
/// <param name="fileName"></param>
/// <param name="name"></param>
/// <returns></returns>
public static string GetFullName(string fileName, string name)
{
return string.IsNullOrEmpty(fileName) ? name : $"{fileName}.{name}";
}
public static bool HasDynamicProperty(object model)
{

View File

@@ -9,9 +9,11 @@ using ThingsGateway.Blazor.Diagrams.Core.Models;
namespace ThingsGateway.Gateway.Application;
#if !Management
[SugarTable("rules", TableDescription = "规则引擎")]
[SugarIndex("unique_rules_name", nameof(Rules.Name), OrderByType.Asc, true)]
[Tenant(SqlSugarConst.DB_Custom)]
#endif
public class Rules : BaseDataEntity
{
[SugarColumn(ColumnDescription = "名称", Length = 200)]

View File

@@ -6,7 +6,7 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Actuator", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/CSharpScript.svg", Desc = nameof(ExecuteScriptNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.CSharpScriptWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Actuator", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/CSharpScript.svg", Desc = nameof(ExecuteScriptNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.CSharpScriptWidget,ThingsGateway.Gateway.Razor")]
public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBase, IDisposable
{
public ExecuteScriptNode(string id, Point? position = null) : base(id, position)
@@ -84,6 +84,7 @@ public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBa
}
set
{
#if !Management
if (text != value)
{
try
@@ -96,11 +97,11 @@ public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBa
}
CSharpScriptEngineExtension.Remove(text);
}
#endif
text = value;
}
}
#if !Management
async Task<OperResult<NodeOutput>> IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
try
@@ -121,9 +122,15 @@ public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBa
return new OperResult<NodeOutput>(ex);
}
}
#else
Task<OperResult<NodeOutput>> IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
return Task.FromResult(new OperResult<NodeOutput>());
}
#endif
public void Dispose()
{
#if !Management
if (!text.IsNullOrWhiteSpace())
{
try
@@ -136,6 +143,7 @@ public class ExecuteScriptNode : TextNode, IActuatorNode, IExexcuteExpressionsBa
}
CSharpScriptEngineExtension.Remove(text);
}
#endif
GC.SuppressFinalize(this);
}
}

View File

@@ -5,12 +5,14 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Actuator", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/Rpc.svg", Desc = nameof(VariableRpcNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.VariableWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Actuator", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/Rpc.svg", Desc = nameof(VariableRpcNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.VariableWidget,ThingsGateway.Gateway.Razor")]
public class VariableRpcNode : VariableNode, IActuatorNode
{
public VariableRpcNode(string id, Point? position = null) : base(id, position)
{ Title = "VariableRpcNode"; }
#if !Management
async Task<OperResult<NodeOutput>> IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
try
@@ -37,4 +39,10 @@ public class VariableRpcNode : VariableNode, IActuatorNode
return new OperResult<NodeOutput>(ex);
}
}
#else
Task<OperResult<NodeOutput>> IActuatorNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
return Task.FromResult(new OperResult<NodeOutput>());
}
#endif
}

View File

@@ -1,13 +1,15 @@

using ThingsGateway.Blazor.Diagrams.Core.Geometry;
#if !Management
using ThingsGateway.Gateway.Application.Extensions;
#endif
using ThingsGateway.NewLife.Extension;
using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/CSharpScript.svg", Desc = nameof(ConditionNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.CSharpScriptWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/CSharpScript.svg", Desc = nameof(ConditionNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.CSharpScriptWidget,ThingsGateway.Gateway.Razor")]
public class ConditionNode : TextNode, IConditionNode
{
public ConditionNode(string id, Point? position = null) : base(id, position)
@@ -15,6 +17,7 @@ public class ConditionNode : TextNode, IConditionNode
Title = "ConditionNode"; Placeholder = "ConditionNode.Placeholder";
Text = "return true;";
}
#if !Management
Task<bool> IConditionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
@@ -23,4 +26,10 @@ public class ConditionNode : TextNode, IConditionNode
Logger?.Trace($"Condition result: {next}");
return Task.FromResult(next);
}
#else
Task<bool> IConditionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
#endif
}

View File

@@ -1,12 +1,15 @@

using ThingsGateway.Blazor.Diagrams.Core.Geometry;
#if !Management
using ThingsGateway.Gateway.Application.Extensions;
#endif
using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/CSharpScript.svg", Desc = nameof(DataNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.CSharpScriptWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/CSharpScript.svg", Desc = nameof(DataNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.CSharpScriptWidget,ThingsGateway.Gateway.Razor")]
public class DataNode : TextNode, IExpressionNode
{
public DataNode(string id, Point? position = null) : base(id, position)
@@ -14,6 +17,7 @@ public class DataNode : TextNode, IExpressionNode
Title = "DataNode"; Placeholder = "DataNode.Placeholder";
Text = "return 1;";
}
#if !Management
Task<OperResult<NodeOutput>> IExpressionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
@@ -31,4 +35,10 @@ public class DataNode : TextNode, IExpressionNode
return Task.FromResult(new OperResult<NodeOutput>(ex));
}
}
#else
Task<OperResult<NodeOutput>> IExpressionNode.ExecuteAsync(NodeInput input, CancellationToken cancellationToken)
{
return Task.FromResult(new OperResult<NodeOutput>());
}
#endif
}

View File

@@ -5,7 +5,7 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/Delay.svg", Desc = nameof(DelayNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.NumberWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Expression", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/Delay.svg", Desc = nameof(DelayNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.NumberWidget,ThingsGateway.Gateway.Razor")]
public class DelayNode : NumberNode, IExpressionNode
{
public DelayNode(string id, Point? position = null) : base(id, position) { Title = "DelayNode"; Placeholder = "DelayNode.Placeholder"; }

View File

@@ -3,7 +3,7 @@ using ThingsGateway.Blazor.Diagrams.Core.Geometry;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Start/End", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/Start.svg", Desc = nameof(StartNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.DefaultWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Start/End", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/Start.svg", Desc = nameof(StartNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.DefaultWidget,ThingsGateway.Gateway.Razor")]
public class StartNode : BaseNode, IStartNode
{
public StartNode(string id, Point? position = null) : base(id, position)

View File

@@ -8,11 +8,13 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/ValueChanged.svg", Desc = nameof(AlarmChangedTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.VariableWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/ValueChanged.svg", Desc = nameof(AlarmChangedTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.VariableWidget,ThingsGateway.Gateway.Razor")]
public class AlarmChangedTriggerNode : VariableNode, ITriggerNode, IDisposable
{
public AlarmChangedTriggerNode(string id, Point? position = null) : base(id, position) { Title = "AlarmChangedTriggerNode"; }
#if !Management
private Func<NodeOutput, CancellationToken, Task> Func { get; set; }
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
{
@@ -104,4 +106,20 @@ public class AlarmChangedTriggerNode : VariableNode, ITriggerNode, IDisposable
alarmChangedTriggerNodes.Remove(this);
}
}
#else
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Dispose()
{
}
#endif
}

View File

@@ -8,11 +8,13 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/ValueChanged.svg", Desc = nameof(DeviceChangedTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.DeviceWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/ValueChanged.svg", Desc = nameof(DeviceChangedTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.DeviceWidget,ThingsGateway.Gateway.Razor")]
public class DeviceChangedTriggerNode : TextNode, ITriggerNode, IDisposable
{
public DeviceChangedTriggerNode(string id, Point? position = null) : base(id, position) { Title = "DeviceChangedTriggerNode"; Placeholder = "Device.Placeholder"; }
#if !Management
private Func<NodeOutput, CancellationToken, Task> Func { get; set; }
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
{
@@ -90,4 +92,18 @@ public class DeviceChangedTriggerNode : TextNode, ITriggerNode, IDisposable
list.Remove(this);
}
}
#else
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Dispose()
{
}
#endif
}

View File

@@ -5,7 +5,7 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/TimeInterval.svg", Desc = nameof(TimeIntervalTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.TextWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/TimeInterval.svg", Desc = nameof(TimeIntervalTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.TextWidget,ThingsGateway.Gateway.Razor")]
public class TimeIntervalTriggerNode : TextNode, ITriggerNode, IDisposable
{
~TimeIntervalTriggerNode()
@@ -14,6 +14,7 @@ public class TimeIntervalTriggerNode : TextNode, ITriggerNode, IDisposable
}
public TimeIntervalTriggerNode(string id, Point? position = null) : base(id, position) { Title = "TimeIntervalTriggerNode"; Placeholder = "TimeIntervalTriggerNode.Placeholder"; }
#if !Management
private IScheduledTask _task;
private Func<NodeOutput, CancellationToken, Task> Func { get; set; }
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
@@ -51,4 +52,21 @@ public class TimeIntervalTriggerNode : TextNode, ITriggerNode, IDisposable
GC.SuppressFinalize(this);
}
#else
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Dispose()
{
}
#endif
}

View File

@@ -8,11 +8,13 @@ using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/ValueChanged.svg", Desc = nameof(ValueChangedTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.DefaultDiagram), WidgetType = "ThingsGateway.Gateway.Razor.VariableWidget,ThingsGateway.Gateway.Razor")]
[CategoryNode(Category = "Trigger", ImgUrl = "_content/ThingsGateway.Gateway.Razor/img/ValueChanged.svg", Desc = nameof(ValueChangedTriggerNode), LocalizerType = typeof(ThingsGateway.Gateway.Application.INode), WidgetType = "ThingsGateway.Gateway.Razor.VariableWidget,ThingsGateway.Gateway.Razor")]
public class ValueChangedTriggerNode : VariableNode, ITriggerNode, IDisposable
{
public ValueChangedTriggerNode(string id, Point? position = null) : base(id, position) { Title = "ValueChangedTriggerNode"; }
#if !Management
private Func<NodeOutput, CancellationToken, Task> Func { get; set; }
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
{
@@ -100,4 +102,20 @@ public class ValueChangedTriggerNode : VariableNode, ITriggerNode, IDisposable
valueChangedTriggerNodes.Remove(this);
}
}
#else
Task ITriggerNode.StartAsync(Func<NodeOutput, CancellationToken, Task> func, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Dispose()
{
}
#endif
}

View File

@@ -8,13 +8,11 @@
// QQ群605534569
// ------------------------------------------------------------------------------
using Microsoft.Extensions.Hosting;
using ThingsGateway.Blazor.Diagrams.Core;
namespace ThingsGateway.Gateway.Application;
public interface IRulesEngineHostedService : IHostedService
public interface IRulesEngineHostedService
{
Dictionary<RulesLog, Diagram> Diagrams { get; }
Task Delete(IEnumerable<long> ids);

View File

@@ -20,17 +20,6 @@ using ThingsGateway.NewLife;
using TouchSocket.Core;
namespace ThingsGateway.Gateway.Application;
public class RulesLog
{
public RulesLog(Rules rules, TextFileLogger log)
{
Log = log;
Rules = rules;
}
public TextFileLogger Log { get; set; }
public Rules Rules { get; set; }
}
internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngineHostedService
{
@@ -97,7 +86,7 @@ internal sealed class RulesEngineHostedService : BackgroundService, IRulesEngine
private (RulesLog rulesLog, DefaultDiagram blazorDiagram) Init(Rules rules)
{
#pragma warning disable CA1863
var log = TextFileLogger.GetMultipleFileLogger(string.Format(LogPathFormat, rules.Name));
var log = TextFileLogger.GetMultipleFileLogger(string.Format(LogPathFormat, rules.Name.SanitizeFileName()));
#pragma warning restore CA1863
log.LogLevel = TouchSocket.Core.LogLevel.Trace;
DefaultDiagram blazorDiagram = new();

Some files were not shown because too many files have changed in this diff Show More